Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Identity overrides in local evaluation mode #34

Merged
merged 3 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
:configuration,
:environment,
:refresh,
:refresh_monitor
:refresh_monitor,
identities_with_overrides: %{}
]

#################################
Expand Down Expand Up @@ -114,9 +115,17 @@

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 @@
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 @@
# 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 @@
) 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 All @@ -237,7 +245,7 @@
# We should never receive this message but I like having a catch all for info msgs
# and just log.
def handle_event(:info, unknown, _state, _data) do
Logger.warn("#{inspect(__MODULE__)} received unexpected message: #{inspect(unknown)}")

Check warning on line 248 in lib/flagsmith_client_poller.ex

View workflow job for this annotation

GitHub Actions / Build and test

Logger.warn/1 is deprecated. Use Logger.warning/2 instead

Check warning on line 248 in lib/flagsmith_client_poller.ex

View workflow job for this annotation

GitHub Actions / Build and test

Logger.warn/1 is deprecated. Use Logger.warning/2 instead
{:keep_state_and_data, []}
end

Expand All @@ -252,6 +260,21 @@
%__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 @@
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 Expand Up @@ -596,7 +596,7 @@
end
rescue
Decimal.Error ->
Logger.warn(

Check warning on line 599 in lib/flagsmith_engine.ex

View workflow job for this annotation

GitHub Actions / Build and test

Logger.warn/1 is deprecated. Use Logger.warning/2 instead

Check warning on line 599 in lib/flagsmith_engine.ex

View workflow job for this annotation

GitHub Actions / Build and test

Logger.warn/1 is deprecated. Use Logger.warning/2 instead
"invalid MODULO segment rule or trait value :: rule: #{inspect(condition_value)} :: value: #{inspect(value)}"
)

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a public attribute (if such a thing exists in Elixir)? If so, should we increment its major version?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not private, but the model itself is not meant to be used directly.

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
Loading