From 744c28972bf073903f445710e116ce023403029a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 7 Feb 2025 12:13:33 -0300 Subject: [PATCH 01/12] mix format --- CONTRIBUTORS-GUIDE.md | 15 ++++--- mix.exs | 5 +-- test/rpc/response_parser_test.exs | 72 +++++++++++++++---------------- test/split/impression_test.exs | 26 ++++++++--- 4 files changed, 65 insertions(+), 53 deletions(-) diff --git a/CONTRIBUTORS-GUIDE.md b/CONTRIBUTORS-GUIDE.md index 0f014b8..df52810 100644 --- a/CONTRIBUTORS-GUIDE.md +++ b/CONTRIBUTORS-GUIDE.md @@ -9,13 +9,14 @@ Split SDK is an open source project and we welcome feedback and contribution. Th 3. While developing, use descriptive messages in your commits. Avoid short or meaningless sentences like: "fix bug". 4. Make sure to add tests for both positive and negative cases. 5. If your changes have any impact on the public API, make sure you update the type specification and documentation attributes (`@spec`, `@doc`, `@moduledoc`), as well as it's related test file. -6. Run the build script (`mix compile`) and the static type analysis (`mix dialyzer`) and make sure it runs with no errors. -7. Run all tests (`mix test`) and make sure there are no failures. -8. `git push` your changes to GitHub within your topic branch. -9. Open a Pull Request(PR) from your forked repo and into the `development` branch of the original repository. -10. When creating your PR, please fill out all the fields of the PR template, as applicable, for the project. -11. Check for conflicts once the pull request is created to make sure your PR can be merged cleanly into `development`. -12. Keep an eye out for any feedback or comments from Split's SDK team. +6. Run the code formatter (`mix format`) and verify that all files are properly formatted. +7. Run the build script (`mix compile`) and the static type analysis (`mix dialyzer`) and make sure it runs with no errors. +8. Run tests (`mix test`) and make sure there are no failures. +9. `git push` your changes to GitHub within your topic branch. +10. Open a Pull Request(PR) from your forked repo and into the `development` branch of the original repository. +11. When creating your PR, please fill out all the fields of the PR template, as applicable, for the project. +12. Check for conflicts once the pull request is created to make sure your PR can be merged cleanly into `development`. +13. Keep an eye out for any feedback or comments from Split's SDK team. # Contact diff --git a/mix.exs b/mix.exs index 1184023..83dc6ec 100644 --- a/mix.exs +++ b/mix.exs @@ -10,11 +10,10 @@ defmodule SplitThinElixir.MixProject do start_permanent: Mix.env() == :prod, deps: deps(), runtime_tools: [:observer], - package: package(), + package: package() ] end - # Package-specific metadata for Hex.pm defp package do [ @@ -25,7 +24,7 @@ defmodule SplitThinElixir.MixProject do "GitHub" => "https://github.com/splitio/elixir-thin-client", "Docs" => "https://hexdocs.pm/split_thin_sdk" }, - maintainers: ["Emiliano Sanchez", "Nicolas Zelaya", "split-fme-libraries@harness.io"], + maintainers: ["Emiliano Sanchez", "Nicolas Zelaya", "split-fme-libraries@harness.io"] ] end diff --git a/test/rpc/response_parser_test.exs b/test/rpc/response_parser_test.exs index ea1e5bb..6f3c31d 100644 --- a/test/rpc/response_parser_test.exs +++ b/test/rpc/response_parser_test.exs @@ -88,9 +88,9 @@ defmodule Split.RPC.ResponseParserTest do assert ResponseParser.parse_response(response, message) == %{ "feature_name1" => %Impression{ - key: "user_key", - bucketing_key: "bucketing_key", - feature: "feature_name1", + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name1", treatment: "on", label: "test label 1", config: nil, @@ -98,9 +98,9 @@ defmodule Split.RPC.ResponseParserTest do timestamp: 1_723_742_604 }, "feature_name2" => %Impression{ - key: "user_key", - bucketing_key: "bucketing_key", - feature: "feature_name2", + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name2", treatment: "off", label: "test label 2", config: nil, @@ -139,9 +139,9 @@ defmodule Split.RPC.ResponseParserTest do assert ResponseParser.parse_response(response, message) == %{ "feature_name1" => %Impression{ - key: "user_key", - bucketing_key: "bucketing_key", - feature: "feature_name1", + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name1", treatment: "on", label: "test label 1", config: "{\"foo\": \"bar\"}", @@ -149,9 +149,9 @@ defmodule Split.RPC.ResponseParserTest do timestamp: 1_723_742_604 }, "feature_name2" => %Impression{ - key: "user_key", - bucketing_key: "bucketing_key", - feature: "feature_name2", + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name2", treatment: "off", label: "test label 2", config: "{\"baz\": \"qux\"}", @@ -188,9 +188,9 @@ defmodule Split.RPC.ResponseParserTest do assert ResponseParser.parse_response(response, message) == %{ "feature_name1" => %Impression{ - key: "user_key", - bucketing_key: "bucketing_key", - feature: "feature_name1", + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name1", treatment: "on", label: "test label 1", config: nil, @@ -198,9 +198,9 @@ defmodule Split.RPC.ResponseParserTest do timestamp: 1_723_742_604 }, "feature_name2" => %Impression{ - key: "user_key", - bucketing_key: "bucketing_key", - feature: "feature_name2", + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name2", treatment: "off", label: "test label 2", config: nil, @@ -239,9 +239,9 @@ defmodule Split.RPC.ResponseParserTest do assert ResponseParser.parse_response(response, message) == %{ "feature_name1" => %Impression{ - key: "user_key", - bucketing_key: "bucketing_key", - feature: "feature_name1", + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name1", treatment: "on", label: "test label 1", config: "{\"foo\": \"bar\"}", @@ -249,9 +249,9 @@ defmodule Split.RPC.ResponseParserTest do timestamp: 1_723_742_604 }, "feature_name2" => %Impression{ - key: "user_key", - bucketing_key: "bucketing_key", - feature: "feature_name2", + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name2", treatment: "off", label: "test label 2", config: "{\"baz\": \"qux\"}", @@ -288,9 +288,9 @@ defmodule Split.RPC.ResponseParserTest do assert ResponseParser.parse_response(response, message) == %{ "feature_name1" => %Impression{ - key: "user_key", - bucketing_key: "bucketing_key", - feature: "feature_name1", + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name1", treatment: "on", label: "test label 1", config: nil, @@ -298,9 +298,9 @@ defmodule Split.RPC.ResponseParserTest do timestamp: 1_723_742_604 }, "feature_name2" => %Impression{ - key: "user_key", - bucketing_key: "bucketing_key", - feature: "feature_name2", + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name2", treatment: "off", label: "test label 2", config: nil, @@ -339,9 +339,9 @@ defmodule Split.RPC.ResponseParserTest do assert ResponseParser.parse_response(response, message) == %{ "feature_name1" => %Impression{ - key: "user_key", - bucketing_key: "bucketing_key", - feature: "feature_name1", + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name1", treatment: "on", label: "test label 1", config: "{\"foo\": \"bar\"}", @@ -349,9 +349,9 @@ defmodule Split.RPC.ResponseParserTest do timestamp: 1_723_742_604 }, "feature_name2" => %Impression{ - key: "user_key", - bucketing_key: "bucketing_key", - feature: "feature_name2", + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name2", treatment: "off", label: "test label 2", config: "{\"baz\": \"qux\"}", diff --git a/test/split/impression_test.exs b/test/split/impression_test.exs index 06ce010..6ddb3bd 100644 --- a/test/split/impression_test.exs +++ b/test/split/impression_test.exs @@ -16,9 +16,9 @@ defmodule Split.ImpressionTest do } expected = %Impression{ - key: 'user_key', - bucketing_key: 'bucketing_key', - feature: 'feature_name', + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name", treatment: "treatment", label: "label", config: "{\"field\": \"value\"}", @@ -26,7 +26,13 @@ defmodule Split.ImpressionTest do timestamp: 2 } - assert expected == Impression.build_from_daemon_response(treatment_payload, 'user_key', 'bucketing_key', 'feature_name') + assert expected == + Impression.build_from_daemon_response( + treatment_payload, + "user_key", + "bucketing_key", + "feature_name" + ) end test "builds an impression struct with nil values" do @@ -35,9 +41,9 @@ defmodule Split.ImpressionTest do } expected = %Impression{ - key: 'user_key', + key: "user_key", bucketing_key: nil, - feature: 'feature_name', + feature: "feature_name", treatment: "treatment", label: nil, config: nil, @@ -45,7 +51,13 @@ defmodule Split.ImpressionTest do timestamp: nil } - assert expected == Impression.build_from_daemon_response(treatment_payload, 'user_key', nil, 'feature_name') + assert expected == + Impression.build_from_daemon_response( + treatment_payload, + "user_key", + nil, + "feature_name" + ) end end end From a340bfa20396eea4fabc1bebacf0a45713a4e9a7 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 7 Feb 2025 13:03:46 -0300 Subject: [PATCH 02/12] Update CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6dbc949..4f5034d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,6 @@ jobs: with: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} - - run: mix deps.get + - run: mix deps.unlock --all # compiles and runs tests against latest versions of dependencies - run: mix test - run: mix dialyzer --format github From 44738a15954db6aed78415172b65cd3b9c6bc8ec Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 7 Feb 2025 18:25:43 -0300 Subject: [PATCH 03/12] Fix split_key type --- lib/split.ex | 8 +++++++- lib/split/rpc/message.ex | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/split.ex b/lib/split.ex index ad29534..f8f83d6 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -64,7 +64,13 @@ defmodule Split do @type options :: [option()] - @type split_key :: String.t() | {:matching_key, String.t(), :bucketing_key, String.t() | nil} + @typedoc """ + The [traffic type identifier](https://help.split.io/hc/en-us/articles/360019916311-Traffic-types). + It can be either a string or a map with a matching key and an optional bucketing key. + """ + @type split_key :: + String.t() + | %{required(:matchingKey) => String.t(), optional(:bucketingKey) => String.t() | nil} @doc """ Builds a child specification to use in a Supervisor. diff --git a/lib/split/rpc/message.ex b/lib/split/rpc/message.ex index 62646a5..8c5cb47 100644 --- a/lib/split/rpc/message.ex +++ b/lib/split/rpc/message.ex @@ -237,7 +237,7 @@ defmodule Split.RPC.Message do } iex> Message.get_treatments_with_config_by_flag_sets( - ...> key: "user_key", + ...> key: %{:matching_key => "user_key"}, ...> feature_names: ["flag_set_name1", "flag_set_name2"] ...> ) %Message{ @@ -364,7 +364,7 @@ defmodule Split.RPC.Message do {matching_key, bucketing_key} = if is_map(key) do - {key.matching_key, key.bucketing_key} + {key.matching_key, Map.get(key, :bucketing_key, nil)} else {key, nil} end From a24de6aa6ca77c2061cb78aaf086dc2afc8fe2de Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 7 Feb 2025 18:37:47 -0300 Subject: [PATCH 04/12] Fix CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f5034d..abd14be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,5 +15,6 @@ jobs: otp-version: ${{matrix.otp}} elixir-version: ${{matrix.elixir}} - run: mix deps.unlock --all # compiles and runs tests against latest versions of dependencies + - run: mix deps.get - run: mix test - run: mix dialyzer --format github From a5b73f055c362f4951196723591d2e4cc9056e6b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 10 Feb 2025 17:07:28 -0300 Subject: [PATCH 05/12] Refactor code snippet formatting (Example https://github.com/elixir-lang/elixir/blob/main/lib/elixir/lib/supervisor.ex) --- lib/split.ex | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/lib/split.ex b/lib/split.ex index f8f83d6..6694528 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -10,27 +10,23 @@ defmodule Split do The most basic approach is to add `Split` as a child of your application's top-most supervisor, i.e. `lib/my_app/application.ex`. - ```elixir - defmodule MyApp.Application do - use Application - - def start(_type, _args) do - children = [ - # ... other children ... - {Split, [socket_path: "/var/run/split.sock"]} - ] - - opts = [strategy: :one_for_one, name: MyApp.Supervisor] - Supervisor.start_link(children, opts) - end - end - ``` + defmodule MyApp.Application do + use Application + + def start(_type, _args) do + children = [ + # ... other children ... + {Split, [socket_path: "/var/run/split.sock"]} + ] + + opts = [strategy: :one_for_one, name: MyApp.Supervisor] + Supervisor.start_link(children, opts) + end + end You can also start `Split` dynamically by calling `Split.Supervisor.start_link/1`: - ```elixir - Split.Supervisor.start_link(opts) - ``` + Split.Supervisor.start_link(opts) ### Options @@ -45,9 +41,7 @@ defmodule Split do Once you have started Split, you are ready to start interacting with the Split.io splitd's daemon to access feature flags and configurations. - ```elixir - Split.get_treatment("user_key", "feature_name") - ``` + Split.get_treatment("user_key", "feature_name") """ alias Split.Telemetry alias Split.Sockets.Pool From c172ca05a6a43d614bb6cc2a6465a4d5a8b6b59f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 11 Feb 2025 10:02:54 -0300 Subject: [PATCH 06/12] Enhance documentation for Split SDK modules and functions --- lib/split.ex | 140 +++++++++++++++++++++++++++++ lib/split/split_view.ex | 15 ++++ lib/split/supervisor.ex | 6 +- lib/split/treatment_with_config.ex | 8 ++ mix.exs | 13 ++- 5 files changed, 180 insertions(+), 2 deletions(-) diff --git a/lib/split.ex b/lib/split.ex index 6694528..e7cc760 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -56,6 +56,7 @@ defmodule Split do | {:pool_size, non_neg_integer()} | {:connect_timeout, non_neg_integer()} + @typedoc "Options to start the `Split` application." @type options :: [option()] @typedoc """ @@ -76,6 +77,16 @@ defmodule Split do @spec child_spec(options()) :: Supervisor.child_spec() defdelegate child_spec(options), to: Split.Supervisor + @doc """ + Gets the treatment string for a given key, feature flag name and optional attributes. + + ## Examples + + iex> Split.get_treatment("user_id", "located_in_usa") + "off" + iex> Split.get_treatment("user_id", "located_in_usa", %{country: "USA"}) + "on" + """ @spec get_treatment(split_key(), String.t(), map() | nil) :: String.t() def get_treatment(key, feature_name, attributes \\ %{}) do request = @@ -88,6 +99,16 @@ defmodule Split do execute_rpc(request) |> impression_to_treatment() end + @doc """ + Gets the treatment with config for a given key, feature flag name and optional attributes. + + ## Examples + + iex> Split.get_treatment_with_config("user_id", "located_in_usa") + %Split.TreatmentWithConfig{treatment: "off", config: nil} + iex> Split.get_treatment("user_id", "located_in_usa", %{country: "USA"}) + %Split.TreatmentWithConfig{treatment: "on", config: nil} + """ @spec get_treatment_with_config(split_key(), String.t(), map() | nil) :: TreatmentWithConfig.t() def get_treatment_with_config(key, feature_name, attributes \\ %{}) do request = @@ -100,6 +121,16 @@ defmodule Split do execute_rpc(request) |> impression_to_treatment_with_config() end + @doc """ + Gets a map of feature flag names to treatments for a given key, list of feature flag names and optional attributes. + + ## Examples + + iex> Split.get_treatments("user_id", ["located_in_usa"]) + %{"located_in_usa" => "off"} + iex> Split.get_treatments("user_id", ["located_in_usa"], %{country: "USA"}) + %{"located_in_usa" => "on"} + """ @spec get_treatments(split_key(), [String.t()], map() | nil) :: %{ String.t() => String.t() } @@ -114,6 +145,16 @@ defmodule Split do execute_rpc(request) |> impressions_to_treatments() end + @doc """ + Gets a map of feature flag names to treatments with config for a given key, list of feature flag names and optional attributes. + + ## Examples + + iex> Split.get_treatments_with_config("user_id", ["located_in_usa"]) + %{"located_in_usa" => %Split.TreatmentWithConfig{treatment: "off", config: nil}} + iex> Split.get_treatments_with_config("user_id", ["located_in_usa"], %{country: "USA"}) + %{"located_in_usa" => %Split.TreatmentWithConfig{treatment: "on", config: nil}} + """ @spec get_treatments_with_config(split_key(), [String.t()], map() | nil) :: %{ String.t() => TreatmentWithConfig.t() } @@ -128,6 +169,16 @@ defmodule Split do execute_rpc(request) |> impressions_to_treatments_with_config() end + @doc """ + Gets a map of feature flag names to treatment strings for a given key, flag set name and optional attributes. + + ## Examples + + iex> Split.get_treatments_by_flag_set("user_id", "frontend_flags") + %{"located_in_usa" => "off"} + iex> Split.get_treatments_by_flag_set("user_id", "frontend_flags", %{country: "USA"}) + %{"located_in_usa" => "on"} + """ @spec get_treatments_by_flag_set(split_key(), String.t(), map() | nil) :: %{ String.t() => String.t() } @@ -142,6 +193,16 @@ defmodule Split do execute_rpc(request) |> impressions_to_treatments() end + @doc """ + Gets a map of feature flag names to treatments with config for a given key, flag set name and optional attributes. + + ## Examples + + iex> Split.get_treatments_with_config_by_flag_set("user_id", "frontend_flags") + %{"located_in_usa" => %Split.TreatmentWithConfig{treatment: "off", config: nil}} + iex> Split.get_treatments_with_config_by_flag_set("user_id", "frontend_flags", %{country: "USA"}) + %{"located_in_usa" => %Split.TreatmentWithConfig{treatment: "on", config: nil}} + """ @spec get_treatments_with_config_by_flag_set( split_key(), String.t(), @@ -163,6 +224,16 @@ defmodule Split do execute_rpc(request) |> impressions_to_treatments_with_config() end + @doc """ + Gets a map of feature flag names to treatment strings for a given key, flag set name and optional attributes. + + ## Examples + + iex> Split.get_treatments_by_flag_sets("user_id", ["frontend_flags", "backend_flags"]) + %{"located_in_usa" => "off"} + iex> Split.get_treatments_by_flag_sets("user_id", ["frontend_flags", "backend_flags"], %{country: "USA"}) + %{"located_in_usa" => "on"} + """ @spec get_treatments_by_flag_sets(split_key(), [String.t()], map() | nil) :: %{String.t() => String.t()} def get_treatments_by_flag_sets( @@ -180,6 +251,16 @@ defmodule Split do execute_rpc(request) |> impressions_to_treatments() end + @doc """ + Gets a map of feature flag names to treatments with config for a given key, flag set name and optional attributes. + + ## Examples + + iex> Split.get_treatments_with_config_by_flag_sets("user_id", ["frontend_flags", "backend_flags"]) + %{"located_in_usa" => %Split.TreatmentWithConfig{treatment: "off", config: nil}} + iex> Split.get_treatments_with_config_by_flag_sets("user_id", ["frontend_flags", "backend_flags"], %{country: "USA"}) + %{"located_in_usa" => %Split.TreatmentWithConfig{treatment: "on", config: nil}} + """ @spec get_treatments_with_config_by_flag_sets( split_key(), [String.t()], @@ -201,18 +282,59 @@ defmodule Split do execute_rpc(request) |> impressions_to_treatments_with_config() end + @doc """ + Tracks an event for a given key, traffic type, event type, and optional numeric value and map of properties. + Returns `true` if the event was successfully tracked, or `false` otherwise, e.g. if the Split daemon is not running or cannot be reached. + + See: https://help.split.io/hc/en-us/articles/26988707417869-Elixir-Thin-Client-SDK#track + + ## Examples + + iex> Split.track("user_id", "user", "my-event") + true + iex> Split.track("user_id", "user", "my-event", 42) + true + iex> Split.track("user_id", "user", "my-event", 42, %{property1: "value1"}) + true + """ @spec track(split_key(), String.t(), String.t(), number() | nil, map() | nil) :: boolean() def track(key, traffic_type, event_type, value \\ nil, properties \\ %{}) do request = Message.track(key, traffic_type, event_type, value, properties) execute_rpc(request) end + @doc """ + Gets the list of all feature flag names. + + ## Examples + + iex> Split.split_names() + ["located_in_usa"] + """ @spec split_names() :: [String.t()] def split_names do request = Message.split_names() execute_rpc(request) end + @doc """ + Gets the data of a given feature flag name in `SplitView` format. + + ## Examples + + iex> Split.split("located_in_usa") + %Split.SplitView{ + name: "located_in_usa", + traffic_type: "user", + killed: false, + treatments: ["on", "off"], + change_number: 123456, + configs: %{ "on" => nil, "off" => nil }, + default_treatment: "off", + sets: ["frontend_flags"], + impressions_disabled: false + } + """ @spec split(String.t()) :: SplitView.t() | nil def split(name) do request = Message.split(name) @@ -220,6 +342,24 @@ defmodule Split do execute_rpc(request) end + @doc """ + Gets the data of all feature flags in `SplitView` format. + + ## Examples + + iex> Split.splits() + [%Split.SplitView{ + name: "located_in_usa", + traffic_type: "user", + killed: false, + treatments: ["on", "off"], + change_number: 123456, + configs: %{ "on" => nil, "off" => nil }, + default_treatment: "off", + sets: ["frontend_flags"], + impressions_disabled: false + }] + """ @spec splits() :: [SplitView.t()] def splits do request = Message.splits() diff --git a/lib/split/split_view.ex b/lib/split/split_view.ex index 312e7e7..c4d7352 100644 --- a/lib/split/split_view.ex +++ b/lib/split/split_view.ex @@ -1,4 +1,19 @@ defmodule Split.SplitView do + @moduledoc """ + This module is a struct that contains information of a feature flag. + + ## Fields + * `:name` - The name of the feature flag + * `:traffic_type` - The traffic type of the feature flag + * `:killed` - A boolean that indicates if the feature flag is killed + * `:treatments` - The list of treatments of the feature flag + * `:change_number` - The change number of the feature flag + * `:configs` - The map of treatments and their configurations + * `:default_treatment` - The default treatment of the feature flag + * `:sets` - The list of flag sets that the feature flag belongs to + * `:impressions_disabled` - A boolean that indicates if the tracking of impressions is disabled + """ + defstruct [ :name, :traffic_type, diff --git a/lib/split/supervisor.ex b/lib/split/supervisor.ex index 9dc20b4..999e182 100644 --- a/lib/split/supervisor.ex +++ b/lib/split/supervisor.ex @@ -1,4 +1,8 @@ defmodule Split.Supervisor do + @moduledoc """ + The supervisor for the Split SDK. + """ + use GenServer alias Split.Sockets.Pool @@ -7,7 +11,7 @@ defmodule Split.Supervisor do {:ok, init_arg} end - @spec start_link(keyword()) :: Supervisor.on_start() + @spec start_link(Split.options()) :: Supervisor.on_start() def start_link(opts) do child = {Pool, opts} Supervisor.start_link([child], strategy: :one_for_one) diff --git a/lib/split/treatment_with_config.ex b/lib/split/treatment_with_config.ex index cac3bdd..fe4ee14 100644 --- a/lib/split/treatment_with_config.ex +++ b/lib/split/treatment_with_config.ex @@ -1,4 +1,12 @@ defmodule Split.TreatmentWithConfig do + @moduledoc """ + This module is a struct that represents a treatment with a configuration. + + ## Fields + * `:treatment` - The treatment string value + * `:config` - The treatment configuration string or nil if the treatment has no configuration + """ + defstruct treatment: "control", config: nil diff --git a/mix.exs b/mix.exs index f394212..ed9a7ab 100644 --- a/mix.exs +++ b/mix.exs @@ -10,7 +10,18 @@ defmodule SplitThinElixir.MixProject do start_permanent: Mix.env() == :prod, deps: deps(), runtime_tools: [:observer], - package: package() + package: package(), + docs: [ + filter_modules: fn mod, _meta -> + # Skip modules that are not part of the public API + mod in [ + Split, + Split.Supervisor, + Split.SplitView, + Split.TreatmentWithConfig + ] + end + ] ] end From 2287c1a7ca2b294b02755c4d8d762c6584f53e79 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 11 Feb 2025 10:50:01 -0300 Subject: [PATCH 07/12] Update and test Elixir version compatibility --- .github/workflows/ci.yml | 10 +++++----- README.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abd14be..dee09d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,17 +3,17 @@ on: push jobs: test: runs-on: ubuntu-20.04 - name: OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} + name: OTP ${{matrix.versions.otp}} / Elixir ${{matrix.versions.elixir}} strategy: matrix: - otp: ['26.2.5'] - elixir: ['1.17.0'] + # Minimum and maximum supported versions + versions: [{ elixir: '1.14.0', otp: '25' }, { elixir: '1.18.0', otp: '26.2.5' }] steps: - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 with: - otp-version: ${{matrix.otp}} - elixir-version: ${{matrix.elixir}} + otp-version: ${{matrix.versions.otp}} + elixir-version: ${{matrix.versions.elixir}} - run: mix deps.unlock --all # compiles and runs tests against latest versions of dependencies - run: mix deps.get - run: mix test diff --git a/README.md b/README.md index 1009be6..d548523 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This SDK is designed to work with Split, the platform for controlled rollouts, w ## Compatibility -The Elixir Thin Client SDK is compatible with Elixir @TODO and later. +The Elixir Thin Client SDK is compatible with Elixir v1.14.0 and later. ## Getting started From 2943111034a793b088c58e13f3821c195574985b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 11 Feb 2025 11:32:00 -0300 Subject: [PATCH 08/12] Fix tests for Elixit v1.18 --- test/sockets/pool_test.exs | 2 +- test/split_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/sockets/pool_test.exs b/test/sockets/pool_test.exs index f5182b3..66821bd 100644 --- a/test/sockets/pool_test.exs +++ b/test/sockets/pool_test.exs @@ -8,7 +8,7 @@ defmodule Split.Sockets.PoolTest do import ExUnit.CaptureLog setup_all context do - test_id = :erlang.phash2(context.case) + test_id = :erlang.phash2(context.module) socket_path = "/tmp/test-splitd-#{test_id}.sock" start_supervised!( diff --git a/test/split_test.exs b/test/split_test.exs index a402969..4c45d67 100644 --- a/test/split_test.exs +++ b/test/split_test.exs @@ -6,7 +6,7 @@ defmodule SplitThinElixirTest do alias Split.SplitView setup_all context do - test_id = :erlang.phash2(context.case) + test_id = :erlang.phash2(context.module) socket_path = "/tmp/test-splitd-#{test_id}.sock" start_supervised!( From ecc68d890f38ee463b321dbe53a11be609f51f59 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 12 Feb 2025 15:05:01 -0300 Subject: [PATCH 09/12] Update README --- CHANGES.txt | 2 +- README.md | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 80d48e7..0f5da52 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -5,7 +5,7 @@ - Renamed the `Split.Treatment` struct to `Split.TreatmentWithConfig` and removed the `label`, `change_number`, and `timestamp` fields. - Moved the `Split` struct to the new `Split.SplitView` module and updated some fields: renamed `configurations` to `configs`, `flag_sets` to `sets`, and added the `impressions_disabled` field. - Updated the return types of `Split.get_treatment/3` and `Split.get_treatments/3` to return a treatment string and a map of treatment strings, respectively. - - Updated all `get_treatment` function signatures: removed the third argument (`bucketing_key`) and expanded the first argument (`key`) to accept a union, allowing either a string or a map with a key and optional bucketing key (`{:matching_key, String.t(), :bucketing_key, String.t() | nil}`). + - Updated all `get_treatment` function signatures: removed the third argument (`bucketing_key`) and expanded the first argument (`key`) to accept a union, allowing either a string or a map with a key and optional bucketing key (`%{required(:matchingKey) => String.t(), optional(:bucketingKey) => String.t() | nil}`). 0.1.0 (January 27, 2025): - BREAKING CHANGES: diff --git a/README.md b/README.md index d548523..946ba0f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This SDK is designed to work with Split, the platform for controlled rollouts, w ## Compatibility -The Elixir Thin Client SDK is compatible with Elixir v1.14.0 and later. +The Elixir Thin Client SDK is compatible with Elixir v1.14.0 and later, and requires [Splitd deamon](https://help.split.io/hc/en-us/articles/18305269686157-Split-Daemon-splitd#local-deployment-recommended) v1.2.0 or later. ## Getting started @@ -30,7 +30,9 @@ After adding the dependency, run `mix deps.get` to fetch the new dependency. ### Using the SDK -Below is a simple example that describes the instantiation and most basic usage of our SDK. Keep in mind that Elixir SDK requires an [SplitD](https://help.split.io/hc/en-us/articles/18305269686157-Split-Daemon-splitd#local-deployment-recommended) instance running in your infrastructure to connect to. +Below is a simple example that describes the instantiation and most basic usage of our SDK. + +**NOTE:** Keep in mind that Elixir SDK requires an [Splitd deamon](https://help.split.io/hc/en-us/articles/18305269686157-Split-Daemon-splitd#local-deployment-recommended) instance running in your infrastructure to connect to, with the link type set to `unix-stream`. ```elixir # Start the SDK supervisor @@ -43,7 +45,7 @@ case Split.get_treatment(user_id, feature_flag_name) do "off" -> # Feature flag is disabled for this user _ -> - # "control" treatment. For example, when feature flag is not found or Elixir SDK wasn't able to connect to SplitD. + # "control" treatment. For example, when feature flag is not found or Elixir SDK wasn't able to connect to Splitd end ``` From e2165cef57ce540fd2433c2b58ac4d8e639854aa Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 12 Feb 2025 22:11:06 -0300 Subject: [PATCH 10/12] Specialize map type for attributes and properties --- README.md | 4 ++-- lib/split.ex | 31 ++++++++++++++++++++++--------- lib/split/rpc/encoder.ex | 13 ++++++++++++- lib/split/rpc/message.ex | 38 ++++++++++++++++++++++++++++++-------- lib/split/split_view.ex | 2 +- 5 files changed, 67 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 946ba0f..9d2d47a 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This SDK is designed to work with Split, the platform for controlled rollouts, w ## Compatibility -The Elixir Thin Client SDK is compatible with Elixir v1.14.0 and later, and requires [Splitd deamon](https://help.split.io/hc/en-us/articles/18305269686157-Split-Daemon-splitd#local-deployment-recommended) v1.2.0 or later. +The Elixir Thin Client SDK is compatible with Elixir v1.14.0 and later, and requires [Splitd daemon](https://help.split.io/hc/en-us/articles/18305269686157-Split-Daemon-splitd#local-deployment-recommended) v1.2.0 or later. ## Getting started @@ -32,7 +32,7 @@ After adding the dependency, run `mix deps.get` to fetch the new dependency. Below is a simple example that describes the instantiation and most basic usage of our SDK. -**NOTE:** Keep in mind that Elixir SDK requires an [Splitd deamon](https://help.split.io/hc/en-us/articles/18305269686157-Split-Daemon-splitd#local-deployment-recommended) instance running in your infrastructure to connect to, with the link type set to `unix-stream`. +**NOTE:** Keep in mind that Elixir SDK requires an [Splitd daemon](https://help.split.io/hc/en-us/articles/18305269686157-Split-Daemon-splitd#local-deployment-recommended) instance running in your infrastructure to connect to, with the link type set to `unix-stream`. ```elixir # Start the SDK supervisor diff --git a/lib/split.ex b/lib/split.ex index e7cc760..bfbb1cb 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -67,6 +67,17 @@ defmodule Split do String.t() | %{required(:matchingKey) => String.t(), optional(:bucketingKey) => String.t() | nil} + @typedoc "A map of attributes to use when evaluating feature flags." + @type attributes :: %{ + optional(atom() | String.t()) => + String.t() | integer() | boolean() | [String.t() | integer()] | nil + } + + @typedoc "A map of properties to use when tracking an event." + @type properties :: %{ + optional(atom() | String.t()) => String.t() | integer() | boolean() | nil + } + @doc """ Builds a child specification to use in a Supervisor. @@ -87,7 +98,7 @@ defmodule Split do iex> Split.get_treatment("user_id", "located_in_usa", %{country: "USA"}) "on" """ - @spec get_treatment(split_key(), String.t(), map() | nil) :: String.t() + @spec get_treatment(split_key(), String.t(), attributes() | nil) :: String.t() def get_treatment(key, feature_name, attributes \\ %{}) do request = Message.get_treatment( @@ -109,7 +120,8 @@ defmodule Split do iex> Split.get_treatment("user_id", "located_in_usa", %{country: "USA"}) %Split.TreatmentWithConfig{treatment: "on", config: nil} """ - @spec get_treatment_with_config(split_key(), String.t(), map() | nil) :: TreatmentWithConfig.t() + @spec get_treatment_with_config(split_key(), String.t(), attributes() | nil) :: + TreatmentWithConfig.t() def get_treatment_with_config(key, feature_name, attributes \\ %{}) do request = Message.get_treatment_with_config( @@ -131,7 +143,7 @@ defmodule Split do iex> Split.get_treatments("user_id", ["located_in_usa"], %{country: "USA"}) %{"located_in_usa" => "on"} """ - @spec get_treatments(split_key(), [String.t()], map() | nil) :: %{ + @spec get_treatments(split_key(), [String.t()], attributes() | nil) :: %{ String.t() => String.t() } def get_treatments(key, feature_names, attributes \\ %{}) do @@ -155,7 +167,7 @@ defmodule Split do iex> Split.get_treatments_with_config("user_id", ["located_in_usa"], %{country: "USA"}) %{"located_in_usa" => %Split.TreatmentWithConfig{treatment: "on", config: nil}} """ - @spec get_treatments_with_config(split_key(), [String.t()], map() | nil) :: %{ + @spec get_treatments_with_config(split_key(), [String.t()], attributes() | nil) :: %{ String.t() => TreatmentWithConfig.t() } def get_treatments_with_config(key, feature_names, attributes \\ %{}) do @@ -179,7 +191,7 @@ defmodule Split do iex> Split.get_treatments_by_flag_set("user_id", "frontend_flags", %{country: "USA"}) %{"located_in_usa" => "on"} """ - @spec get_treatments_by_flag_set(split_key(), String.t(), map() | nil) :: %{ + @spec get_treatments_by_flag_set(split_key(), String.t(), attributes() | nil) :: %{ String.t() => String.t() } def get_treatments_by_flag_set(key, flag_set_name, attributes \\ %{}) do @@ -206,7 +218,7 @@ defmodule Split do @spec get_treatments_with_config_by_flag_set( split_key(), String.t(), - map() | nil + attributes() | nil ) :: %{String.t() => TreatmentWithConfig.t()} def get_treatments_with_config_by_flag_set( @@ -234,7 +246,7 @@ defmodule Split do iex> Split.get_treatments_by_flag_sets("user_id", ["frontend_flags", "backend_flags"], %{country: "USA"}) %{"located_in_usa" => "on"} """ - @spec get_treatments_by_flag_sets(split_key(), [String.t()], map() | nil) :: + @spec get_treatments_by_flag_sets(split_key(), [String.t()], attributes() | nil) :: %{String.t() => String.t()} def get_treatments_by_flag_sets( key, @@ -264,7 +276,7 @@ defmodule Split do @spec get_treatments_with_config_by_flag_sets( split_key(), [String.t()], - map() | nil + attributes() | nil ) :: %{String.t() => TreatmentWithConfig.t()} def get_treatments_with_config_by_flag_sets( @@ -297,7 +309,8 @@ defmodule Split do iex> Split.track("user_id", "user", "my-event", 42, %{property1: "value1"}) true """ - @spec track(split_key(), String.t(), String.t(), number() | nil, map() | nil) :: boolean() + @spec track(split_key(), String.t(), String.t(), number() | nil, properties() | nil) :: + boolean() def track(key, traffic_type, event_type, value \\ nil, properties \\ %{}) do request = Message.track(key, traffic_type, event_type, value, properties) execute_rpc(request) diff --git a/lib/split/rpc/encoder.ex b/lib/split/rpc/encoder.ex index 0c98fc8..a21b0aa 100644 --- a/lib/split/rpc/encoder.ex +++ b/lib/split/rpc/encoder.ex @@ -12,8 +12,19 @@ defmodule Split.RPC.Encoder do iex> message = Message.split("test_split") ...> [_size, encoded] = Encoder.encode(message) ...> Msgpax.unpack!(encoded) - %{"a" => ["test_split"], "o" => 161, "v" => 1} + + + iex> message = Message.get_treatment(key: %{matching_key: "user_id"}, feature_name: "test_split", attributes: %{ :foo => "bar", "baz" => 1 }) + ...> [_size, encoded] = Encoder.encode(message) + ...> Msgpax.unpack!(encoded) + %{"a" => ["user_id", nil, "test_split", %{"baz" => 1, "foo" => "bar"}], "o" => 17, "v" => 1} + + + iex> message = Message.track(%{matching_key: "user_id", bucketing_key: "bucket"}, "user", "purchase", 100.5, %{ "baz" => 1, foo: "bar" }) + ...> [_size, encoded] = Encoder.encode(message) + ...> Msgpax.unpack!(encoded) + %{"a" => ["user_id", "user", "purchase", 100.5, %{"baz" => 1, "foo" => "bar"}], "o" => 128, "v" => 1} """ @spec encode(Message.t()) :: iodata() def encode(message) do diff --git a/lib/split/rpc/message.ex b/lib/split/rpc/message.ex index 8c5cb47..7a8d91d 100644 --- a/lib/split/rpc/message.ex +++ b/lib/split/rpc/message.ex @@ -24,12 +24,12 @@ defmodule Split.RPC.Message do @type get_treatment_args :: {:key, Split.split_key()} | {:feature_name, String.t()} - | {:attributes, map() | nil} + | {:attributes, Split.attributes() | nil} @type get_treatments_args :: {:key, Split.split_key()} | {:feature_names, list(String.t())} - | {:attributes, map() | nil} + | {:attributes, Split.attributes() | nil} @doc """ Builds a message to register a client in splitd. @@ -49,7 +49,8 @@ defmodule Split.RPC.Message do iex> Message.get_treatment( ...> key: %{:matching_key => "user_key", :bucketing_key => "bucketing_key"}, - ...> feature_name: "feature_name" + ...> feature_name: "feature_name", + ...> attributes: %{} ...> ) %Message{a: ["user_key", "bucketing_key", "feature_name", %{}], o: 17, v: 1} @@ -68,9 +69,10 @@ defmodule Split.RPC.Message do iex> Message.get_treatment_with_config( ...> key: %{:matching_key => "user_key", :bucketing_key => "bucketing_key"}, - ...> feature_name: "feature_name" + ...> feature_name: "feature_name", + ...> attributes: %{"foo" => "bar", :baz => 1} ...> ) - %Message{a: ["user_key", "bucketing_key", "feature_name", %{}], o: 19, v: 1} + %Message{a: ["user_key", "bucketing_key", "feature_name", %{"foo" => "bar", :baz => 1}], o: 19, v: 1} iex> Message.get_treatment_with_config( ...> key: "user_key", @@ -289,7 +291,7 @@ defmodule Split.RPC.Message do ## Examples - iex> Message.track("user_key", "traffic_type", "my_event", 1.5, %{foo: "bar"}) + iex> Message.track("user_key", "traffic_type", "my_event", 1.5, %{:foo => "bar"}) %Message{ v: 1, o: 128, @@ -299,11 +301,19 @@ defmodule Split.RPC.Message do iex> Message.track("user_key", "traffic_type", "my_event") %Message{v: 1, o: 128, a: ["user_key", "traffic_type", "my_event", nil, %{}]} """ - @spec track(String.t(), String.t(), String.t(), any(), map()) :: t() + @spec track(Split.split_key(), String.t(), String.t(), number() | nil, Split.properties()) :: + t() def track(key, traffic_type, event_type, value \\ nil, properties \\ %{}) do + matching_key = + if is_map(key) do + key.matching_key + else + key + end + %__MODULE__{ o: @track_opcode, - a: [key, traffic_type, event_type, value, properties] + a: [matching_key, traffic_type, event_type, value, properties] } end @@ -324,6 +334,18 @@ defmodule Split.RPC.Message do iex> Message.opcode_to_rpc_name(@get_treatments_with_config_opcode) :get_treatments_with_config + iex> Message.opcode_to_rpc_name(@get_treatments_by_flag_set_opcode) + :get_treatments_by_flag_set + + iex> Message.opcode_to_rpc_name(@get_treatments_with_config_by_flag_set_opcode) + :get_treatments_with_config_by_flag_set + + iex> Message.opcode_to_rpc_name(@get_treatments_by_flag_sets_opcode) + :get_treatments_by_flag_sets + + iex> Message.opcode_to_rpc_name(@get_treatments_with_config_by_flag_sets_opcode) + :get_treatments_with_config_by_flag_sets + iex> Message.opcode_to_rpc_name(@split_opcode) :split diff --git a/lib/split/split_view.ex b/lib/split/split_view.ex index c4d7352..b445f5e 100644 --- a/lib/split/split_view.ex +++ b/lib/split/split_view.ex @@ -32,7 +32,7 @@ defmodule Split.SplitView do killed: boolean(), treatments: [String.t()], change_number: integer(), - configs: map(), + configs: %{String.t() => String.t() | nil}, default_treatment: String.t(), sets: [String.t()], impressions_disabled: boolean() From 0193d1afa4f3b546772ae09e6327acbe99e22a84 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 13 Feb 2025 15:48:24 -0300 Subject: [PATCH 11/12] Doc comments fixes --- lib/split.ex | 2 +- lib/split/rpc/message.ex | 2 +- lib/split/split_view.ex | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/split.ex b/lib/split.ex index bfbb1cb..6380993 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -50,7 +50,7 @@ defmodule Split do alias Split.RPC.Message alias Split.RPC.ResponseParser - @typedoc "An option that can be provided when starting `Split`." + @typedoc "An option that can be provided when starting `Split`. See [options](#module-options) for more information." @type option :: {:socket_path, String.t()} | {:pool_size, non_neg_integer()} diff --git a/lib/split/rpc/message.ex b/lib/split/rpc/message.ex index 7a8d91d..c0584d7 100644 --- a/lib/split/rpc/message.ex +++ b/lib/split/rpc/message.ex @@ -1,5 +1,5 @@ defmodule Split.RPC.Message do - @doc """ + @moduledoc """ Represents an RPC message to be sent to splitd. """ use Split.RPC.Opcodes diff --git a/lib/split/split_view.ex b/lib/split/split_view.ex index b445f5e..2a904bf 100644 --- a/lib/split/split_view.ex +++ b/lib/split/split_view.ex @@ -1,6 +1,6 @@ defmodule Split.SplitView do @moduledoc """ - This module is a struct that contains information of a feature flag. + This module defines a struct that contains information about a feature flag. ## Fields * `:name` - The name of the feature flag From ba149589af653d076dd06db55c6e00814bd7487d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 14 Feb 2025 11:23:41 -0300 Subject: [PATCH 12/12] stable version --- CHANGES.txt | 2 +- mix.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 0f5da52..eaff78b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,4 @@ -0.2.0 (February XX, 2025): +0.2.0 (February 14, 2025): - Added new variations of the get treatment functions to support evaluating flags in given flag set/s: `Split.get_treatments_by_flag_set/3`, `Split.get_treatments_by_flag_sets/3`, `Split.get_treatments_with_config_by_flag_set/3`, and `Split.get_treatments_with_config_by_flag_sets/3`. - BREAKING CHANGES: - Removed the `fallback_enabled` option from `Split.Supervisor.start_link/1`. Fallback behavior is now always enabled, so `Split` functions no longer return `{:error, _}` tuples but instead use the fallback value when an error occurs. diff --git a/mix.exs b/mix.exs index ed9a7ab..2caeccf 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule SplitThinElixir.MixProject do def project do [ app: :split, - version: "0.2.0-rc.0", + version: "0.2.0", elixir: "~> 1.14", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod,