Skip to content

Commit

Permalink
feat: Identity overrides in local evaluation mode (#34)
Browse files Browse the repository at this point in the history
* feat: Add Environment.identity_overrides, remove integrations config attributes

* feat: Identity overrides in local evaluation mode
- 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

* chore: Bump version
  • Loading branch information
khvn26 authored Apr 9, 2024
1 parent 83abd38 commit d10e496
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 37 deletions.
5 changes: 3 additions & 2 deletions lib/flagsmith_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,9 @@ 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
37 changes: 30 additions & 7 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,17 @@ 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 +157,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 +218,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,8 +231,7 @@ defmodule Flagsmith.Client.Poller do
) do
case result do
{:ok, %Schemas.Environment{} = env} ->
{:keep_state, %{data | refresh_monitor: nil, environment: env},
[{:next_event, :internal, :set_refresh}]}
{:keep_state, update_data(data, env), [{:next_event, :internal, :set_refresh}]}

error ->
Logger.error(
Expand Down Expand Up @@ -252,6 +260,21 @@ 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
%__MODULE__{
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: 2 additions & 8 deletions lib/schemas/environment.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ defmodule Flagsmith.Schemas.Environment do
typed_embedded_schema do
field(:api_key, :string)
embeds_many(:feature_states, __MODULE__.FeatureState)
embeds_many(:identity_overrides, Flagsmith.Schemas.Identity)
embeds_one(:project, __MODULE__.Project)
embeds_one(:amplitude_config, __MODULE__.Integration)
embeds_one(:segment_config, __MODULE__.Integration)
embeds_one(:mixpanel_config, __MODULE__.Integration)
embeds_one(:heap_config, __MODULE__.Integration)

field(:__configuration__, :map)
end
Expand All @@ -26,11 +23,8 @@ defmodule Flagsmith.Schemas.Environment do
struct
|> cast(params, [:api_key, :id])
|> cast_embed(:feature_states)
|> cast_embed(:identity_overrides)
|> cast_embed(:project)
|> cast_embed(:amplitude_config)
|> cast_embed(:segment_config)
|> cast_embed(:mixpanel_config)
|> cast_embed(:heap_config)
end

@doc false
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
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule FlagsmithEngine.MixProject do
def project do
[
app: :flagsmith_engine,
version: "2.0.0",
version: "2.1.0",
elixir: "~> 1.12",
start_permanent: Mix.env() == :prod,
deps: deps(),
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"
}
]
}
]
}
}
6 changes: 1 addition & 5 deletions test/flagsmith_engine_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ defmodule Flagsmith.EngineTest do
assert {:ok,
%Environment{
__configuration__: nil,
amplitude_config: nil,
api_key: "cU3oztxgvRgZifpLepQJTX",
feature_states: [
%Environment.FeatureState{
Expand Down Expand Up @@ -82,9 +81,7 @@ defmodule Flagsmith.EngineTest do
multivariate_feature_state_values: []
}
],
heap_config: nil,
id: 11278,
mixpanel_config: nil,
project: %Environment.Project{
hide_disabled_flags: false,
id: 4732,
Expand Down Expand Up @@ -172,8 +169,7 @@ defmodule Flagsmith.EngineTest do
]
}
]
},
segment_config: nil
}
} = parsed} = Flagsmith.Engine.parse_environment(env_map)

assert env_map_2 = Test.Generators.json_env()
Expand Down
Loading

0 comments on commit d10e496

Please sign in to comment.