open-feature / flagd

A feature flag daemon with a Unix philosophy
https://openfeature.dev
Apache License 2.0
569 stars 67 forks source link

[FEATURE] Support for switch based targeting #1258

Closed janslow closed 7 months ago

janslow commented 8 months ago

Requirements

Hey, I'm evaluating flagd and I was wondering if you'd consider implementing an operator that looks up a variant based on the value of a variable?

The use case is that we currently have lots of per-customer targeting in our current system. Whilst some of these can hopefully be replaced with more flexible rules (which is why I'm looking at OpenFeature/flagd), many of them will need to stay this way (e.g., we need to communicate with customers individually before changing which variant they use).

For example, based on the value of the customerId context variable, I'd want the example flag to evaluate as follows:

Currently, the only way to do this would be to have lots of nested if statements, which can get very complex very quickly:

  "targeting": {
    "if": [
      { "===": [{ "var": "customerId" }, "customer-A"] },
      "variantA",
      {
        "if": [
          { "in": [{ "var": "customerId" }, ["customer-B1", "customer-B2"]] },
          "variantB",
          "variantC"
        ]
      }
    ]
  }

Proposal 1

Add a switch rule which acts like a switch expression in Go/JavaScript/Java, which takes an input expression, searches all provided "cases" for an exact match and returns the corresponding expression, or returns a default expression.

For example, the following flagd config would implement the above use case

{
  "$schema": "https://flagd.dev/schema/v0/flags.json",
  "flags": {
    "example": {
      "variants": {
        "variantA": "A",
        "variantB": "B",
        "variantC": "C"
      },
      "targeting": {
        "switch": {
          "expression": { "var": "customerId" },
          "cases": {
            // "<searchValue>": "<result>"
            "customer-A": "variantA",
            "customer-B1": "variantB",
            "customer-B2": "variantB"
          },
          "default": "variantC"
        }
      },
      "state": "ENABLED",
      "defaultVariant": "variantC"
    }
  }
}

A slight variation on the design would be for cases to be an array of tuples (e.g., [["customer-A", "variantA"], ...]) or array of objects (e.g., [{ "searchValue": "customer-A", "result": "variantA" }, ...]), to support any primitive as case keys (i.e., { 1: "variantA" } isn't valid JSON).

This can be efficiently evaluated (as long as the case keys are required to be primitives), by pre-building a hash map (or similar) for fast lookups

Proposal 2

Alternatively, a more generic option would be to improve support for "else if" style statements, so that the nested ifs in my original example can be flattened.

For example,

  "targeting": {
    "if": [
      // if
      { "===": [{ "var": "customerId" }, "customer-A"] },
      "variantA",
      // else if
      { "in": [{ "var": "customerId" }, ["customer-B1", "customer-B2"]] },
      "variantB",
      // else
      "variantC"
    ]
  }

This syntax is similar to an IFS expression in most spreadsheet software, with the addition of support for defaults (if there's an odd number of entries, there's a default value).

The disadvantage of this is that it's much harder to efficiently evaluate this expression when there's a large number of conditions (e.g., you'd need to look at each case statement and realise that they're all matchers for a single common expression, then build a hash map)

colebaileygit commented 8 months ago

What's interesting is proposal 2 is already supported by jsonlogic, and based on my testing also works fine with flagd and in-memory providers. You do get some warnings but it works - should we just update the schema to reflect this or is there some reason this is discouraged?

Additionally I found that cat from jsonlogic is not allowed inside the first argument of fractional, but then when I run it anyway it actually works as expected: Screenshot 2024-03-28 at 21 47 30

beeme1mr commented 8 months ago

Hey @janslow, thanks for bringing this up. I agree that nested logic is difficult to work with. JsonLogic, the rules engine we leverage behind the scenes, supports chained if/then/else/then logic but some of the libraries we're using do not. I've created and linked issues to add support to those libraries. If those changes are accepted, we can update the JSON schema to allow for chained conditions.

We could potentially add a switch statement, but I would prefer to see if we can get chained if conditions working.

beeme1mr commented 8 months ago

Hey @janslow, good news! The second proposal is already possible You can test it in the flagd playground here. We'll need to update the JSON schema so it isn't flagged as invalid. I've created an issue that captures the requirements.

image

beeme1mr commented 7 months ago

Support for chainable if conditions can be tracked here.