flagd icon indicating copy to clipboard operation
flagd copied to clipboard

chore(ADR): Create fractional-non-string-rand-units.md

Open cupofcat opened this issue 3 months ago • 9 comments

This PR

  • Proposes Support Non-String Inputs for Fractional Bucketing ADR

Related Issues

https://github.com/open-feature/flagd/issues/1737

cupofcat avatar Aug 21 '25 11:08 cupofcat

Deploy Preview for polite-licorice-3db33c ready!

Name Link
Latest commit a50f893a91628d01b26a33e458b30bd52aef0831
Latest deploy log https://app.netlify.com/projects/polite-licorice-3db33c/deploys/68b7287d959aea00081dbece
Deploy Preview https://deploy-preview-1783--polite-licorice-3db33c.netlify.app
Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

netlify[bot] avatar Aug 21 '25 11:08 netlify[bot]

I've been thinking about this more and I wonder WDYT about this approach:

We take this pre-1.0 opportunity to introduce a breaking change for how hashing / bucketing works:

  • If fractional arguments are all arrays, then we use targetingKey for hashing, similar to today
  • Otherwise, the first argument needs to be an object ({})

If the first argument is an object, it's evaluated and the evaluated value is passed to CBOR for marshalling into a consistent byte array. That byte array is passed to MurMur3 for hashing.

If targetingKey is used, a 2 element array is created with the first element being the flag key, and the second element being the targetingKey value. That array is passed to CBOR for marshalling and the resulting byte array is passed to MurMur3 for hashing.

CBOR libraries in each language ensure that the same values get the same byte encoding and murmur3 libraries will ensure that same byte arrays will get the same hash. This works across any type, even for strings.

That way we have langauge-agnostic, stable, and consistent bucketing.

CBOR is specified in an Internet Standard RFC by IETF, which should mean this stays stable for foreseeable future.

cupofcat avatar Aug 22 '25 11:08 cupofcat

We take this pre-1.0 opportunity to introduce a breaking change for how hashing / bucketing works:

  • If fractional arguments are all arrays, then we use targetingKey for hashing, similar to today
  • Otherwise, the first argument needs to be an object ({})

@cupofcat would you mind showing an example configuration of what you had in mind? When you say object, do you mean referencing a context attribute { "var": "useId" } or something else?

beeme1mr avatar Aug 22 '25 13:08 beeme1mr

We take this pre-1.0 opportunity to introduce a breaking change for how hashing / bucketing works:

  • If fractional arguments are all arrays, then we use targetingKey for hashing, similar to today
  • Otherwise, the first argument needs to be an object ({})

@cupofcat would you mind showing an example configuration of what you had in mind? When you say object, do you mean referencing a context attribute { "var": "useId" } or something else?

I meant that essentially the first item in fractional is {...} and not a list. It can be { var: ...} or something else. I'm open to other suggestions, we just need some way to detect if we should use targetingKey or evaluate the first argument for hashing input.

I would want to keep it pretty generic though. In theory, a list of elements could also be a valid input to hashing.

Edit: The above did not make sense, sorry.

@beeme1mr I've been thinking about this more and I think we have the following options:

  1. Similarly to today we have an optional 1st argument to fractional to provide value to hash for bucketing.
  2. We make the first argument mandatory; if someone wants to use targetingKey they need to provide null as first argument
  3. We introduce a new custom operator to be used inside fractional A. something like cbor-8949-bytes that is guaranteed to return a consistent byte array in every language B. something like object that preserves the keys and evaluates the values C. we can brainstorm other custom operator with other semantics too

For (1), since the first argument is optional, we need a way to distinguish it from the subsequent ones. Today, this is done based on type (if it casts to a string, it's treated specially). That means the first argument, if the user wants to use it for bucketing, cannot evaluate to an array. If it is an array we cannot distinguish it from the rest of fractional values. We could go further, and look into types inside the array and say it cannot start with [string, number] but overall I think this is a really hacky semantic. We could say, that if it evaluates to a map/dictionary/object then we encode it to bytes and hash that. However, I cannot seem to figure out how to return an object in JSONLogic using existing operators...

(2) is much clearer, but the semantic of the current schema changes (the schema itself can stay the same). We make the first argument mandatory as the bucketing input. If it's null we use the targetingKey. This is a pretty big breaking change, as all the flag configs that rely on targetingKey would break. But, perhaps, this is OK when moving to 1.0.

(3A) Similarly to today, if the first value in fractional array is bytes we hash that. If not, we use the current behavior as is.

"fractional": [
  {
    "cbor-8949-bytes": [{"var" : "$flagd.flagKey"}, {"var" : "some-var"}]
  },
  ["a", 50], ...
]

(3B) Similarly to today, if the first value in fractional array is a map/dict we hash that. If not, we use the current behavior as is.

"fractional": [
  {
    "object": {
      "comment" : "Any keys are valid, we will just translate all of this to bytes (JSONLogic would still evaluate the values)",
      "key1" : {"var" : "$flagd.flagKey"},
      "key2" : {"var" : "some-var"},
      "extra-salt" : "himalayan"
    }
  },
  ["a", 50], ...
]

If none of the above works, perhaps there is also:

  1. We introduce a new custom operator replacing fractional. We can call it fractional1 or fractionalWithKey or fractionalBytes or something. We make it officially recommended for flagd 1.0 and we deprecate fractional. We could keep evaluating fractional using current logic with warnings. We could also remove it completely from 1.0, given that this will be a pretty big breaking change anyway for the users.

WDYT?

P.S. What I find complicates this exercise is that today we send the entire contents of targeting to JSONLogic for evaluation. So we cannot really do any parsing on the structure of the object. Our fractional evaluation implementation already receives evaluated values so we don't know what the arguments looked like.

So I guess there is also:

  1. Parse either targeting or fractional looking for specific fields to control the behavior of what gets passed to JSONLogic and how.

cupofcat avatar Aug 22 '25 13:08 cupofcat

Based on the discussions on Slack I updated this ADR to focus on hashing improvements in the existing fractional only.

cupofcat avatar Sep 01 '25 12:09 cupofcat

I updated the ADR to explicitly require CBOR encodings + some other minor improvements based on feedback.

cupofcat avatar Sep 02 '25 17:09 cupofcat

I am in favor of this proposal, but I can't help but consider this a bit of a blocker.

@beeme1mr WDYT about this one? If we cant append the flag key, should we remove this in the string case? I'd prefer not to have string as a "special case".

toddbaert avatar Oct 27 '25 19:10 toddbaert

I am in favor of this proposal, but I can't help but consider this a bit of a blocker.

@beeme1mr WDYT about this one? If we cant append the flag key, should we remove this in the string case? I'd prefer not to have string as a "special case".

I would not necessarily consider this a special case for strings. It's a result of JSONLogic rather than flagd approach (JSON Logic has "cat" operator for strings but does not have anything like that for non-string types). We can fix that by implementing an additional custom operator.

That way we keep the new approach backwards compatible with old configs (and, tbh, there is no way around this - "cat" is a feature of JSONLogic, not flagd, so we cannot really block it unless we do some hacks).

cupofcat avatar Oct 29 '25 15:10 cupofcat