Skip to content

Commit

Permalink
feat: Identity overrides in local evaluation mode
Browse files Browse the repository at this point in the history
- Rename `Identity.flags` to `Identity.identity_features`
- Store identity overrides by identifier
- Use stored identities when evaluating identity flags
- Use a JSON file fixture for tests
  • Loading branch information
khvn26 committed Apr 5, 2024
1 parent 31eed75 commit 2d7b95d
Show file tree
Hide file tree
Showing 7 changed files with 234 additions and 17 deletions.
4 changes: 2 additions & 2 deletions lib/flagsmith_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,8 @@ defmodule Flagsmith.Client do

case Tesla.post(http_client(config), @api_paths.identities, query) do
{:ok, %{status: status, body: body}} when status >= 200 and status < 300 ->
with %Schemas.Identity{flags: flags} <- Schemas.Identity.from_response(body),
flags <- build_flags(flags, config) do
with %Schemas.Identity{identity_features: identity_features} <- Schemas.Identity.from_response(body),
flags <- build_flags(identity_features, config) do
{:ok, flags}
else
error ->
Expand Down
28 changes: 22 additions & 6 deletions lib/flagsmith_client_poller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ defmodule Flagsmith.Client.Poller do
:configuration,
:environment,
:refresh,
:refresh_monitor
:refresh_monitor,
identities_with_overrides: %{}
]

#################################
Expand Down Expand Up @@ -114,9 +115,16 @@ defmodule Flagsmith.Client.Poller do

def handle_event({:call, from}, {:get_identity_flags, identifier, traits}, _, %__MODULE__{
environment: env,
configuration: config
configuration: config,
identities_with_overrides: overrides
}) do
identity = Schemas.Identity.from_id_traits(identifier, traits, env.api_key)
identity =
case Map.get(overrides, identifier) do
nil ->
Schemas.Identity.from_id_traits(identifier, traits, env.api_key)
existing ->
%Schemas.Identity{existing | traits: Flagsmith.Schemas.Traits.Trait.from(traits)}
end

flags =
env
Expand Down Expand Up @@ -148,7 +156,7 @@ defmodule Flagsmith.Client.Poller do
def handle_event(:internal, :initial_load, :loading, %__MODULE__{configuration: config} = data) do
case Flagsmith.Client.get_environment_request(config) do
{:ok, environment} ->
{:next_state, :on, %__MODULE__{data | environment: environment},
{:next_state, :on, update_data(data, environment),
[{:next_event, :internal, :set_refresh}]}

error ->
Expand Down Expand Up @@ -209,7 +217,7 @@ defmodule Flagsmith.Client.Poller do
# a process other than the one we have stored under the `:refresh_monitor` key
# we still make sure it's matching.
#
# Then we just check if the response is an `:ok` tuple with an `Environment.t`
# Then we just check if the response is an `:ok` tuple with an `Environment.t`
# we replace the `:environment` key on our statem data and following user queries
# will receive the new env or flags. If not we let it stay as is.
#
Expand All @@ -222,7 +230,7 @@ defmodule Flagsmith.Client.Poller do
) do
case result do
{:ok, %Schemas.Environment{} = env} ->
{:keep_state, %{data | refresh_monitor: nil, environment: env},
{:keep_state, update_data(data, env),
[{:next_event, :internal, :set_refresh}]}

error ->
Expand Down Expand Up @@ -252,6 +260,14 @@ defmodule Flagsmith.Client.Poller do
%__MODULE__{configuration: config, refresh: refresh_milliseconds}
end

# Update identities with overrides along with the environment.
defp update_data(data, environment) do
%{data | refresh_monitor: nil, environment: environment, identities_with_overrides: Enum.reduce(
environment.identity_overrides, %{}, fn identity, acc -> Map.put(acc, identity.identifier, identity)
end)}
end


@doc false
# this function is just so we can spawn a proper function with an MFA tuple
def get_environment(pid, config) do
Expand Down
2 changes: 1 addition & 1 deletion lib/flagsmith_engine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ defmodule Flagsmith.Engine do
feature_states: fs,
project: %Environment.Project{segments: segments}
} = env,
%Identity{flags: identity_features} = identity,
%Identity{identity_features: identity_features} = identity,
override_traits \\ []
) do
with identity <- Identity.set_env_key(identity, env),
Expand Down
10 changes: 4 additions & 6 deletions lib/schemas/identity.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ defmodule Flagsmith.Schemas.Identity do
field(:django_id, :integer)
field(:identifier, :string)
field(:environment_key, :string)
embeds_many(:flags, Flagsmith.Schemas.Features.FeatureState)
embeds_many(:identity_features, Flagsmith.Schemas.Features.FeatureState)
embeds_many(:traits, Flagsmith.Schemas.Traits.Trait)
end

Expand All @@ -24,7 +24,7 @@ defmodule Flagsmith.Schemas.Identity do
|> cast(params, [:identifier, :environment_key, :django_id])
|> validate_required([:identifier])
|> cast_embed(:traits)
|> cast_embed(:flags)
|> cast_embed(:identity_features)
end

