Skip to content

Commit d10e496

Browse files
authored
feat: Identity overrides in local evaluation mode (#34)
* 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
1 parent 83abd38 commit d10e496

File tree

10 files changed

+248
-37
lines changed

10 files changed

+248
-37
lines changed

lib/flagsmith_client.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,9 @@ defmodule Flagsmith.Client do
177177

178178
case Tesla.post(http_client(config), @api_paths.identities, query) do
179179
{:ok, %{status: status, body: body}} when status >= 200 and status < 300 ->
180-
with %Schemas.Identity{flags: flags} <- Schemas.Identity.from_response(body),
181-
flags <- build_flags(flags, config) do
180+
with %Schemas.Identity{identity_features: identity_features} <-
181+
Schemas.Identity.from_response(body),
182+
flags <- build_flags(identity_features, config) do
182183
{:ok, flags}
183184
else
184185
error ->

lib/flagsmith_client_poller.ex

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ defmodule Flagsmith.Client.Poller do
1818
:configuration,
1919
:environment,
2020
:refresh,
21-
:refresh_monitor
21+
:refresh_monitor,
22+
identities_with_overrides: %{}
2223
]
2324

2425
#################################
@@ -114,9 +115,17 @@ defmodule Flagsmith.Client.Poller do
114115

115116
def handle_event({:call, from}, {:get_identity_flags, identifier, traits}, _, %__MODULE__{
116117
environment: env,
117-
configuration: config
118+
configuration: config,
119+
identities_with_overrides: overrides
118120
}) do
119-
identity = Schemas.Identity.from_id_traits(identifier, traits, env.api_key)
121+
identity =
122+
case Map.get(overrides, identifier) do
123+
nil ->
124+
Schemas.Identity.from_id_traits(identifier, traits, env.api_key)
125+
126+
existing ->
127+
%Schemas.Identity{existing | traits: Flagsmith.Schemas.Traits.Trait.from(traits)}
128+
end
120129

121130
flags =
122131
env
@@ -148,7 +157,7 @@ defmodule Flagsmith.Client.Poller do
148157
def handle_event(:internal, :initial_load, :loading, %__MODULE__{configuration: config} = data) do
149158
case Flagsmith.Client.get_environment_request(config) do
150159
{:ok, environment} ->
151-
{:next_state, :on, %__MODULE__{data | environment: environment},
160+
{:next_state, :on, update_data(data, environment),
152161
[{:next_event, :internal, :set_refresh}]}
153162

154163
error ->
@@ -209,7 +218,7 @@ defmodule Flagsmith.Client.Poller do
209218
# a process other than the one we have stored under the `:refresh_monitor` key
210219
# we still make sure it's matching.
211220
#
212-
# Then we just check if the response is an `:ok` tuple with an `Environment.t`
221+
# Then we just check if the response is an `:ok` tuple with an `Environment.t`
213222
# we replace the `:environment` key on our statem data and following user queries
214223
# will receive the new env or flags. If not we let it stay as is.
215224
#
@@ -222,8 +231,7 @@ defmodule Flagsmith.Client.Poller do
222231
) do
223232
case result do
224233
{:ok, %Schemas.Environment{} = env} ->
225-
{:keep_state, %{data | refresh_monitor: nil, environment: env},
226-
[{:next_event, :internal, :set_refresh}]}
234+
{:keep_state, update_data(data, env), [{:next_event, :internal, :set_refresh}]}
227235

228236
error ->
229237
Logger.error(
@@ -252,6 +260,21 @@ defmodule Flagsmith.Client.Poller do
252260
%__MODULE__{configuration: config, refresh: refresh_milliseconds}
253261
end
254262

263+
# Update identities with overrides along with the environment.
264+
defp update_data(data, environment) do
265+
%__MODULE__{
266+
data
267+
| refresh_monitor: nil,
268+
environment: environment,
269+
identities_with_overrides:
270+
Enum.reduce(
271+
environment.identity_overrides,
272+
%{},
273+
fn identity, acc -> Map.put(acc, identity.identifier, identity) end
274+
)
275+
}
276+
end
277+
255278
@doc false
256279
# this function is just so we can spawn a proper function with an MFA tuple
257280
def get_environment(pid, config) do

lib/flagsmith_engine.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ defmodule Flagsmith.Engine do
6767
feature_states: fs,
6868
project: %Environment.Project{segments: segments}
6969
} = env,
70-
%Identity{flags: identity_features} = identity,
70+
%Identity{identity_features: identity_features} = identity,
7171
override_traits \\ []
7272
) do
7373
with identity <- Identity.set_env_key(identity, env),

lib/schemas/environment.ex

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,8 @@ defmodule Flagsmith.Schemas.Environment do
1010
typed_embedded_schema do
1111
field(:api_key, :string)
1212
embeds_many(:feature_states, __MODULE__.FeatureState)
13+
embeds_many(:identity_overrides, Flagsmith.Schemas.Identity)
1314
embeds_one(:project, __MODULE__.Project)
14-
embeds_one(:amplitude_config, __MODULE__.Integration)
15-
embeds_one(:segment_config, __MODULE__.Integration)
16-
embeds_one(:mixpanel_config, __MODULE__.Integration)
17-
embeds_one(:heap_config, __MODULE__.Integration)
1815

1916
field(:__configuration__, :map)
2017
end
@@ -26,11 +23,8 @@ defmodule Flagsmith.Schemas.Environment do
2623
struct
2724
|> cast(params, [:api_key, :id])
2825
|> cast_embed(:feature_states)
26+
|> cast_embed(:identity_overrides)
2927
|> cast_embed(:project)
30-
|> cast_embed(:amplitude_config)
31-
|> cast_embed(:segment_config)
32-
|> cast_embed(:mixpanel_config)
33-
|> cast_embed(:heap_config)
3428
end
3529

3630
@doc false

lib/schemas/identity.ex

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ defmodule Flagsmith.Schemas.Identity do
1212
field(:django_id, :integer)
1313
field(:identifier, :string)
1414
field(:environment_key, :string)
15-
embeds_many(:flags, Flagsmith.Schemas.Features.FeatureState)
15+
embeds_many(:identity_features, Flagsmith.Schemas.Features.FeatureState)
1616
embeds_many(:traits, Flagsmith.Schemas.Traits.Trait)
1717
end
1818

@@ -24,7 +24,7 @@ defmodule Flagsmith.Schemas.Identity do
2424
|> cast(params, [:identifier, :environment_key, :django_id])
2525
|> validate_required([:identifier])
2626
|> cast_embed(:traits)
27-
|> cast_embed(:flags)
27+
|> cast_embed(:identity_features)
2828
end
2929

3030
@doc false
@@ -43,16 +43,14 @@ defmodule Flagsmith.Schemas.Identity do
4343
@doc false
4444
@spec from_response(element :: map() | list(map())) :: __MODULE__.t() | any()
4545
def from_response(element) when is_map(element) do
46-
element
46+
Map.put(element, "identity_features", Map.get(element, "flags"))
4747
|> changeset()
4848
|> apply_changes()
4949
end
5050

5151
def from_response(elements) when is_list(elements) do
5252
Enum.map(elements, fn element ->
53-
element
54-
|> changeset()
55-
|> apply_changes()
53+
from_response(element)
5654
end)
5755
end
5856

mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ defmodule FlagsmithEngine.MixProject do
44
def project do
55
[
66
app: :flagsmith_engine,
7-
version: "2.0.0",
7+
version: "2.1.0",
88
elixir: "~> 1.12",
99
start_permanent: Mix.env() == :prod,
1010
deps: deps(),

test/data/environment.json

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
{
2+
"api_key": "cU3oztxgvRgZifpLepQJTX",
3+
"feature_states": [
4+
{
5+
"django_id": 72267,
6+
"enabled": false,
7+
"feature": {
8+
"id": 13534,
9+
"name": "header_size",
10+
"type": "MULTIVARIATE"
11+
},
12+
"feature_state_value": "24px",
13+
"featurestate_uuid": "16c5a45c-1d9c-4f44-bebe-5b73d60f897d",
14+
"multivariate_feature_state_values": [
15+
{
16+
"id": 2915,
17+
"multivariate_feature_option": {
18+
"id": 849,
19+
"value": "34px"
20+
},
21+
"mv_fs_value_uuid": "448a7777-91cf-47b0-bf16-a4d566ef7745",
22+
"percentage_allocation": 60.0
23+
}
24+
]
25+
},
26+
{
27+
"django_id": 72269,
28+
"enabled": false,
29+
"feature": {
30+
"id": 13535,
31+
"name": "body_size",
32+
"type": "STANDARD"
33+
},
34+
"feature_state_value": "18px",
35+
"featurestate_uuid": "c3c61a9a-f153-46b2-8e9e-dd80d6529201",
36+
"multivariate_feature_state_values": []
37+
},
38+
{
39+
"django_id": 92461,
40+
"enabled": true,
41+
"feature": {
42+
"id": 17985,
43+
"name": "secret_button",
44+
"type": "STANDARD"
45+
},
46+
"feature_state_value": "{\"colour\": \"#ababab\"}",
47+
"featurestate_uuid": "d6bbf961-1752-4548-97d1-02d60cc1ab44",
48+
"multivariate_feature_state_values": []
49+
},
50+
{
51+
"django_id": 94235,
52+
"enabled": true,
53+
"feature": {
54+
"id": 18382,
55+
"name": "test_identity",
56+
"type": "STANDARD"
57+
},
58+
"feature_state_value": "very_yes",
59+
"featurestate_uuid": "aa1a4512-b1c7-44d3-a263-c21676852a52",
60+
"multivariate_feature_state_values": []
61+
}
62+
],
63+
"id": 11278,
64+
"identity_overrides": [
65+
{
66+
"identifier": "overridden-id",
67+
"identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01",
68+
"created_date": "2019-08-27T14:53:45.698555Z",
69+
"updated_at": "2023-07-14 16:12:00.000000",
70+
"environment_api_key": "cU3oztxgvRgZifpLepQJTX",
71+
"identity_features": [
72+
{
73+
"feature": {
74+
"id": 18382,
75+
"name": "test_identity",
76+
"type": "STANDARD"
77+
},
78+
"feature_state_value": "some-overridden-value",
79+
"enabled": false
80+
}
81+
]
82+
}
83+
],
84+
"project": {
85+
"hide_disabled_flags": false,
86+
"id": 4732,
87+
"name": "testing-api",
88+
"organisation": {
89+
"feature_analytics": false,
90+
"id": 4131,
91+
"name": "Mr. Bojangles Inc",
92+
"persist_trait_data": true,
93+
"stop_serving_flags": false
94+
},
95+
"segments": [
96+
{
97+
"feature_states": [
98+
{
99+
"django_id": 95632,
100+
"enabled": true,
101+
"feature": {
102+
"id": 17985,
103+
"name": "secret_button",
104+
"type": "STANDARD"
105+
},
106+
"feature_state_value": "",
107+
"featurestate_uuid": "3b58d149-fdb3-4815-b537-6583291523dd",
108+
"multivariate_feature_state_values": []
109+
}
110+
],
111+
"id": 5241,
112+
"name": "test_segment",
113+
"rules": [
114+
{
115+
"conditions": [],
116+
"rules": [
117+
{
118+
"conditions": [
119+
{
120+
"operator": "EQUAL",
121+
"property_": "show_popup",
122+
"value": "false"
123+
}
124+
],
125+
"rules": [],
126+
"type": "ANY"
127+
}
128+
],
129+
"type": "ALL"
130+
}
131+
]
132+
},
133+
{
134+
"feature_states": [
135+
{
136+
"django_id": 95631,
137+
"enabled": true,
138+
"feature": {
139+
"id": 17985,
140+
"name": "secret_button",
141+
"type": "STANDARD"
142+
},
143+
"feature_state_value": "",
144+
"featurestate_uuid": "adb486aa-563d-4b1d-9f72-bf5b210bf94f",
145+
"multivariate_feature_state_values": []
146+
}
147+
],
148+
"id": 5243,
149+
"name": "test_perc",
150+
"rules": [
151+
{
152+
"conditions": [],
153+
"rules": [
154+
{
155+
"conditions": [
156+
{
157+
"operator": "PERCENTAGE_SPLIT",
158+
"property_": "",
159+
"value": "20"
160+
}
161+
],
162+
"rules": [],
163+
"type": "ANY"
164+
}
165+
],
166+
"type": "ALL"
167+
}
168+
]
169+
}
170+
]
171+
}
172+
}

test/flagsmith_engine_test.exs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ defmodule Flagsmith.EngineTest do
2020
assert {:ok,
2121
%Environment{
2222
__configuration__: nil,
23-
amplitude_config: nil,
2423
api_key: "cU3oztxgvRgZifpLepQJTX",
2524
feature_states: [
2625
%Environment.FeatureState{
@@ -82,9 +81,7 @@ defmodule Flagsmith.EngineTest do
8281
multivariate_feature_state_values: []
8382
}
8483
],
85-
heap_config: nil,
8684
id: 11278,
87-
mixpanel_config: nil,
8885
project: %Environment.Project{
8986
hide_disabled_flags: false,
9087
id: 4732,
@@ -172,8 +169,7 @@ defmodule Flagsmith.EngineTest do
172169
]
173170
}
174171
]
175-
},
176-
segment_config: nil
172+
}
177173
} = parsed} = Flagsmith.Engine.parse_environment(env_map)
178174

179175
assert env_map_2 = Test.Generators.json_env()

0 commit comments

Comments
 (0)