@doc false
Expand All @@ -43,16 +43,14 @@ defmodule Flagsmith.Schemas.Identity do
@doc false
@spec from_response(element :: map() | list(map())) :: __MODULE__.t() | any()
def from_response(element) when is_map(element) do
element
Map.put(element, "identity_features", Map.get(element, "flags"))
|> changeset()
|> apply_changes()
end

def from_response(elements) when is_list(elements) do
Enum.map(elements, fn element ->
element
|> changeset()
|> apply_changes()
from_response(element)
end)
end

Expand Down
172 changes: 172 additions & 0 deletions test/data/environment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
{
"api_key": "cU3oztxgvRgZifpLepQJTX",
"feature_states": [
{
"django_id": 72267,
"enabled": false,
"feature": {
"id": 13534,
"name": "header_size",
"type": "MULTIVARIATE"
},
"feature_state_value": "24px",
"featurestate_uuid": "16c5a45c-1d9c-4f44-bebe-5b73d60f897d",
"multivariate_feature_state_values": [
{
"id": 2915,
"multivariate_feature_option": {
"id": 849,
"value": "34px"
},
"mv_fs_value_uuid": "448a7777-91cf-47b0-bf16-a4d566ef7745",
"percentage_allocation": 60.0
}
]
},
{
"django_id": 72269,
"enabled": false,
"feature": {
"id": 13535,
"name": "body_size",
"type": "STANDARD"
},
"feature_state_value": "18px",
"featurestate_uuid": "c3c61a9a-f153-46b2-8e9e-dd80d6529201",
"multivariate_feature_state_values": []
},
{
"django_id": 92461,
"enabled": true,
"feature": {
"id": 17985,
"name": "secret_button",
"type": "STANDARD"
},
"feature_state_value": "{\"colour\": \"#ababab\"}",
"featurestate_uuid": "d6bbf961-1752-4548-97d1-02d60cc1ab44",
"multivariate_feature_state_values": []
},
{
"django_id": 94235,
"enabled": true,
"feature": {
"id": 18382,
"name": "test_identity",
"type": "STANDARD"
},
"feature_state_value": "very_yes",
"featurestate_uuid": "aa1a4512-b1c7-44d3-a263-c21676852a52",
"multivariate_feature_state_values": []
}
],
"id": 11278,
"identity_overrides": [
{
"identifier": "overridden-id",
"identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01",
"created_date": "2019-08-27T14:53:45.698555Z",
"updated_at": "2023-07-14 16:12:00.000000",
"environment_api_key": "cU3oztxgvRgZifpLepQJTX",
"identity_features": [
{
"feature": {
"id": 18382,
"name": "test_identity",
"type": "STANDARD"
},
"feature_state_value": "some-overridden-value",
"enabled": false
}
]
}
],
"project": {
"hide_disabled_flags": false,
"id": 4732,
"name": "testing-api",
"organisation": {
"feature_analytics": false,
"id": 4131,
"name": "Mr. Bojangles Inc",
"persist_trait_data": true,
"stop_serving_flags": false
},
"segments": [
{
"feature_states": [
{
"django_id": 95632,
"enabled": true,
"feature": {
"id": 17985,
"name": "secret_button",
"type": "STANDARD"
},
"feature_state_value": "",
"featurestate_uuid": "3b58d149-fdb3-4815-b537-6583291523dd",
"multivariate_feature_state_values": []
}
],
"id": 5241,
"name": "test_segment",
"rules": [
{
"conditions": [],
"rules": [
{
"conditions": [
{
"operator": "EQUAL",
"property_": "show_popup",
"value": "false"
}
],
"rules": [],
"type": "ANY"
}
],
"type": "ALL"
}
]
},
{
"feature_states": [
{
"django_id": 95631,
"enabled": true,
"feature": {
"id": 17985,
"name": "secret_button",
"type": "STANDARD"
},
"feature_state_value": "",
"featurestate_uuid": "adb486aa-563d-4b1d-9f72-bf5b210bf94f",
"multivariate_feature_state_values": []
}
],
"id": 5243,
"name": "test_perc",
"rules": [
{
"conditions": [],
"rules": [
{
"conditions": [
{
"operator": "PERCENTAGE_SPLIT",
"property_": "",
"value": "20"
}
],
"rules": [],
"type": "ANY"
}
],
"type": "ALL"
}
]
}
]
}
}
31 changes: 31 additions & 0 deletions test/flagsmith_poller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,37 @@ defmodule Flagsmith.Client.Poller.Test do
%{trait_key: "show_popup", trait_value: false}
])

# finally, verify that identity overrides work correctly
assert {:ok,
%Flagsmith.Schemas.Flags{
__configuration__: %Flagsmith.Configuration{},
flags: %{
"body_size" => %Flagsmith.Schemas.Flag{
enabled: false,
feature_name: "body_size",
value: "18px"
},
"header_size" => %Flagsmith.Schemas.Flag{
enabled: false,
feature_name: "header_size",
value: "34px"
},
"secret_button" => %Flagsmith.Schemas.Flag{
enabled: true,
feature_name: "secret_button",
value: nil
},
"test_identity" => %Flagsmith.Schemas.Flag{
enabled: false,
feature_name: "test_identity",
value: "some-overridden-value"
}
}
}} =
Flagsmith.Client.get_identity_flags(config, "overridden-id", [
%{trait_key: "show_popup", trait_value: false}
])

# sanity check that nowhere did the poller process exit/crash
assert ^pid = Flagsmith.Client.Poller.whereis(config.environment_key)
end
Expand Down
4 changes: 2 additions & 2 deletions test/support/generators.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule Flagsmith.Engine.Test.Generators do
alias Flagsmith.Schemas.Traits.Trait.Value

def json_env() do
"{\"api_key\":\"cU3oztxgvRgZifpLepQJTX\",\"feature_states\":[{\"django_id\":72267,\"enabled\":false,\"feature\":{\"id\":13534,\"name\":\"header_size\",\"type\":\"MULTIVARIATE\"},\"feature_state_value\":\"24px\",\"featurestate_uuid\":\"16c5a45c-1d9c-4f44-bebe-5b73d60f897d\",\"multivariate_feature_state_values\":[{\"id\":2915,\"multivariate_feature_option\":{\"id\":849,\"value\":\"34px\"},\"mv_fs_value_uuid\":\"448a7777-91cf-47b0-bf16-a4d566ef7745\",\"percentage_allocation\":60.0}]},{\"django_id\":72269,\"enabled\":false,\"feature\":{\"id\":13535,\"name\":\"body_size\",\"type\":\"STANDARD\"},\"feature_state_value\":\"18px\",\"featurestate_uuid\":\"c3c61a9a-f153-46b2-8e9e-dd80d6529201\",\"multivariate_feature_state_values\":[]},{\"django_id\":92461,\"enabled\": true,\"feature\":{\"id\":17985,\"name\":\"secret_button\",\"type\":\"STANDARD\"},\"feature_state_value\":\"{\\\"colour\\\": \\\"#ababab\\\"}\",\"featurestate_uuid\":\"d6bbf961-1752-4548-97d1-02d60cc1ab44\",\"multivariate_feature_state_values\":[]},{\"django_id\":94235,\"enabled\":true,\"feature\":{\"id\":18382,\"name\":\"test_identity\",\"type\":\"STANDARD\"},\"feature_state_value\":\"very_yes\",\"featurestate_uuid\":\"aa1a4512-b1c7-44d3-a263-c21676852a52\",\"multivariate_feature_state_values\":[]}],\"id\":11278,\"project\":{\"hide_disabled_flags\":false,\"id\":4732,\"name\":\"testing-api\",\"organisation\":{\"feature_analytics\":false,\"id\":4131,\"name\":\"Mr. Bojangles Inc\",\"persist_trait_data\":true,\"stop_serving_flags\":false},\"segments\":[{\"feature_states\":[{\"django_id\":95632,\"enabled\":true,\"feature\":{\"id\":17985,\"name\":\"secret_button\",\"type\":\"STANDARD\"},\"feature_state_value\":\"\",\"featurestate_uuid\":\"3b58d149-fdb3-4815-b537-6583291523dd\",\"multivariate_feature_state_values\":[]}],\"id\":5241,\"name\":\"test_segment\",\"rules\":[{\"conditions\":[],\"rules\":[{\"conditions\":[{\"operator\":\"EQUAL\",\"property_\":\"show_popup\",\"value\":\"false\"}],\"rules\":[],\"type\":\"ANY\"}],\"type\":\"ALL\"}]},{\"feature_states\":[{\"django_id\":95631,\"enabled\":true,\"feature\":{\"id\":17985,\"name\":\"secret_button\",\"type\":\"STANDARD\"},\"feature_state_value\":\"\",\"featurestate_uuid\":\"adb486aa-563d-4b1d-9f72-bf5b210bf94f\",\"multivariate_feature_state_values\":[]}],\"id\":5243,\"name\":\"test_perc\",\"rules\":[{\"conditions\":[],\"rules\":[{\"conditions\":[{\"operator\":\"PERCENTAGE_SPLIT\",\"property_\":\"\",\"value\":\"20\"}],\"rules\":[],\"type\":\"ANY\"}],\"type\":\"ALL\"}]}]}}"
File.read!("test/data/environment.json")
end

def map_env(), do: Jason.decode!(json_env())
Expand Down Expand Up @@ -171,7 +171,7 @@ defmodule Flagsmith.Engine.Test.Generators do

def full_identity() do
%Identity{
flags: [
identity_features: [
%Features.FeatureState{
enabled: false,
environment: 11278,
Expand Down

0 comments on commit 2d7b95d

Please sign in to comment.