From 589bd86ee1e42266a8a68ef5b5363f86595df999 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 17 Jan 2025 15:01:35 -0300 Subject: [PATCH 01/46] Update SDK function signatures --- lib/rpc/response_parser.ex | 7 ++++--- lib/split.ex | 13 ++++++++----- lib/split/treatment.ex | 29 +++++++++++++++++++++++++++++ lib/split/treatment_with_config.ex | 9 +++++++++ test/rpc/response_parser_test.exs | 17 ++++++++++------- test/split_test.exs | 2 +- 6 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 lib/split/treatment_with_config.ex diff --git a/lib/rpc/response_parser.ex b/lib/rpc/response_parser.ex index 78422c9..3da18aa 100644 --- a/lib/rpc/response_parser.ex +++ b/lib/rpc/response_parser.ex @@ -78,7 +78,7 @@ defmodule Split.RPC.ResponseParser do }, _opts ) do - {:ok, %{split_names: split_names}} + {:ok, split_names} end def parse_response( @@ -164,9 +164,10 @@ defmodule Split.RPC.ResponseParser do killed: payload["k"], treatments: payload["s"], change_number: payload["c"], - configurations: payload["f"], + configs: payload["f"], default_treatment: payload["d"], - flag_sets: payload["e"] + sets: payload["e"], + impressions_disabled: payload["i"] || false } end end diff --git a/lib/split.ex b/lib/split.ex index 4a430bb..35c91eb 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -8,15 +8,17 @@ defmodule Split do alias Split.RPC.Message alias Split.RPC.ResponseParser + # @TODO move struct to Split.SplitView module and document it @type t :: %Split{ name: String.t(), traffic_type: String.t(), killed: boolean(), treatments: [String.t()], change_number: integer(), - configurations: map(), + configs: map(), default_treatment: String.t(), - flag_sets: [String.t()] + sets: [String.t()], + impressions_disabled: boolean() } defstruct [ @@ -25,9 +27,10 @@ defmodule Split do :killed, :treatments, :change_number, - :configurations, + :configs, :default_treatment, - :flag_sets + :sets, + :impressions_disabled ] @spec get_treatment(String.t(), String.t(), String.t() | nil, map() | nil) :: @@ -92,7 +95,7 @@ defmodule Split do execute_rpc(request) end - @spec split_names() :: {:ok, %{split_names: String.t()}} | {:error, term()} + @spec split_names() :: {:ok, [String.t()]} | {:error, term()} def split_names do request = Message.split_names() execute_rpc(request) diff --git a/lib/split/treatment.ex b/lib/split/treatment.ex index 6cea07b..5fc1475 100644 --- a/lib/split/treatment.ex +++ b/lib/split/treatment.ex @@ -29,4 +29,33 @@ defmodule Split.Treatment do timestamp: timestamp } end + + @spec map_to_treatment_with_config(t()) :: Split.TreatmentWithConfig.t() + def map_to_treatment_with_config(treatment) do + %Split.TreatmentWithConfig{ + treatment: treatment.treatment, + config: treatment.config + } + end + + @spec map_to_treatment_string(t()) :: String.t() + def map_to_treatment_string(treatment) do + treatment.treatment + end + + @spec map_treatments_to_treatments_string({:ok, %{String.t() => Split.Treatment.t()}}) :: + %{String.t() => String.t()} + def map_treatments_to_treatments_string(treatments) do + treatments + |> Enum.map(fn {key, treatment} -> {key, treatment.treatment} end) + |> Enum.into(%{}) + end + + @spec map_treatments_to_treatments_with_config({:ok, %{String.t() => Split.Treatment.t()}}) :: + %{String.t() => Split.TreatmentWithConfig.t()} + def map_treatments_to_treatments_with_config(treatments) do + treatments + |> Enum.map(fn {key, treatment} -> {key, map_to_treatment_with_config(treatment)} end) + |> Enum.into(%{}) + end end diff --git a/lib/split/treatment_with_config.ex b/lib/split/treatment_with_config.ex new file mode 100644 index 0000000..cac3bdd --- /dev/null +++ b/lib/split/treatment_with_config.ex @@ -0,0 +1,9 @@ +defmodule Split.TreatmentWithConfig do + defstruct treatment: "control", + config: nil + + @type t :: %__MODULE__{ + treatment: String.t(), + config: String.t() | nil + } +end diff --git a/test/rpc/response_parser_test.exs b/test/rpc/response_parser_test.exs index d15f261..e8291d3 100644 --- a/test/rpc/response_parser_test.exs +++ b/test/rpc/response_parser_test.exs @@ -177,9 +177,10 @@ defmodule Split.RPC.ResponseParserTest do killed: false, treatments: ["treatment_a", "treatment_b", "treatment_c"], change_number: 1_499_375_079_065, - configurations: %{}, + configs: %{}, default_treatment: "treatment_a", - flag_sets: [] + sets: [], + impressions_disabled: false }} end @@ -233,9 +234,10 @@ defmodule Split.RPC.ResponseParserTest do killed: false, treatments: ["treatment_a", "treatment_b", "treatment_c"], change_number: 1_499_375_079_065, - configurations: %{}, + configs: %{}, default_treatment: "treatment_a", - flag_sets: [] + sets: [], + impressions_disabled: false }, %Split{ name: "feature_b", @@ -243,9 +245,10 @@ defmodule Split.RPC.ResponseParserTest do killed: false, treatments: ["on", "off"], change_number: 1_499_375_079_066, - configurations: %{}, + configs: %{}, default_treatment: "off", - flag_sets: [] + sets: [], + impressions_disabled: false } ]} end @@ -266,7 +269,7 @@ defmodule Split.RPC.ResponseParserTest do }} assert ResponseParser.parse_response(response, message) == - {:ok, %{split_names: ["feature_a", "feature_b"]}} + {:ok, ["feature_a", "feature_b"]} end test "parses successful track RPC call" do diff --git a/test/split_test.exs b/test/split_test.exs index cf15a6b..254be0d 100644 --- a/test/split_test.exs +++ b/test/split_test.exs @@ -101,7 +101,7 @@ defmodule SplitThinElixirTest do end test "split_names/0" do - assert {:ok, %{split_names: ["ethan_test"]}} == Split.split_names() + assert {:ok, ["ethan_test"]} == Split.split_names() end test "split/1" do From 3ac27770881979ef2611dc4c3d27e0112ebf6254 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 20 Jan 2025 12:56:02 -0300 Subject: [PATCH 02/46] Support keyword list as options for the Split supervisor and raise a descriptive error if socket_path is not provided --- .gitignore | 2 ++ lib/sockets/pool.ex | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f1a03a3..1a3ddc0 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ split_thin_elixir-*.tar splitd # Ignore the splitd configuration file support/splitd.yaml + +/.vscode/ \ No newline at end of file diff --git a/lib/sockets/pool.ex b/lib/sockets/pool.ex index 41b08b6..beb2e06 100644 --- a/lib/sockets/pool.ex +++ b/lib/sockets/pool.ex @@ -16,7 +16,16 @@ defmodule Split.Sockets.Pool do } end - def start_link(opts) do + def start_link(opts) when is_list(opts) do + start_link(Map.new(opts)) + end + + def start_link(opts) when is_map(opts) do + # Validate required options + unless Map.has_key?(opts, :socket_path) do + raise ArgumentError, "socket_path is required" + end + fallback_enabled = Map.get(opts, :fallback_enabled, false) :persistent_term.put(:splitd_fallback_enabled, fallback_enabled) From c1e981ef4bc59a726c84947c6863bceffe510313 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 20 Jan 2025 16:01:12 -0300 Subject: [PATCH 03/46] Add default for socket_path --- README.md | 6 +++--- lib/sockets/pool.ex | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d39d873..c4bdcc9 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,10 @@ Or by starting it manually: Split.Sockets.Supervisor.start_link(opts) ``` -Where `opts` is a keyword list with the following options: +Where `opts` is a keyword list or map with the following options: -- `:socket_path` - The path to the splitd socket file. For example `/var/run/splitd.sock`. -- `:fallback_enabled` - A boolean that indicates wether we should return errors when RPC communication fails or falling back to a default value . Default is `false`. +- `:socket_path` - The path to the splitd socket file. Default is `/var/run/splitd.sock`. +- `:fallback_enabled` - A boolean that indicates wether we should return errors when RPC communication fails or falling back to a default value. Default is `false`. Once you have started Split, you are ready to start interacting with the Split.io splitd's daemon. diff --git a/lib/sockets/pool.ex b/lib/sockets/pool.ex index beb2e06..ea3f401 100644 --- a/lib/sockets/pool.ex +++ b/lib/sockets/pool.ex @@ -21,11 +21,7 @@ defmodule Split.Sockets.Pool do end def start_link(opts) when is_map(opts) do - # Validate required options - unless Map.has_key?(opts, :socket_path) do - raise ArgumentError, "socket_path is required" - end - + socket_path = Map.get(opts, :socket_path, "/var/run/splitd.sock") fallback_enabled = Map.get(opts, :fallback_enabled, false) :persistent_term.put(:splitd_fallback_enabled, fallback_enabled) @@ -34,6 +30,7 @@ defmodule Split.Sockets.Pool do opts = opts + |> Map.put_new(:socket_path, socket_path) |> Map.put_new(:fallback_enabled, fallback_enabled) |> Map.put_new(:pool_size, pool_size) |> Map.put_new(:pool_name, pool_name) From 3877b5d5d43739be861226e4ca55fa7e9a11f8d9 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 21 Jan 2025 10:26:42 -0300 Subject: [PATCH 04/46] Fix fallback for split_names operation --- lib/rpc/fallback.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rpc/fallback.ex b/lib/rpc/fallback.ex index aa9b083..01186c4 100644 --- a/lib/rpc/fallback.ex +++ b/lib/rpc/fallback.ex @@ -41,7 +41,7 @@ defmodule Split.RPC.Fallback do {:ok, []} iex> Fallback.fallback(%Message{o: 0xA0}) - {:ok, %{split_names: []}} + {:ok, []} iex> Fallback.fallback(%Message{o: 0x80}) :ok @@ -73,7 +73,7 @@ defmodule Split.RPC.Fallback do end def fallback(%Message{o: @split_names_opcode}) do - {:ok, %{split_names: []}} + {:ok, []} end def fallback(%Message{o: @track_opcode}) do From 1a79e7bfc2bca1f132eb59c8151b133a706f62db Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 29 Jan 2025 01:26:49 -0300 Subject: [PATCH 05/46] Add get_treatment_by_flag_set methods --- lib/split.ex | 84 +++++++++++++- lib/split/rpc/fallback.ex | 11 ++ lib/split/rpc/message.ex | 120 +++++++++++++++++++ lib/split/rpc/opcodes.ex | 8 ++ lib/split/rpc/response_parser.ex | 28 +++++ test/rpc/response_parser_test.exs | 180 +++++++++++++++++++++++++++++ test/split_test.exs | 89 ++++++++++++++ test/support/mock_splitd_server.ex | 12 ++ 8 files changed, 531 insertions(+), 1 deletion(-) diff --git a/lib/split.ex b/lib/split.ex index 089a42e..3ed71f7 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -156,7 +156,89 @@ defmodule Split do execute_rpc(request) end - @spec track(String.t(), String.t(), String.t(), term(), map()) :: :ok | {:error, term()} + @spec get_treatments_by_flag_set(String.t(), String.t(), String.t() | nil, map() | nil) :: + {:ok, %{String.t() => Treatment.t()}} | {:error, term()} + def get_treatments_by_flag_set(user_key, flag_set_name, bucketing_key \\ nil, attributes \\ %{}) do + request = + Message.get_treatments_by_flag_set( + user_key: user_key, + feature_name: flag_set_name, + bucketing_key: bucketing_key, + attributes: attributes + ) + + execute_rpc(request) + end + + @spec get_treatments_with_config_by_flag_set( + String.t(), + String.t(), + String.t() | nil, + map() | nil + ) :: + {:ok, %{String.t() => Treatment.t()}} | {:error, term()} + def get_treatments_with_config_by_flag_set( + user_key, + flag_set_name, + bucketing_key \\ nil, + attributes \\ %{} + ) do + request = + Message.get_treatments_with_config_by_flag_set( + user_key: user_key, + feature_name: flag_set_name, + bucketing_key: bucketing_key, + attributes: attributes + ) + + execute_rpc(request) + end + + @spec get_treatments_by_flag_sets(String.t(), [String.t()], String.t() | nil, map() | nil) :: + {:ok, %{String.t() => Treatment.t()}} | {:error, term()} + def get_treatments_by_flag_sets( + user_key, + flag_set_names, + bucketing_key \\ nil, + attributes \\ %{} + ) do + request = + Message.get_treatments_by_flag_sets( + user_key: user_key, + feature_names: flag_set_names, + bucketing_key: bucketing_key, + attributes: attributes + ) + + execute_rpc(request) + end + + @spec get_treatments_with_config_by_flag_sets( + String.t(), + [String.t()], + String.t() | nil, + map() | nil + ) :: + {:ok, %{String.t() => Treatment.t()}} | {:error, term()} + def get_treatments_with_config_by_flag_sets( + user_key, + flag_set_names, + bucketing_key \\ nil, + attributes \\ %{} + ) do + request = + Message.get_treatments_with_config_by_flag_sets( + user_key: user_key, + feature_names: flag_set_names, + bucketing_key: bucketing_key, + attributes: attributes + ) + + execute_rpc(request) + end + + @spec track(String.t(), String.t(), String.t(), number() | nil, map() | nil) :: + :ok | {:error, term()} def track(user_key, traffic_type, event_type, value \\ nil, properties \\ %{}) do request = Message.track(user_key, traffic_type, event_type, value, properties) execute_rpc(request) diff --git a/lib/split/rpc/fallback.ex b/lib/split/rpc/fallback.ex index 01186c4..b75dc1a 100644 --- a/lib/split/rpc/fallback.ex +++ b/lib/split/rpc/fallback.ex @@ -64,6 +64,17 @@ defmodule Split.RPC.Fallback do {:ok, treatments} end + def fallback(%Message{o: opcode, a: _}) + when opcode in [ + @get_treatments_by_flag_set_opcode, + @get_treatments_with_config_by_flag_set_opcode, + @get_treatments_by_flag_sets_opcode, + @get_treatments_with_config_by_flag_sets_opcode + ] do + + {:ok, %{}} # Empty map since we don't have a way to know the feature names + end + def fallback(%Message{o: @split_opcode}) do {:ok, nil} end diff --git a/lib/split/rpc/message.ex b/lib/split/rpc/message.ex index 434cf6a..b1db532 100644 --- a/lib/split/rpc/message.ex +++ b/lib/split/rpc/message.ex @@ -145,6 +145,122 @@ defmodule Split.RPC.Message do treatment_payload(opts, @get_treatments_with_config_opcode, multiple: true) end + @doc """ + Builds a message to get the treatments for a flag set. + + ## Examples + + iex> Message.get_treatments_by_flag_set( + ...> user_key: "user_key", + ...> feature_name: "flag_set_name", + ...> bucketing_key: "bucketing_key" + ...> ) + %Message{ + a: ["user_key", "bucketing_key", "flag_set_name", %{}], + o: 21, + v: 1 + } + + iex> Message.get_treatments_by_flag_set( + ...> user_key: "user_key", + ...> feature_name: "flag_set_name" + ...> ) + %Message{a: ["user_key", nil, "flag_set_name", %{}], o: 21, v: 1} + """ + @spec get_treatments_by_flag_set([get_treatment_args()]) :: t() + def get_treatments_by_flag_set(opts) do + treatment_payload(opts, @get_treatments_by_flag_set_opcode, multiple: false) + end + + @doc """ + Builds a message to get the treatments for a flag set with configuration. + + ## Examples + + iex> Message.get_treatments_with_config_by_flag_set( + ...> user_key: "user_key", + ...> feature_name: "flag_set_name", + ...> bucketing_key: "bucketing_key" + ...> ) + %Message{ + a: ["user_key", "bucketing_key", "flag_set_name", %{}], + o: 22, + v: 1 + } + + iex> Message.get_treatments_with_config_by_flag_set( + ...> user_key: "user_key", + ...> feature_name: "flag_set_name" + ...> ) + %Message{ + a: ["user_key", nil, "flag_set_name", %{}], + o: 22, + v: 1 + } + """ + @spec get_treatments_with_config_by_flag_set([get_treatment_args()]) :: t() + def get_treatments_with_config_by_flag_set(opts) do + treatment_payload(opts, @get_treatments_with_config_by_flag_set_opcode, multiple: false) + end + + @doc """ + Builds a message to get the treatments for multiple flag sets. + + ## Examples + + iex> Message.get_treatments_by_flag_sets( + ...> user_key: "user_key", + ...> feature_names: ["flag_set_name1", "flag_set_name2"], + ...> bucketing_key: "bucketing_key" + ...> ) + %Message{ + a: ["user_key", "bucketing_key", ["flag_set_name1", "flag_set_name2"], %{}], + o: 23, + v: 1 + } + + iex> Message.get_treatments_by_flag_sets( + ...> user_key: "user_key", + ...> feature_names: ["flag_set_name1", "flag_set_name2"] + ...> ) + %Message{a: ["user_key", nil, ["flag_set_name1", "flag_set_name2"], %{}], o: 23, v: 1} + """ + @spec get_treatments_by_flag_sets([get_treatments_args()]) :: t() + def get_treatments_by_flag_sets(opts) do + treatment_payload(opts, @get_treatments_by_flag_sets_opcode, multiple: true) + end + + @doc """ + Builds a message to get the treatments for multiple flag sets with configuration. + + ## Examples + + iex> Message.get_treatments_with_config_by_flag_sets( + ...> user_key: "user_key", + ...> feature_names: ["flag_set_name1", "flag_set_name2"], + ...> bucketing_key: "bucketing_key" + ...> ) + %Message{ + a: ["user_key", "bucketing_key", ["flag_set_name1", "flag_set_name2"], %{}], + o: 24, + v: 1 + } + + iex> Message.get_treatments_with_config_by_flag_sets( + ...> user_key: "user_key", + ...> feature_names: ["flag_set_name1", "flag_set_name2"] + ...> ) + %Message{ + a: ["user_key", nil, ["flag_set_name1", "flag_set_name2"], %{}], + o: 24, + v: 1 + } + """ + @spec get_treatments_with_config_by_flag_sets([get_treatments_args()]) :: t() + def get_treatments_with_config_by_flag_sets(opts) do + treatment_payload(opts, @get_treatments_with_config_by_flag_sets_opcode, multiple: true) + end + @doc """ Builds a message to return information about an specific split (feature flag). @@ -235,6 +351,10 @@ defmodule Split.RPC.Message do def opcode_to_rpc_name(@get_treatments_opcode), do: :get_treatments def opcode_to_rpc_name(@get_treatment_with_config_opcode), do: :get_treatment_with_config def opcode_to_rpc_name(@get_treatments_with_config_opcode), do: :get_treatments_with_config + def opcode_to_rpc_name(@get_treatments_by_flag_set_opcode), do: :get_treatments_by_flag_set + def opcode_to_rpc_name(@get_treatments_with_config_by_flag_set_opcode), do: :get_treatments_with_config_by_flag_set + def opcode_to_rpc_name(@get_treatments_by_flag_sets_opcode), do: :get_treatments_by_flag_sets + def opcode_to_rpc_name(@get_treatments_with_config_by_flag_sets_opcode), do: :get_treatments_with_config_by_flag_sets def opcode_to_rpc_name(@split_opcode), do: :split def opcode_to_rpc_name(@splits_opcode), do: :splits def opcode_to_rpc_name(@split_names_opcode), do: :split_names diff --git a/lib/split/rpc/opcodes.ex b/lib/split/rpc/opcodes.ex index 7ece6f4..b9c1e83 100644 --- a/lib/split/rpc/opcodes.ex +++ b/lib/split/rpc/opcodes.ex @@ -8,6 +8,10 @@ defmodule Split.RPC.Opcodes do @get_treatments_opcode 0x12 @get_treatment_with_config_opcode 0x13 @get_treatments_with_config_opcode 0x14 + @get_treatments_by_flag_set_opcode 0x15 + @get_treatments_with_config_by_flag_set_opcode 0x16 + @get_treatments_by_flag_sets_opcode 0x17 + @get_treatments_with_config_by_flag_sets_opcode 0x18 @split_opcode 0xA1 @splits_opcode 0xA2 @split_names_opcode 0xA0 @@ -19,6 +23,10 @@ defmodule Split.RPC.Opcodes do @get_treatment_with_config_opcode, @get_treatments_opcode, @get_treatments_with_config_opcode, + @get_treatments_by_flag_set_opcode, + @get_treatments_with_config_by_flag_set_opcode, + @get_treatments_by_flag_sets_opcode, + @get_treatments_with_config_by_flag_sets_opcode, @split_opcode, @splits_opcode, @split_names_opcode, diff --git a/lib/split/rpc/response_parser.ex b/lib/split/rpc/response_parser.ex index 3da18aa..2f493f9 100644 --- a/lib/split/rpc/response_parser.ex +++ b/lib/split/rpc/response_parser.ex @@ -63,6 +63,34 @@ defmodule Split.RPC.ResponseParser do {:ok, mapped_treatments} end + def parse_response( + {:ok, %{"s" => @status_ok, "p" => %{"r" => treatments}}}, + %Message{ + o: opcode, + a: args + }, + _opts + ) + when opcode in [ + @get_treatments_by_flag_set_opcode, + @get_treatments_with_config_by_flag_set_opcode, + @get_treatments_by_flag_sets_opcode, + @get_treatments_with_config_by_flag_sets_opcode + ] do + user_key = Enum.at(args, 0) + + mapped_treatments = + treatments + |> Enum.map(fn {feature_name, treatment} -> + processed_treatment = Treatment.build_from_daemon_response(treatment) + Telemetry.send_impression(user_key, feature_name, processed_treatment) + {feature_name, processed_treatment} + end) + |> Map.new() + + {:ok, mapped_treatments} + end + def parse_response( {:ok, %{"s" => @status_ok, "p" => payload}}, %Message{o: @split_opcode}, diff --git a/test/rpc/response_parser_test.exs b/test/rpc/response_parser_test.exs index e8291d3..7ad4e6f 100644 --- a/test/rpc/response_parser_test.exs +++ b/test/rpc/response_parser_test.exs @@ -146,6 +146,186 @@ defmodule Split.RPC.ResponseParserTest do }} end + test "parses get_treatments_by_flag_set RPC response" do + message = %Message{ + o: @get_treatments_by_flag_set_opcode, + a: ["user_key", "bucketing_key", "flag_set_name"] + } + + response = + {:ok, + %{ + "s" => 1, + "p" => %{ + "r" => %{ + "feature_name1" => %{ + "t" => "on", + "l" => %{"l" => "test label 1", "c" => 123, "m" => 1_723_742_604} + }, + "feature_name2" => %{ + "t" => "off", + "l" => %{"l" => "test label 2", "c" => 456, "m" => 1_723_742_604} + } + } + } + }} + + assert ResponseParser.parse_response(response, message) == + {:ok, + %{ + "feature_name1" => %Split.Treatment{ + treatment: "on", + label: "test label 1", + config: nil, + change_number: 123, + timestamp: 1_723_742_604 + }, + "feature_name2" => %Split.Treatment{ + treatment: "off", + label: "test label 2", + config: nil, + change_number: 456, + timestamp: 1_723_742_604 + } + }} + end + + test "parses get_treatments_with_config_by_flag_set RPC response" do + message = %Message{ + o: @get_treatments_with_config_by_flag_set_opcode, + a: ["user_key", "bucketing_key", "flag_set_name"] + } + + response = + {:ok, + %{ + "s" => 1, + "p" => %{ + "r" => %{ + "feature_name1" => %{ + "t" => "on", + "l" => %{"l" => "test label 1", "c" => 123, "m" => 1_723_742_604}, + "c" => "{\"foo\": \"bar\"}" + }, + "feature_name2" => %{ + "t" => "off", + "l" => %{"l" => "test label 2", "c" => 456, "m" => 1_723_742_604}, + "c" => "{\"baz\": \"qux\"}" + } + } + } + }} + + assert ResponseParser.parse_response(response, message) == + {:ok, + %{ + "feature_name1" => %Split.Treatment{ + treatment: "on", + label: "test label 1", + config: "{\"foo\": \"bar\"}", + change_number: 123, + timestamp: 1_723_742_604 + }, + "feature_name2" => %Split.Treatment{ + treatment: "off", + label: "test label 2", + config: "{\"baz\": \"qux\"}", + change_number: 456, + timestamp: 1_723_742_604 + } + }} + end + + test "parses get_treatments_by_flag_sets RPC response" do + message = %Message{ + o: @get_treatments_by_flag_sets_opcode, + a: ["user_key", "bucketing_key", ["flag_set_name1", "flag_set_name2"]] + } + + response = + {:ok, + %{ + "s" => 1, + "p" => %{ + "r" => %{ + "feature_name1" => %{ + "t" => "on", + "l" => %{"l" => "test label 1", "c" => 123, "m" => 1_723_742_604} + }, + "feature_name2" => %{ + "t" => "off", + "l" => %{"l" => "test label 2", "c" => 456, "m" => 1_723_742_604} + } + } + } + }} + + assert ResponseParser.parse_response(response, message) == + {:ok, + %{ + "feature_name1" => %Split.Treatment{ + treatment: "on", + label: "test label 1", + config: nil, + change_number: 123, + timestamp: 1_723_742_604 + }, + "feature_name2" => %Split.Treatment{ + treatment: "off", + label: "test label 2", + config: nil, + change_number: 456, + timestamp: 1_723_742_604 + } + }} + end + + test "parses get_treatments_with_config_by_flag_sets RPC response" do + message = %Message{ + o: @get_treatments_with_config_by_flag_sets_opcode, + a: ["user_key", "bucketing_key", ["flag_set_name1", "flag_set_name2"]] + } + + response = + {:ok, + %{ + "s" => 1, + "p" => %{ + "r" => %{ + "feature_name1" => %{ + "t" => "on", + "l" => %{"l" => "test label 1", "c" => 123, "m" => 1_723_742_604}, + "c" => "{\"foo\": \"bar\"}" + }, + "feature_name2" => %{ + "t" => "off", + "l" => %{"l" => "test label 2", "c" => 456, "m" => 1_723_742_604}, + "c" => "{\"baz\": \"qux\"}" + } + } + } + }} + + assert ResponseParser.parse_response(response, message) == + {:ok, + %{ + "feature_name1" => %Split.Treatment{ + treatment: "on", + label: "test label 1", + config: "{\"foo\": \"bar\"}", + change_number: 123, + timestamp: 1_723_742_604 + }, + "feature_name2" => %Split.Treatment{ + treatment: "off", + label: "test label 2", + config: "{\"baz\": \"qux\"}", + change_number: 456, + timestamp: 1_723_742_604 + } + }} + end + test "parses split RPC call" do message = %Message{o: @split_opcode, a: []} diff --git a/test/split_test.exs b/test/split_test.exs index d39d851..1182928 100644 --- a/test/split_test.exs +++ b/test/split_test.exs @@ -94,6 +94,95 @@ defmodule SplitThinElixirTest do end end + describe "get_treatments_by_flag_set/2" do + test "returns expected map with structs" do + assert {:ok, %{"emi_test" => %Treatment{treatment: "on"}}} = + Split.get_treatments_by_flag_set( + "user-id-" <> to_string(Enum.random(1..100_000)), + "flag_set_name" + ) + end + + test "emits telemetry event for impression listening" do + ref = :telemetry_test.attach_event_handlers(self(), [[:split, :impression]]) + + Split.get_treatments_by_flag_set( + "user-id-" <> to_string(Enum.random(1..100_000)), + "flag_set_name" + ) + + assert_received {[:split, :impression], ^ref, _, %{impression: %Impression{}}} + end + end + + describe "get_treatments_with_config_by_flag_set/2" do + test "returns expected struct" do + assert {:ok, %{"emi_test" => %Treatment{treatment: "on", config: %{"foo" => "bar"}}}} = + Split.get_treatments_with_config_by_flag_set( + "user-id-" <> to_string(Enum.random(1..100_000)), + "flag_set_name" + ) + end + + test "emits telemetry event for impression listening" do + ref = :telemetry_test.attach_event_handlers(self(), [[:split, :impression]]) + + Split.get_treatments_with_config_by_flag_set( + "user-id-" <> to_string(Enum.random(1..100_000)), + "flag_set_name" + ) + + assert_received {[:split, :impression], ^ref, _, %{impression: %Impression{}}} + end + end + + describe "get_treatments_by_flag_sets/2" do + test "returns expected map with structs" do + assert {:ok, %{"emi_test" => %Treatment{treatment: "on"}}} = + Split.get_treatments_by_flag_sets( + "user-id-" <> to_string(Enum.random(1..100_000)), + [ + "flag_set_name" + ] + ) + end + + test "emits telemetry event for impression listening" do + ref = :telemetry_test.attach_event_handlers(self(), [[:split, :impression]]) + + Split.get_treatments_by_flag_sets("user-id-" <> to_string(Enum.random(1..100_000)), [ + "flag_set_name" + ]) + + assert_received {[:split, :impression], ^ref, _, %{impression: %Impression{}}} + end + end + + describe "get_treatments_with_config_by_flag_sets/2" do + test "returns expected struct" do + assert {:ok, %{"emi_test" => %Treatment{treatment: "on", config: %{"foo" => "bar"}}}} = + Split.get_treatments_with_config_by_flag_sets( + "user-id-" <> to_string(Enum.random(1..100_000)), + [ + "flag_set_name" + ] + ) + end + + test "emits telemetry event for impression listening" do + ref = :telemetry_test.attach_event_handlers(self(), [[:split, :impression]]) + + Split.get_treatments_with_config_by_flag_sets( + "user-id-" <> to_string(Enum.random(1..100_000)), + [ + "flag_set_name" + ] + ) + + assert_received {[:split, :impression], ^ref, _, %{impression: %Impression{}}} + end + end + test "track/3" do assert :ok = Split.track("user-id-" <> to_string(Enum.random(1..100_000)), "account", "purchase") diff --git a/test/support/mock_splitd_server.ex b/test/support/mock_splitd_server.ex index f3668a2..91190c0 100644 --- a/test/support/mock_splitd_server.ex +++ b/test/support/mock_splitd_server.ex @@ -105,6 +105,18 @@ defmodule Split.Test.MockSplitdServer do %{"s" => 1, "p" => %{"r" => [%{"t" => "on", "c" => %{"foo" => "bar"}}]}} end + defp build_response(21), do: %{"s" => 1, "p" => %{"r" => %{"emi_test" => %{"t" => "on"}}}} + + defp build_response(22) do + %{"s" => 1, "p" => %{"r" => %{"emi_test" => %{"t" => "on", "c" => %{"foo" => "bar"}}}}} + end + + defp build_response(23), do: %{"s" => 1, "p" => %{"r" => %{"emi_test" => %{"t" => "on"}}}} + + defp build_response(24) do + %{"s" => 1, "p" => %{"r" => %{"emi_test" => %{"t" => "on", "c" => %{"foo" => "bar"}}}}} + end + defp build_response(128), do: %{"s" => 1, "p" => %{"s" => true}} defp build_response(160), do: %{"s" => 1, "p" => %{"n" => ["ethan_test"]}} From 53e254304dff47bd9fd20005ed520dfeed2db4c4 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 3 Feb 2025 14:00:49 -0300 Subject: [PATCH 06/46] Added CHANGES.txt and LICENSE files --- .github/workflows/update-license-year.yml | 45 +++++ CHANGES.txt | 7 + LICENSE | 208 ++-------------------- 3 files changed, 62 insertions(+), 198 deletions(-) create mode 100644 .github/workflows/update-license-year.yml create mode 100644 CHANGES.txt diff --git a/.github/workflows/update-license-year.yml b/.github/workflows/update-license-year.yml new file mode 100644 index 0000000..199a0ef --- /dev/null +++ b/.github/workflows/update-license-year.yml @@ -0,0 +1,45 @@ +name: Update License Year + +on: + schedule: + - cron: "0 3 1 1 *" # 03:00 AM on January 1 + +permissions: + contents: write + pull-requests: write + +jobs: + test: + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set Current year + run: "echo CURRENT=$(date +%Y) >> $GITHUB_ENV" + + - name: Set Previous Year + run: "echo PREVIOUS=$(($CURRENT-1)) >> $GITHUB_ENV" + + - name: Update LICENSE + uses: jacobtomlinson/gha-find-replace@v3 + with: + find: ${{ env.PREVIOUS }} + replace: ${{ env.CURRENT }} + include: "LICENSE" + regex: false + + - name: Commit files + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git commit -m "Updated License Year" -a + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + title: Update License Year + branch: update-license diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100644 index 0000000..51273e6 --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,7 @@ +0.1.0 (January 27, 2025): + - BREAKING CHANGES: + - Renamed `Split.Socket.Supervisor` module to `Split.Supervisor`, and updated the project structure to use a Context which is more in line to how Elixir libraries are structured. + - Refactored the options passed to the Split.Supervisor.start_link function to use Keywords instead of Maps to be more in line with other Elixir libraries and common practices (See https://github.com/splitio/elixir-thin-client/pull/17). + +0.0.0 (January 21, 2025): + - Initial public release. \ No newline at end of file diff --git a/LICENSE b/LICENSE index 261eeb9..df08de3 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,13 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Copyright © 2025 Split Software, Inc. - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - 1. Definitions. + http://www.apache.org/licenses/LICENSE-2.0 - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. From 46df7ed66ce9b2127e7193e7bd81510ba3b4bc9d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 3 Feb 2025 14:17:03 -0300 Subject: [PATCH 07/46] Add CONTRIBUTORS-GUIDE.md file --- .github/pull_request_template.md | 7 +++++++ CONTRIBUTORS-GUIDE.md | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 .github/pull_request_template.md create mode 100644 CONTRIBUTORS-GUIDE.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..0529a4b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,7 @@ +# Elixir thin client SDK + +## What did you accomplish? + +## How do we test the changes introduced in this PR? + +## Extra Notes diff --git a/CONTRIBUTORS-GUIDE.md b/CONTRIBUTORS-GUIDE.md new file mode 100644 index 0000000..b5a4d55 --- /dev/null +++ b/CONTRIBUTORS-GUIDE.md @@ -0,0 +1,22 @@ +# Contributing to the Split Elixir thin client SDK + +Split SDK is an open source project and we welcome feedback and contribution. The information below describes how to build the project with your changes, run the tests, and send the Pull Request(PR). + +## Development process + +1. Fork the repository and create a topic branch from `development` branch. Please use a descriptive name for your branch. +2. Run `mix deps.get` to have the dependencies up to date. +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 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. + +# Contact + +If you have any other questions or need to contact us directly in a private manner send us a note at sdks@split.io From 4d48e0fe0756fe61f082cada55ba8c3c19544bc1 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 4 Feb 2025 13:24:24 -0300 Subject: [PATCH 08/46] Update README.md following the format of our public SDK repositories --- CHANGES.txt | 2 +- README.md | 99 ++++++++++++++++++++++++++++++++--------------------- 2 files changed, 61 insertions(+), 40 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 51273e6..7185505 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,4 +4,4 @@ - Refactored the options passed to the Split.Supervisor.start_link function to use Keywords instead of Maps to be more in line with other Elixir libraries and common practices (See https://github.com/splitio/elixir-thin-client/pull/17). 0.0.0 (January 21, 2025): - - Initial public release. \ No newline at end of file + - Initial public release. diff --git a/README.md b/README.md index 917aa07..8d0fb3b 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,93 @@ -# SplitThinElixir +# Split SDK for Elixir -## Getting Started +[![hex.pm version](https://img.shields.io/hexpm/v/split_thin_sdk)](https://img.shields.io/hexpm/v/split_thin_sdk) [![Build Status](https://github.com/splitio/elixir-thin-client/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/splitio/elixir-thin-client/actions/workflows/ci-cd.yml) [![Greenkeeper badge](https://badges.greenkeeper.io/splitio/elixir-thin-client.svg)](https://greenkeeper.io/) -A step-by-step guide on how to integrate the Split.io thin client for Elixir into your app. +## Overview +This SDK is designed to work with Split, the platform for controlled rollouts, which serves features to your users via feature flags to manage your complete customer experience. + +[![Twitter Follow](https://img.shields.io/twitter/follow/splitsoftware.svg?style=social&label=Follow&maxAge=1529000)](https://twitter.com/intent/follow?screen_name=splitsoftware) + +## Compatibility + +The Elixir Thin Client SDK is compatible with Elixir @TODO and later. + +## Getting started ### Installing from Hex.pm -The Split Elixir thin client is publisehd as a package in hex.pm. It can be installed +The Split Elixir thin client is published as a package in hex.pm. It can be installed by adding `split_thin_elixir` to your list of dependencies in `mix.exs`: ```elixir def deps do [ - {:split_thin_elixir, "~> 0.1.0"} + {:split, "~> 0.1.0", hex: :split_thin_sdk} ] end ``` After adding the dependency, run `mix deps.get` to fetch the new dependency. -### Usage - -In order to use the Split Thin Client, you must start the [Split Daemon (splitd)](https://help.split.io/hc/en-us/articles/18305269686157-Split-Daemon-splitd). +### Using the SDK -Then you can start the Elixir Split Thin Client, either in your supervision tree: +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. ```elixir -children = [ - {Split, opts} -] +# Start the SDK supervisor +Split.Supervisor.start_link(socket_path: "/var/run/splitd.sock") + +# Get treatment for a user +case Split.get_treatment(user_id, feature_flag_name) do + "on" -> + # Feature flag is enabled for this user + "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. +end ``` -Or by starting it manually: +Please refer to [our official docs](https://help.split.io/hc/en-us/articles/@TODO-Elixir-Thin-Client-SDK) to learn about all the functionality provided by our SDK and the configuration options available for tailoring it to your current application setup. -```elixir -Split.Supervisor.start_link(opts) -``` - -Where `opts` is a keyword list with the following options: +## Submitting issues -- `:socket_path`: **REQUIRED** The path to the splitd socket file. For example `/var/run/splitd.sock`. -- `:fallback_enabled`: **OPTIONAL** A boolean that indicates wether we should return errors when RPC communication fails or falling back to a default value . Default is `false`. -- `:pool_size`: **OPTIONAL** The size of the pool of connections to the splitd daemon. Default is the number of online schedulers in the Erlang VM (See: https://www.erlang.org/doc/apps/erts/erl_cmd.html). -- `:connect_timeout`: **OPTIONAL** The timeout in milliseconds to connect to the splitd daemon. Default is `1000`. +The Split team monitors all issues submitted to this [issue tracker](https://github.com/splitio/elixir-thin-client/issues). We encourage you to use this issue tracker to submit any bug reports, feedback, and feature enhancements. We'll do our best to respond in a timely manner. -Once you have started Split, you are ready to start interacting with the Split.io splitd's daemon. +## Contributing +Please see [Contributors Guide](CONTRIBUTORS-GUIDE.md) to find all you need to submit a Pull Request (PR). -## Testing +## License +Licensed under the Apache License, Version 2.0. See: [Apache License](http://www.apache.org/licenses/). -### Running splitd for integration testing +## About Split -There is a convenience makefile target to run `splitd` for integration testing. This is useful to test the client against a real split server. You will need to export the `SPLIT_API_KEY` environment variable exported in your shell to run splitd: +Split is the leading Feature Delivery Platform for engineering teams that want to confidently deploy features as fast as they can develop them. Split’s fine-grained management, real-time monitoring, and data-driven experimentation ensure that new features will improve the customer experience without breaking or degrading performance. Companies like Twilio, Salesforce, GoDaddy and WePay trust Split to power their feature delivery. -```sh -export SPLIT_API_KEY=your-api-key -make start_splitd -``` +To learn more about Split, contact hello@split.io, or get started with feature flags for free at https://www.split.io/signup. -### Running tests +Split has built and maintains SDKs for: -To run the tests, you can use the following command: +* .NET [Github](https://github.com/splitio/dotnet-client) [Docs](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK) +* Android [Github](https://github.com/splitio/android-client) [Docs](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK) +* Angular [Github](https://github.com/splitio/angular-sdk-plugin) [Docs](https://help.split.io/hc/en-us/articles/6495326064397-Angular-utilities) +* Elixir thin-client [Github](https://github.com/splitio/elixir-thin-client) [Docs](https://help.split.io/hc/en-us/articles/@TODO-Elixir-Thin-Client-SDK) +* Flutter [Github](https://github.com/splitio/flutter-sdk-plugin) [Docs](https://help.split.io/hc/en-us/articles/8096158017165-Flutter-plugin) +* GO [Github](https://github.com/splitio/go-client) [Docs](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK) +* iOS [Github](https://github.com/splitio/ios-client) [Docs](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK) +* Java [Github](https://github.com/splitio/java-client) [Docs](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK) +* JavaScript [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK) +* JavaScript for Browser [Github](https://github.com/splitio/javascript-browser-client) [Docs](https://help.split.io/hc/en-us/articles/360058730852-Browser-SDK) +* Node.js [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK) +* PHP [Github](https://github.com/splitio/php-client) [Docs](https://help.split.io/hc/en-us/articles/360020350372-PHP-SDK) +* PHP thin-client [Github](https://github.com/splitio/php-thin-client) [Docs](https://help.split.io/hc/en-us/articles/18305128673933-PHP-Thin-Client-SDK) +* Python [Github](https://github.com/splitio/python-client) [Docs](https://help.split.io/hc/en-us/articles/360020359652-Python-SDK) +* React [Github](https://github.com/splitio/react-client) [Docs](https://help.split.io/hc/en-us/articles/360038825091-React-SDK) +* React Native [Github](https://github.com/splitio/react-native-client) [Docs](https://help.split.io/hc/en-us/articles/4406066357901-React-Native-SDK) +* Redux [Github](https://github.com/splitio/redux-client) [Docs](https://help.split.io/hc/en-us/articles/360038851551-Redux-SDK) +* Ruby [Github](https://github.com/splitio/ruby-client) [Docs](https://help.split.io/hc/en-us/articles/360020673251-Ruby-SDK) -```sh -mix test -``` +For a comprehensive list of open source projects visit our [Github page](https://github.com/splitio?utf8=%E2%9C%93&query=%20only%3Apublic%20). -Or if you want to use TDD fashion with [fswatch](https://github.com/emcrisostomo/fswatch) when test files change: +**Learn more about Split:** -```sh -fswatch lib test | mix test --listen-on-stdin -``` +Visit [split.io/product](https://www.split.io/product) for an overview of Split, or visit our documentation at [help.split.io](https://help.split.io) for more detailed information. From 4e00f9402355cf854e0eba2a5284b22fcf6136cc Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 4 Feb 2025 19:18:45 -0300 Subject: [PATCH 09/46] Add Split.split_key type and update the spec of Split track and getTreatment functions to accept an split_key value as 1st argument --- lib/split.ex | 25 ++++++++++++------------- lib/split/rpc/message.ex | 35 +++++++++++++++++------------------ 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/lib/split.ex b/lib/split.ex index 28ae9ad..b3b050d 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -76,6 +76,8 @@ defmodule Split do @type options :: [option()] + @type split_key :: String.t() | {:matching_key, String.t(), :bucketing_key, String.t() | nil} + defstruct [ :name, :traffic_type, @@ -97,63 +99,60 @@ defmodule Split do @spec child_spec(options()) :: Supervisor.child_spec() defdelegate child_spec(options), to: Split.Supervisor - @spec get_treatment(String.t(), String.t(), String.t() | nil, map() | nil) :: + @spec get_treatment(split_key(), String.t(), map() | nil) :: {:ok, Treatment.t()} | {:error, term()} - def get_treatment(user_key, feature_name, bucketing_key \\ nil, attributes \\ %{}) do + def get_treatment(user_key, feature_name, attributes \\ %{}) do request = Message.get_treatment( user_key: user_key, feature_name: feature_name, - bucketing_key: bucketing_key, attributes: attributes ) execute_rpc(request) end - @spec get_treatment_with_config(String.t(), String.t(), String.t() | nil, map() | nil) :: + @spec get_treatment_with_config(split_key(), String.t(), map() | nil) :: {:ok, Treatment.t()} | {:error, term()} - def get_treatment_with_config(user_key, feature_name, bucketing_key \\ nil, attributes \\ %{}) do + def get_treatment_with_config(user_key, feature_name, attributes \\ %{}) do request = Message.get_treatment_with_config( user_key: user_key, feature_name: feature_name, - bucketing_key: bucketing_key, attributes: attributes ) execute_rpc(request) end - @spec get_treatments(String.t(), [String.t()], String.t() | nil, map() | nil) :: + @spec get_treatments(split_key(), [String.t()], map() | nil) :: {:ok, %{String.t() => Treatment.t()}} | {:error, term()} - def get_treatments(user_key, feature_names, bucketing_key \\ nil, attributes \\ %{}) do + def get_treatments(user_key, feature_names, attributes \\ %{}) do request = Message.get_treatments( user_key: user_key, feature_names: feature_names, - bucketing_key: bucketing_key, attributes: attributes ) execute_rpc(request) end - @spec get_treatments_with_config(String.t(), [String.t()], String.t() | nil, map() | nil) :: + @spec get_treatments_with_config(split_key(), [String.t()], map() | nil) :: {:ok, %{String.t() => Treatment.t()}} | {:error, term()} - def get_treatments_with_config(user_key, feature_names, bucketing_key \\ nil, attributes \\ %{}) do + def get_treatments_with_config(user_key, feature_names, attributes \\ %{}) do request = Message.get_treatments_with_config( user_key: user_key, feature_names: feature_names, - bucketing_key: bucketing_key, attributes: attributes ) execute_rpc(request) end - @spec track(String.t(), String.t(), String.t(), term(), map()) :: :ok | {:error, term()} + @spec track(split_key(), String.t(), String.t(), number() | nil, map() | nil) :: + :ok | {:error, term()} def track(user_key, traffic_type, event_type, value \\ nil, properties \\ %{}) do request = Message.track(user_key, traffic_type, event_type, value, properties) execute_rpc(request) diff --git a/lib/split/rpc/message.ex b/lib/split/rpc/message.ex index 434cf6a..086a46d 100644 --- a/lib/split/rpc/message.ex +++ b/lib/split/rpc/message.ex @@ -22,15 +22,13 @@ defmodule Split.RPC.Message do } @type get_treatment_args :: - {:user_key, String.t()} + {:user_key, Split.split_key()} | {:feature_name, String.t()} - | {:bucketing_key, String.t() | nil} | {:attributes, map() | nil} @type get_treatments_args :: - {:user_key, String.t()} + {:user_key, Split.split_key()} | {:feature_names, list(String.t())} - | {:bucketing_key, String.t() | nil} | {:attributes, map() | nil} @doc """ @@ -50,9 +48,8 @@ defmodule Split.RPC.Message do ## Examples iex> Message.get_treatment( - ...> user_key: "user_key", - ...> feature_name: "feature_name", - ...> bucketing_key: "bucketing_key" + ...> user_key: %{:matching_key => "user_key", :bucketing_key => "bucketing_key"}, + ...> feature_name: "feature_name" ...> ) %Message{a: ["user_key", "bucketing_key", "feature_name", %{}], o: 17, v: 1} @@ -70,9 +67,8 @@ defmodule Split.RPC.Message do ## Examples iex> Message.get_treatment_with_config( - ...> user_key: "user_key", - ...> feature_name: "feature_name", - ...> bucketing_key: "bucketing_key" + ...> user_key: %{:matching_key => "user_key", :bucketing_key => "bucketing_key"}, + ...> feature_name: "feature_name" ...> ) %Message{a: ["user_key", "bucketing_key", "feature_name", %{}], o: 19, v: 1} @@ -93,9 +89,8 @@ defmodule Split.RPC.Message do ## Examples iex> Message.get_treatments( - ...> user_key: "user_key", - ...> feature_names: ["feature_name1", "feature_name2"], - ...> bucketing_key: "bucketing_key" + ...> user_key: %{:matching_key => "user_key", :bucketing_key => "bucketing_key"}, + ...> feature_names: ["feature_name1", "feature_name2"] ...> ) %Message{ a: ["user_key", "bucketing_key", ["feature_name1", "feature_name2"], %{}], @@ -120,9 +115,8 @@ defmodule Split.RPC.Message do ## Examples iex> Message.get_treatments_with_config( - ...> user_key: "user_key", - ...> feature_names: ["feature_name1", "feature_name2"], - ...> bucketing_key: "bucketing_key" + ...> user_key: %{:matching_key => "user_key", :bucketing_key => "bucketing_key"}, + ...> feature_names: ["feature_name1", "feature_name2"] ...> ) %Message{ a: ["user_key", "bucketing_key", ["feature_name1", "feature_name2"], %{}], @@ -245,14 +239,19 @@ defmodule Split.RPC.Message do if Keyword.get(opts, :multiple, false), do: :feature_names, else: :feature_name user_key = Keyword.fetch!(data, :user_key) + {matching_key, bucketing_key} = + if is_map(user_key) do + {user_key.matching_key, user_key.bucketing_key} + else + {user_key, nil} + end feature_name = Keyword.fetch!(data, features_key) - bucketing_key = Keyword.get(data, :bucketing_key, nil) attributes = Keyword.get(data, :attributes, %{}) %__MODULE__{ o: opcode, a: [ - user_key, + matching_key, bucketing_key, feature_name, attributes From 2f2794f1d65bf38ec531b64102cfa060cbc6f548 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 4 Feb 2025 19:19:19 -0300 Subject: [PATCH 10/46] Rename argument user_key to key --- lib/split.ex | 20 ++++++++++---------- lib/split/rpc/message.ex | 32 ++++++++++++++++---------------- lib/split/rpc/response_parser.ex | 8 ++++---- lib/split/telemetry.ex | 4 ++-- test/sockets/conn_test.exs | 6 +++--- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/lib/split.ex b/lib/split.ex index b3b050d..7688c42 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -101,10 +101,10 @@ defmodule Split do @spec get_treatment(split_key(), String.t(), map() | nil) :: {:ok, Treatment.t()} | {:error, term()} - def get_treatment(user_key, feature_name, attributes \\ %{}) do + def get_treatment(key, feature_name, attributes \\ %{}) do request = Message.get_treatment( - user_key: user_key, + key: key, feature_name: feature_name, attributes: attributes ) @@ -114,10 +114,10 @@ defmodule Split do @spec get_treatment_with_config(split_key(), String.t(), map() | nil) :: {:ok, Treatment.t()} | {:error, term()} - def get_treatment_with_config(user_key, feature_name, attributes \\ %{}) do + def get_treatment_with_config(key, feature_name, attributes \\ %{}) do request = Message.get_treatment_with_config( - user_key: user_key, + key: key, feature_name: feature_name, attributes: attributes ) @@ -127,10 +127,10 @@ defmodule Split do @spec get_treatments(split_key(), [String.t()], map() | nil) :: {:ok, %{String.t() => Treatment.t()}} | {:error, term()} - def get_treatments(user_key, feature_names, attributes \\ %{}) do + def get_treatments(key, feature_names, attributes \\ %{}) do request = Message.get_treatments( - user_key: user_key, + key: key, feature_names: feature_names, attributes: attributes ) @@ -140,10 +140,10 @@ defmodule Split do @spec get_treatments_with_config(split_key(), [String.t()], map() | nil) :: {:ok, %{String.t() => Treatment.t()}} | {:error, term()} - def get_treatments_with_config(user_key, feature_names, attributes \\ %{}) do + def get_treatments_with_config(key, feature_names, attributes \\ %{}) do request = Message.get_treatments_with_config( - user_key: user_key, + key: key, feature_names: feature_names, attributes: attributes ) @@ -153,8 +153,8 @@ defmodule Split do @spec track(split_key(), String.t(), String.t(), number() | nil, map() | nil) :: :ok | {:error, term()} - def track(user_key, traffic_type, event_type, value \\ nil, properties \\ %{}) do - request = Message.track(user_key, traffic_type, event_type, value, properties) + 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 diff --git a/lib/split/rpc/message.ex b/lib/split/rpc/message.ex index 086a46d..043545e 100644 --- a/lib/split/rpc/message.ex +++ b/lib/split/rpc/message.ex @@ -22,12 +22,12 @@ defmodule Split.RPC.Message do } @type get_treatment_args :: - {:user_key, Split.split_key()} + {:key, Split.split_key()} | {:feature_name, String.t()} | {:attributes, map() | nil} @type get_treatments_args :: - {:user_key, Split.split_key()} + {:key, Split.split_key()} | {:feature_names, list(String.t())} | {:attributes, map() | nil} @@ -48,12 +48,12 @@ defmodule Split.RPC.Message do ## Examples iex> Message.get_treatment( - ...> user_key: %{:matching_key => "user_key", :bucketing_key => "bucketing_key"}, + ...> key: %{:matching_key => "user_key", :bucketing_key => "bucketing_key"}, ...> feature_name: "feature_name" ...> ) %Message{a: ["user_key", "bucketing_key", "feature_name", %{}], o: 17, v: 1} - iex> Message.get_treatment(user_key: "user_key", feature_name: "feature_name") + iex> Message.get_treatment(key: "user_key", feature_name: "feature_name") %Message{a: ["user_key", nil, "feature_name", %{}], o: 17, v: 1} """ @spec get_treatment([get_treatment_args()]) :: t() @@ -67,13 +67,13 @@ defmodule Split.RPC.Message do ## Examples iex> Message.get_treatment_with_config( - ...> user_key: %{:matching_key => "user_key", :bucketing_key => "bucketing_key"}, + ...> key: %{:matching_key => "user_key", :bucketing_key => "bucketing_key"}, ...> feature_name: "feature_name" ...> ) %Message{a: ["user_key", "bucketing_key", "feature_name", %{}], o: 19, v: 1} iex> Message.get_treatment_with_config( - ...> user_key: "user_key", + ...> key: "user_key", ...> feature_name: "feature_name" ...> ) %Message{a: ["user_key", nil, "feature_name", %{}], o: 19, v: 1} @@ -89,7 +89,7 @@ defmodule Split.RPC.Message do ## Examples iex> Message.get_treatments( - ...> user_key: %{:matching_key => "user_key", :bucketing_key => "bucketing_key"}, + ...> key: %{:matching_key => "user_key", :bucketing_key => "bucketing_key"}, ...> feature_names: ["feature_name1", "feature_name2"] ...> ) %Message{ @@ -99,7 +99,7 @@ defmodule Split.RPC.Message do } iex> Message.get_treatments( - ...> user_key: "user_key", + ...> key: "user_key", ...> feature_names: ["feature_name1", "feature_name2"] ...> ) %Message{a: ["user_key", nil, ["feature_name1", "feature_name2"], %{}], o: 18, v: 1} @@ -115,7 +115,7 @@ defmodule Split.RPC.Message do ## Examples iex> Message.get_treatments_with_config( - ...> user_key: %{:matching_key => "user_key", :bucketing_key => "bucketing_key"}, + ...> key: %{:matching_key => "user_key", :bucketing_key => "bucketing_key"}, ...> feature_names: ["feature_name1", "feature_name2"] ...> ) %Message{ @@ -125,7 +125,7 @@ defmodule Split.RPC.Message do } iex> Message.get_treatments_with_config( - ...> user_key: "user_key", + ...> key: "user_key", ...> feature_names: ["feature_name1", "feature_name2"] ...> ) %Message{ @@ -188,10 +188,10 @@ defmodule Split.RPC.Message do %Message{v: 1, o: 128, a: ["user_key", "traffic_type", "my_event", nil, %{}]} """ @spec track(String.t(), String.t(), String.t(), any(), map()) :: t() - def track(user_key, traffic_type, event_type, value \\ nil, properties \\ %{}) do + def track(key, traffic_type, event_type, value \\ nil, properties \\ %{}) do %__MODULE__{ o: @track_opcode, - a: [user_key, traffic_type, event_type, value, properties] + a: [key, traffic_type, event_type, value, properties] } end @@ -238,12 +238,12 @@ defmodule Split.RPC.Message do features_key = if Keyword.get(opts, :multiple, false), do: :feature_names, else: :feature_name - user_key = Keyword.fetch!(data, :user_key) + key = Keyword.fetch!(data, :key) {matching_key, bucketing_key} = - if is_map(user_key) do - {user_key.matching_key, user_key.bucketing_key} + if is_map(key) do + {key.matching_key, key.bucketing_key} else - {user_key, nil} + {key, nil} end feature_name = Keyword.fetch!(data, features_key) attributes = Keyword.get(data, :attributes, %{}) diff --git a/lib/split/rpc/response_parser.ex b/lib/split/rpc/response_parser.ex index 78422c9..110fb46 100644 --- a/lib/split/rpc/response_parser.ex +++ b/lib/split/rpc/response_parser.ex @@ -34,10 +34,10 @@ defmodule Split.RPC.ResponseParser do ) when opcode in [@get_treatment_opcode, @get_treatment_with_config_opcode] do treatment = Treatment.build_from_daemon_response(treatment_data) - user_key = Enum.at(args, 0) + key = Enum.at(args, 0) feature_name = Enum.at(args, 2) - Telemetry.send_impression(user_key, feature_name, treatment) + Telemetry.send_impression(key, feature_name, treatment) {:ok, treatment} end @@ -51,12 +51,12 @@ defmodule Split.RPC.ResponseParser do ) when opcode in [@get_treatments_opcode, @get_treatments_with_config_opcode] do treatments = Enum.map(treatments, &Treatment.build_from_daemon_response/1) - user_key = Enum.at(args, 0) + key = Enum.at(args, 0) feature_names = Enum.at(args, 2) mapped_treatments = Enum.zip_reduce(feature_names, treatments, %{}, fn feature_name, treatment, acc -> - Telemetry.send_impression(user_key, feature_name, treatment) + Telemetry.send_impression(key, feature_name, treatment) Map.put(acc, feature_name, treatment) end) diff --git a/lib/split/telemetry.ex b/lib/split/telemetry.ex index 1b66770..74d9cbe 100644 --- a/lib/split/telemetry.ex +++ b/lib/split/telemetry.ex @@ -301,10 +301,10 @@ defmodule Split.Telemetry do Emits a telemetry `impression` event when a Split treatment has been evaluated. """ @spec send_impression(String.t(), String.t(), Treatment.t()) :: :ok - def send_impression(user_key, feature_name, %Treatment{} = treatment) do + def send_impression(key, feature_name, %Treatment{} = treatment) do :telemetry.execute([@app_name, :impression], %{}, %{ impression: %Split.Impression{ - key: user_key, + key: key, feature: feature_name, treatment: treatment.treatment, label: treatment.label, diff --git a/test/sockets/conn_test.exs b/test/sockets/conn_test.exs index fbb7a1a..9546132 100644 --- a/test/sockets/conn_test.exs +++ b/test/sockets/conn_test.exs @@ -87,7 +87,7 @@ defmodule Split.Sockets.ConnTest do {:ok, conn} = Conn.new(socket_path) |> Conn.connect() - message = Message.get_treatment(user_key: "user-id", feature_name: "feature") + message = Message.get_treatment(key: "user-id", feature_name: "feature") {:ok, _conn, response} = Conn.send_message(conn, message) @@ -108,7 +108,7 @@ defmodule Split.Sockets.ConnTest do {:ok, conn} = Conn.new(socket_path) |> Conn.connect() - message = Message.get_treatment(user_key: "user-id", feature_name: "feature") + message = Message.get_treatment(key: "user-id", feature_name: "feature") # Stop the mocked splitd socket to receive connection errors :ok = stop_supervised(splitd_name) @@ -129,7 +129,7 @@ defmodule Split.Sockets.ConnTest do {:ok, conn} = Conn.new(socket_path) |> Conn.connect() - message = Message.get_treatment(user_key: "user-id", feature_name: "feature") + message = Message.get_treatment(key: "user-id", feature_name: "feature") assert {:ok, _conn, response} = Conn.send_message(conn, message) From 5a2b9d30c763e9eaf83fed466f2798c057a13831 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 4 Feb 2025 19:27:39 -0300 Subject: [PATCH 11/46] Update changelog entry --- CHANGES.txt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 7185505..92b8e4b 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,7 +1,11 @@ +0.2.0 (February XX, 2025): + - BREAKING CHANGES: + - Updated the signature of all `get_treatment` functions: removed the 3rd argument (`bucketing_key`), and extended the first argument (`key`) to accept a union, allowing a string or a map with the key and optional bucketing key (`{:matching_key, String.t(), :bucketing_key, String.t() | nil}`). + 0.1.0 (January 27, 2025): - BREAKING CHANGES: - - Renamed `Split.Socket.Supervisor` module to `Split.Supervisor`, and updated the project structure to use a Context which is more in line to how Elixir libraries are structured. - - Refactored the options passed to the Split.Supervisor.start_link function to use Keywords instead of Maps to be more in line with other Elixir libraries and common practices (See https://github.com/splitio/elixir-thin-client/pull/17). + - Renamed `Split.Socket.Supervisor` module to `Split.Supervisor`, and updated the project structure to use a Context which is more in line to how Elixir libraries are structured (By @codeadict in https://github.com/splitio/elixir-thin-client/pull/17). + - Refactored the options passed to the Split.Supervisor.start_link function to use Keywords instead of Maps to be more in line with other Elixir libraries and common practices (By @codeadict in https://github.com/splitio/elixir-thin-client/pull/17). 0.0.0 (January 21, 2025): - Initial public release. From 12bb8f5016c848d8ec367a7860c270f6fa93f085 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 Feb 2025 11:12:46 -0300 Subject: [PATCH 12/46] Update split/1 spec to allow nil return value --- lib/split.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/split.ex b/lib/split.ex index 089a42e..579cc91 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -168,7 +168,7 @@ defmodule Split do execute_rpc(request) end - @spec split(String.t()) :: {:ok, Split.t()} | {:error, term()} + @spec split(String.t()) :: {:ok, Split.t() | nil} | {:error, term()} def split(name) do request = Message.split(name) From 624175b6bd97df25e9113495fb840ef037230d57 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 Feb 2025 16:11:54 -0300 Subject: [PATCH 13/46] Remove fallback_enabled option and error tuple from return values --- lib/split.ex | 35 ++-- lib/split/rpc/fallback.ex | 42 ++--- lib/split/rpc/response_parser.ex | 47 ++--- lib/split/sockets/pool.ex | 4 - test/rpc/response_parser_test.exs | 303 ++++++++++++++---------------- test/split_test.exs | 18 +- 6 files changed, 204 insertions(+), 245 deletions(-) diff --git a/lib/split.ex b/lib/split.ex index 28ae9ad..bd11283 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -17,7 +17,7 @@ defmodule Split do def start(_type, _args) do children = [ # ... other children ... - {Split, [socket_path: "/var/run/split.sock", fallback_enabled: true]} + {Split, [socket_path: "/var/run/split.sock"]} ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] @@ -37,7 +37,6 @@ defmodule Split do `Split` takes a number of keyword arguments as options when starting. The following options are available: - `:socket_path`: **REQUIRED** The path to the splitd socket file. For example `/var/run/splitd.sock`. - - `:fallback_enabled`: **OPTIONAL** A boolean that indicates wether we should return errors when RPC communication fails or falling back to a default value . Default is `false`. - `:pool_size`: **OPTIONAL** The size of the pool of connections to the splitd daemon. Default is the number of online schedulers in the Erlang VM (See: https://www.erlang.org/doc/apps/erts/erl_cmd.html). - `:connect_timeout`: **OPTIONAL** The timeout in milliseconds to connect to the splitd daemon. Default is `1000`. @@ -70,7 +69,6 @@ defmodule Split do @typedoc "An option that can be provided when starting `Split`." @type option :: {:socket_path, String.t()} - | {:fallback_enabled, boolean()} | {:pool_size, non_neg_integer()} | {:connect_timeout, non_neg_integer()} @@ -97,8 +95,7 @@ defmodule Split do @spec child_spec(options()) :: Supervisor.child_spec() defdelegate child_spec(options), to: Split.Supervisor - @spec get_treatment(String.t(), String.t(), String.t() | nil, map() | nil) :: - {:ok, Treatment.t()} | {:error, term()} + @spec get_treatment(String.t(), String.t(), String.t() | nil, map() | nil) :: Treatment.t() def get_treatment(user_key, feature_name, bucketing_key \\ nil, attributes \\ %{}) do request = Message.get_treatment( @@ -112,7 +109,7 @@ defmodule Split do end @spec get_treatment_with_config(String.t(), String.t(), String.t() | nil, map() | nil) :: - {:ok, Treatment.t()} | {:error, term()} + Treatment.t() def get_treatment_with_config(user_key, feature_name, bucketing_key \\ nil, attributes \\ %{}) do request = Message.get_treatment_with_config( @@ -125,8 +122,9 @@ defmodule Split do execute_rpc(request) end - @spec get_treatments(String.t(), [String.t()], String.t() | nil, map() | nil) :: - {:ok, %{String.t() => Treatment.t()}} | {:error, term()} + @spec get_treatments(String.t(), [String.t()], String.t() | nil, map() | nil) :: %{ + String.t() => Treatment.t() + } def get_treatments(user_key, feature_names, bucketing_key \\ nil, attributes \\ %{}) do request = Message.get_treatments( @@ -139,8 +137,9 @@ defmodule Split do execute_rpc(request) end - @spec get_treatments_with_config(String.t(), [String.t()], String.t() | nil, map() | nil) :: - {:ok, %{String.t() => Treatment.t()}} | {:error, term()} + @spec get_treatments_with_config(String.t(), [String.t()], String.t() | nil, map() | nil) :: %{ + String.t() => Treatment.t() + } def get_treatments_with_config(user_key, feature_names, bucketing_key \\ nil, attributes \\ %{}) do request = Message.get_treatments_with_config( @@ -153,26 +152,26 @@ defmodule Split do execute_rpc(request) end - @spec track(String.t(), String.t(), String.t(), term(), map()) :: :ok | {:error, term()} + @spec track(String.t(), String.t(), String.t(), term(), map()) :: boolean() def track(user_key, traffic_type, event_type, value \\ nil, properties \\ %{}) do request = Message.track(user_key, traffic_type, event_type, value, properties) execute_rpc(request) end - @spec split_names() :: {:ok, %{split_names: String.t()}} | {:error, term()} + @spec split_names() :: %{split_names: String.t()} def split_names do request = Message.split_names() execute_rpc(request) end - @spec split(String.t()) :: {:ok, Split.t()} | {:error, term()} + @spec split(String.t()) :: Split.t() def split(name) do request = Message.split(name) execute_rpc(request) end - @spec splits() :: {:ok, [Split.t()]} | {:error, term()} + @spec splits() :: [Split.t()] def splits do request = Message.splits() execute_rpc(request) @@ -191,14 +190,8 @@ defmodule Split do |> Pool.send_message(opts) |> ResponseParser.parse_response(request, span_context: telemetry_span_context) |> case do - :ok -> - {:ok, %{}} - - {:ok, data} = response -> + data = response -> {response, %{response: data}} - - {:error, reason} = error -> - {error, %{error: reason}} end end) end diff --git a/lib/split/rpc/fallback.ex b/lib/split/rpc/fallback.ex index aa9b083..aeeca19 100644 --- a/lib/split/rpc/fallback.ex +++ b/lib/split/rpc/fallback.ex @@ -2,8 +2,7 @@ defmodule Split.RPC.Fallback do @moduledoc """ This module is used to provide default values for all Splitd RPC calls. - When a call to Splitd fails, and the SDK was initialized with `fallback_enabled`, - the fallback values are returned instead of the error received from the socket. + When a call to Splitd fails, the fallback values are returned instead of the error received from the socket. """ use Split.RPC.Opcodes @@ -16,40 +15,39 @@ defmodule Split.RPC.Fallback do ## Examples iex> Fallback.fallback(%Message{o: 0x11}) - {:ok, %Treatment{treatment: "control", label: "fallback treatment"}} + %Treatment{treatment: "control", label: "exception"} iex> Fallback.fallback(%Message{o: 0x13}) - {:ok, %Treatment{treatment: "control", label: "fallback treatment", config: nil}} + %Treatment{treatment: "control", label: "exception", config: nil} iex> Fallback.fallback(%Message{ ...> o: 0x12, ...> a: ["user_key", "bucketing_key", ["feature_1", "feature_2"], %{}] ...> }) - {:ok, - %{ - "feature_1" => %Treatment{treatment: "control", label: "fallback treatment"}, - "feature_2" => %Treatment{treatment: "control", label: "fallback treatment"} - }} + %{ + "feature_1" => %Treatment{treatment: "control", label: "exception"}, + "feature_2" => %Treatment{treatment: "control", label: "exception"} + } iex> Fallback.fallback(%Message{o: 0x14, a: ["user_key", "bucketing_key", ["feature_a"], %{}]}) - {:ok, %{"feature_a" => %Treatment{treatment: "control", label: "fallback treatment", config: nil}}} + %{"feature_a" => %Treatment{treatment: "control", label: "exception", config: nil}} iex> Fallback.fallback(%Message{o: 0xA1}) - {:ok, nil} + nil iex> Fallback.fallback(%Message{o: 0xA2}) - {:ok, []} + [] iex> Fallback.fallback(%Message{o: 0xA0}) - {:ok, %{split_names: []}} + %{split_names: []} iex> Fallback.fallback(%Message{o: 0x80}) - :ok + false """ - @spec fallback(Message.t()) :: {:ok, map() | Treatment.t(), list(), nil} | :ok + @spec fallback(Message.t()) :: map() | Treatment.t() | list() | boolean() | nil def fallback(%Message{o: opcode}) when opcode in [@get_treatment_opcode, @get_treatment_with_config_opcode] do - {:ok, %Treatment{label: "fallback treatment"}} + %Treatment{label: "exception"} end def fallback(%Message{o: opcode, a: args}) @@ -58,25 +56,25 @@ defmodule Split.RPC.Fallback do treatments = Enum.reduce(feature_names, %{}, fn feature_name, acc -> - Map.put(acc, feature_name, %Treatment{label: "fallback treatment"}) + Map.put(acc, feature_name, %Treatment{label: "exception"}) end) - {:ok, treatments} + treatments end def fallback(%Message{o: @split_opcode}) do - {:ok, nil} + nil end def fallback(%Message{o: @splits_opcode}) do - {:ok, []} + [] end def fallback(%Message{o: @split_names_opcode}) do - {:ok, %{split_names: []}} + %{split_names: []} end def fallback(%Message{o: @track_opcode}) do - :ok + false end end diff --git a/lib/split/rpc/response_parser.ex b/lib/split/rpc/response_parser.ex index 78422c9..2fc56af 100644 --- a/lib/split/rpc/response_parser.ex +++ b/lib/split/rpc/response_parser.ex @@ -17,11 +17,8 @@ defmodule Split.RPC.ResponseParser do """ @spec parse_response(response :: splitd_response(), request :: Message.t(), [ {:span_context, reference()} | {:span_context, nil} - ]) :: - :ok - | {:ok, map() | list() | Treatment.t() | Split.t() | nil} - | {:error, term()} - | :error + ]) :: map() | list() | Treatment.t() | Split.t() | boolean() | nil + def parse_response(response, original_request, opts \\ []) def parse_response( @@ -38,7 +35,7 @@ defmodule Split.RPC.ResponseParser do feature_name = Enum.at(args, 2) Telemetry.send_impression(user_key, feature_name, treatment) - {:ok, treatment} + treatment end def parse_response( @@ -60,7 +57,7 @@ defmodule Split.RPC.ResponseParser do Map.put(acc, feature_name, treatment) end) - {:ok, mapped_treatments} + mapped_treatments end def parse_response( @@ -68,7 +65,7 @@ defmodule Split.RPC.ResponseParser do %Message{o: @split_opcode}, _opts ) do - {:ok, parse_split(payload)} + parse_split(payload) end def parse_response( @@ -78,7 +75,7 @@ defmodule Split.RPC.ResponseParser do }, _opts ) do - {:ok, %{split_names: split_names}} + %{split_names: split_names} end def parse_response( @@ -93,7 +90,7 @@ defmodule Split.RPC.ResponseParser do [parse_split(split) | acc] end) - {:ok, splits} + splits end def parse_response( @@ -104,9 +101,9 @@ defmodule Split.RPC.ResponseParser do _opts ) do if tracked? do - :ok + true else - :error + false end end @@ -120,7 +117,7 @@ defmodule Split.RPC.ResponseParser do response: inspect(raw_response) ) - maybe_fallback({:error, :splitd_internal_error}, message, opts) + fallback(message, opts) end def parse_response({:ok, raw_response}, %Message{} = message, opts) do @@ -129,7 +126,7 @@ defmodule Split.RPC.ResponseParser do response: inspect(raw_response) ) - maybe_fallback({:error, :splitd_parse_error}, message, opts) + fallback(message, opts) end def parse_response({:error, reason}, request, opts) do @@ -138,23 +135,19 @@ defmodule Split.RPC.ResponseParser do reason: inspect(reason) ) - maybe_fallback({:error, reason}, request, opts) + fallback(request, opts) end - defp maybe_fallback(response, original_request, opts) do - if :persistent_term.get(:splitd_fallback_enabled, false) do - fallback_response = Fallback.fallback(original_request) - - if Keyword.has_key?(opts, :span_context) do - Telemetry.span_event([:rpc, :fallback], opts[:span_context], %{ - response: fallback_response - }) - end + defp fallback(original_request, opts) do + fallback_response = Fallback.fallback(original_request) - fallback_response - else - response + if Keyword.has_key?(opts, :span_context) do + Telemetry.span_event([:rpc, :fallback], opts[:span_context], %{ + response: fallback_response + }) end + + fallback_response end defp parse_split(payload) do diff --git a/lib/split/sockets/pool.ex b/lib/split/sockets/pool.ex index a3bb3c8..778d1c3 100644 --- a/lib/split/sockets/pool.ex +++ b/lib/split/sockets/pool.ex @@ -17,15 +17,11 @@ defmodule Split.Sockets.Pool do end def start_link(opts) do - fallback_enabled = Keyword.get(opts, :fallback_enabled, false) - :persistent_term.put(:splitd_fallback_enabled, fallback_enabled) - pool_name = Keyword.get(opts, :pool_name, __MODULE__) pool_size = Keyword.get(opts, :pool_size, System.schedulers_online()) opts = opts - |> Keyword.put_new(:fallback_enabled, fallback_enabled) |> Keyword.put_new(:pool_size, pool_size) |> Keyword.put_new(:pool_name, pool_name) diff --git a/test/rpc/response_parser_test.exs b/test/rpc/response_parser_test.exs index d15f261..618e2f3 100644 --- a/test/rpc/response_parser_test.exs +++ b/test/rpc/response_parser_test.exs @@ -24,14 +24,13 @@ defmodule Split.RPC.ResponseParserTest do }} assert ResponseParser.parse_response(response, message) == - {:ok, - %Treatment{ - change_number: 123, - config: nil, - label: "test label", - timestamp: 1_723_742_604, - treatment: "on" - }} + %Treatment{ + change_number: 123, + config: nil, + label: "test label", + timestamp: 1_723_742_604, + treatment: "on" + } end test "parses get_treatment_with_config RPC response" do @@ -52,14 +51,13 @@ defmodule Split.RPC.ResponseParserTest do }} assert ResponseParser.parse_response(response, message) == - {:ok, - %Treatment{ - change_number: 123, - config: "{\"foo\": \"bar\"}", - label: "test label", - timestamp: 1_723_742_604, - treatment: "on" - }} + %Treatment{ + change_number: 123, + config: "{\"foo\": \"bar\"}", + label: "test label", + timestamp: 1_723_742_604, + treatment: "on" + } end test "parses get_treatments RPC response" do @@ -81,23 +79,22 @@ defmodule Split.RPC.ResponseParserTest do }} assert ResponseParser.parse_response(response, message) == - {:ok, - %{ - "feature_name1" => %Split.Treatment{ - treatment: "on", - label: "test label 1", - config: nil, - change_number: 123, - timestamp: 1_723_742_604 - }, - "feature_name2" => %Split.Treatment{ - treatment: "off", - label: "test label 2", - config: nil, - change_number: 456, - timestamp: 1_723_742_604 - } - }} + %{ + "feature_name1" => %Split.Treatment{ + treatment: "on", + label: "test label 1", + config: nil, + change_number: 123, + timestamp: 1_723_742_604 + }, + "feature_name2" => %Split.Treatment{ + treatment: "off", + label: "test label 2", + config: nil, + change_number: 456, + timestamp: 1_723_742_604 + } + } end test "parses get_treatments_with_config RPC response" do @@ -127,23 +124,22 @@ defmodule Split.RPC.ResponseParserTest do }} assert ResponseParser.parse_response(response, message) == - {:ok, - %{ - "feature_name1" => %Split.Treatment{ - treatment: "on", - label: "test label 1", - config: "{\"foo\": \"bar\"}", - change_number: 123, - timestamp: 1_723_742_604 - }, - "feature_name2" => %Split.Treatment{ - treatment: "off", - label: "test label 2", - config: "{\"baz\": \"qux\"}", - change_number: 456, - timestamp: 1_723_742_604 - } - }} + %{ + "feature_name1" => %Split.Treatment{ + treatment: "on", + label: "test label 1", + config: "{\"foo\": \"bar\"}", + change_number: 123, + timestamp: 1_723_742_604 + }, + "feature_name2" => %Split.Treatment{ + treatment: "off", + label: "test label 2", + config: "{\"baz\": \"qux\"}", + change_number: 456, + timestamp: 1_723_742_604 + } + } end test "parses split RPC call" do @@ -170,17 +166,16 @@ defmodule Split.RPC.ResponseParserTest do }} assert ResponseParser.parse_response(response, message) == - {:ok, - %Split{ - name: "feature_name", - traffic_type: "user", - killed: false, - treatments: ["treatment_a", "treatment_b", "treatment_c"], - change_number: 1_499_375_079_065, - configurations: %{}, - default_treatment: "treatment_a", - flag_sets: [] - }} + %Split{ + name: "feature_name", + traffic_type: "user", + killed: false, + treatments: ["treatment_a", "treatment_b", "treatment_c"], + change_number: 1_499_375_079_065, + configurations: %{}, + default_treatment: "treatment_a", + flag_sets: [] + } end test "parses splits RPC call" do @@ -224,30 +219,29 @@ defmodule Split.RPC.ResponseParserTest do }} # Order of splits is not guaranteed - assert ResponseParser.parse_response(response, message) |> sorted_by(& &1.name) == - {:ok, - [ - %Split{ - name: "feature_a", - traffic_type: "user", - killed: false, - treatments: ["treatment_a", "treatment_b", "treatment_c"], - change_number: 1_499_375_079_065, - configurations: %{}, - default_treatment: "treatment_a", - flag_sets: [] - }, - %Split{ - name: "feature_b", - traffic_type: "user", - killed: false, - treatments: ["on", "off"], - change_number: 1_499_375_079_066, - configurations: %{}, - default_treatment: "off", - flag_sets: [] - } - ]} + assert ResponseParser.parse_response(response, message) |> Enum.sort_by(& &1.name) == + [ + %Split{ + name: "feature_a", + traffic_type: "user", + killed: false, + treatments: ["treatment_a", "treatment_b", "treatment_c"], + change_number: 1_499_375_079_065, + configurations: %{}, + default_treatment: "treatment_a", + flag_sets: [] + }, + %Split{ + name: "feature_b", + traffic_type: "user", + killed: false, + treatments: ["on", "off"], + change_number: 1_499_375_079_066, + configurations: %{}, + default_treatment: "off", + flag_sets: [] + } + ] end test "parses split_names RPC call" do @@ -266,7 +260,7 @@ defmodule Split.RPC.ResponseParserTest do }} assert ResponseParser.parse_response(response, message) == - {:ok, %{split_names: ["feature_a", "feature_b"]}} + %{split_names: ["feature_a", "feature_b"]} end test "parses successful track RPC call" do @@ -274,7 +268,7 @@ defmodule Split.RPC.ResponseParserTest do response = {:ok, %{"s" => 1, "p" => %{"s" => true}}} - assert ResponseParser.parse_response(response, message) == :ok + assert ResponseParser.parse_response(response, message) == true end test "parses failed track RPC call" do @@ -282,59 +276,53 @@ defmodule Split.RPC.ResponseParserTest do response = {:ok, %{"s" => 1, "p" => %{"s" => false}}} - assert ResponseParser.parse_response(response, message) == :error - end - - test "handles splitd internal error" do - message = %Message{ - o: @get_treatments_with_config_opcode, - a: ["user_key", "bucketing_key", ["feature_name1", "feature_name2"]] - } - - response = {:ok, %{"s" => 0x10, "p" => %{"m" => "Some bad error"}}} - - assert capture_log(fn -> - assert ResponseParser.parse_response(response, message) == - {:error, :splitd_internal_error} - end) =~ "Error response received from Splitd" - end - - test "handles unknow/unparsable payload" do - message = %Message{ - o: @get_treatments_with_config_opcode, - a: ["user_key", "bucketing_key", ["feature_name1", "feature_name2"]] - } - - response = {:ok, "some bad payload"} - - assert capture_log(fn -> - assert ResponseParser.parse_response(response, message) == - {:error, :splitd_parse_error} - end) =~ "Unable to parse Splitd response" + assert ResponseParser.parse_response(response, message) == false end - test "handles socket errors" do - message = %Message{ - o: @get_treatments_with_config_opcode, - a: ["user_key", "bucketing_key", ["feature_name1", "feature_name2"]] - } - - response = {:error, :enoent} - - assert capture_log(fn -> - assert ResponseParser.parse_response(response, message) == - {:error, :enoent} - end) =~ "Error while communicating with Splitd" - end + # test "handles splitd internal error" do + # message = %Message{ + # o: @get_treatments_with_config_opcode, + # a: ["user_key", "bucketing_key", ["feature_name1", "feature_name2"]] + # } + + # response = {:ok, %{"s" => 0x10, "p" => %{"m" => "Some bad error"}}} + + # assert capture_log(fn -> + # assert ResponseParser.parse_response(response, message) == + # {:error, :splitd_internal_error} + # end) =~ "Error response received from Splitd" + # end + + # test "handles unknow/unparsable payload" do + # message = %Message{ + # o: @get_treatments_with_config_opcode, + # a: ["user_key", "bucketing_key", ["feature_name1", "feature_name2"]] + # } + + # response = {:ok, "some bad payload"} + + # assert capture_log(fn -> + # assert ResponseParser.parse_response(response, message) == + # {:error, :splitd_parse_error} + # end) =~ "Unable to parse Splitd response" + # end + + # test "handles socket errors" do + # message = %Message{ + # o: @get_treatments_with_config_opcode, + # a: ["user_key", "bucketing_key", ["feature_name1", "feature_name2"]] + # } + + # response = {:error, :enoent} + + # assert capture_log(fn -> + # assert ResponseParser.parse_response(response, message) == + # {:error, :enoent} + # end) =~ "Error while communicating with Splitd" + # end end - describe "parse_response/2 with fallback enabled" do - setup do - old_value = :persistent_term.get(:splitd_fallback_enabled, false) - :persistent_term.put(:splitd_fallback_enabled, true) - on_exit(fn -> :persistent_term.put(:splitd_fallback_enabled, old_value) end) - end - + describe "parse_response/2 with fallback" do test "returns fallback for the sent message on invalid splitd response" do message = %Message{o: @split_opcode, a: []} @@ -342,7 +330,7 @@ defmodule Split.RPC.ResponseParserTest do assert capture_log(fn -> assert ResponseParser.parse_response(response, message) == - {:ok, nil} + nil end) =~ "Unable to parse Splitd response" end @@ -356,14 +344,13 @@ defmodule Split.RPC.ResponseParserTest do assert capture_log(fn -> assert ResponseParser.parse_response(response, message) == - {:ok, - %Split.Treatment{ - change_number: nil, - config: nil, - label: "fallback treatment", - timestamp: nil, - treatment: "control" - }} + %Split.Treatment{ + change_number: nil, + config: nil, + label: "exception", + timestamp: nil, + treatment: "control" + } end) =~ "Error response received from Splitd" end @@ -377,16 +364,15 @@ defmodule Split.RPC.ResponseParserTest do assert capture_log(fn -> assert ResponseParser.parse_response(response, message) == - {:ok, - %{ - "feature_name1" => %Split.Treatment{ - treatment: "control", - label: "fallback treatment", - config: nil, - change_number: nil, - timestamp: nil - } - }} + %{ + "feature_name1" => %Split.Treatment{ + treatment: "control", + label: "exception", + config: nil, + change_number: nil, + timestamp: nil + } + } end) =~ "Error while communicating with Splitd" end @@ -418,11 +404,4 @@ defmodule Split.RPC.ResponseParserTest do }} end end - - defp sorted_by(response, fun) do - response - |> elem(1) - |> Enum.sort_by(fun) - |> then(&{:ok, &1}) - end end diff --git a/test/split_test.exs b/test/split_test.exs index 4d6ebc9..da51f96 100644 --- a/test/split_test.exs +++ b/test/split_test.exs @@ -21,7 +21,7 @@ defmodule SplitThinElixirTest do describe "get_treatment/2" do test "returns expected struct" do - assert {:ok, %{treatment: "on"}} = + assert %{treatment: "on"} = Split.get_treatment("user-id-" <> to_string(Enum.random(1..100_000)), "ethan_test") end @@ -36,7 +36,7 @@ defmodule SplitThinElixirTest do describe "get_treatment_with_config/2" do test "returns expected struct" do - assert {:ok, %Treatment{treatment: "on", config: %{"foo" => "bar"}}} = + assert %Treatment{treatment: "on", config: %{"foo" => "bar"}} = Split.get_treatment_with_config( "user-id-" <> to_string(Enum.random(1..100_000)), "ethan_test" @@ -57,7 +57,7 @@ defmodule SplitThinElixirTest do describe "get_treatments/2" do test "returns expected map with structs" do - assert {:ok, %{"ethan_test" => %Treatment{treatment: "on"}}} = + assert %{"ethan_test" => %Treatment{treatment: "on"}} = Split.get_treatments("user-id-" <> to_string(Enum.random(1..100_000)), [ "ethan_test" ]) @@ -74,7 +74,7 @@ defmodule SplitThinElixirTest do describe "get_treatments_with_config/2" do test "returns expected struct" do - assert {:ok, %{"ethan_test" => %Treatment{treatment: "on", config: %{"foo" => "bar"}}}} = + assert %{"ethan_test" => %Treatment{treatment: "on", config: %{"foo" => "bar"}}} = Split.get_treatments_with_config( "user-id-" <> to_string(Enum.random(1..100_000)), [ @@ -95,21 +95,21 @@ defmodule SplitThinElixirTest do end test "track/3" do - assert :ok = + assert true = Split.track("user-id-" <> to_string(Enum.random(1..100_000)), "account", "purchase") end test "split_names/0" do - assert {:ok, %{split_names: ["ethan_test"]}} == Split.split_names() + assert %{split_names: ["ethan_test"]} == Split.split_names() end test "split/1" do - assert {:ok, %Split{name: "test-split"}} = + assert %Split{name: "test-split"} = Split.split("test-split") end test "splits/0" do - assert {:ok, [%Split{name: "test-split"}]} = Split.splits() + assert [%Split{name: "test-split"}] = Split.splits() end describe "telemetry" do @@ -120,7 +120,7 @@ defmodule SplitThinElixirTest do [:split, :rpc, :stop] ]) - {:ok, split} = Split.split("test-split") + split = Split.split("test-split") assert_received {[:split, :rpc, :start], ^ref, _, %{rpc_call: :split}} From 0acac189d9b782d015b3bf28f60a9dfb38216add Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 Feb 2025 16:14:31 -0300 Subject: [PATCH 14/46] Update changelog entry --- CHANGES.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.txt b/CHANGES.txt index 7185505..c513631 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,8 @@ +0.2.0 (February XX, 2025): + - BREAKING CHANGES: + - Removed the `fallback_enabled` option from the `Split.Supervisor.start_link` function. The fallback behavior is now always enabled. + - Updated the return type of `Split` functions to never return `{:error, _}` tuples. Instead, they will use the fallback value when an error occurs. + 0.1.0 (January 27, 2025): - BREAKING CHANGES: - Renamed `Split.Socket.Supervisor` module to `Split.Supervisor`, and updated the project structure to use a Context which is more in line to how Elixir libraries are structured. From a6741ae522b93badfdeb35952e049fef875ac00a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 Feb 2025 17:32:13 -0300 Subject: [PATCH 15/46] Update changelog entry --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index c513631..e641326 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,6 @@ 0.2.0 (February XX, 2025): - BREAKING CHANGES: - - Removed the `fallback_enabled` option from the `Split.Supervisor.start_link` function. The fallback behavior is now always enabled. + - Removed the `fallback_enabled` option from the `Split.Supervisor.start_link/1` function. The fallback behavior is now always enabled. - Updated the return type of `Split` functions to never return `{:error, _}` tuples. Instead, they will use the fallback value when an error occurs. 0.1.0 (January 27, 2025): From 168eb62a135a9effd983acf5d9c2cce1a2bad6b0 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 Feb 2025 19:45:24 -0300 Subject: [PATCH 16/46] Update return types of get_treatment and get_treatments functions to return strings instead of treatment structs --- lib/split.ex | 8 ++++---- test/split_test.exs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/split.ex b/lib/split.ex index 86bed28..703fe80 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -98,7 +98,7 @@ defmodule Split do @spec child_spec(options()) :: Supervisor.child_spec() defdelegate child_spec(options), to: Split.Supervisor - @spec get_treatment(String.t(), String.t(), String.t() | nil, map() | nil) :: Treatment.t() + @spec get_treatment(String.t(), String.t(), String.t() | nil, map() | nil) :: String.t() def get_treatment(user_key, feature_name, bucketing_key \\ nil, attributes \\ %{}) do request = Message.get_treatment( @@ -108,7 +108,7 @@ defmodule Split do attributes: attributes ) - execute_rpc(request) + execute_rpc(request).treatment end @spec get_treatment_with_config(String.t(), String.t(), String.t() | nil, map() | nil) :: @@ -126,7 +126,7 @@ defmodule Split do end @spec get_treatments(String.t(), [String.t()], String.t() | nil, map() | nil) :: %{ - String.t() => Treatment.t() + String.t() => String.t() } def get_treatments(user_key, feature_names, bucketing_key \\ nil, attributes \\ %{}) do request = @@ -137,7 +137,7 @@ defmodule Split do attributes: attributes ) - execute_rpc(request) + execute_rpc(request) |> Enum.into(%{}, fn {key, treatment} -> {key, treatment.treatment} end) end @spec get_treatments_with_config(String.t(), [String.t()], String.t() | nil, map() | nil) :: %{ diff --git a/test/split_test.exs b/test/split_test.exs index e257392..36f440d 100644 --- a/test/split_test.exs +++ b/test/split_test.exs @@ -21,7 +21,7 @@ defmodule SplitThinElixirTest do describe "get_treatment/2" do test "returns expected struct" do - assert %{treatment: "on"} = + assert "on" = Split.get_treatment("user-id-" <> to_string(Enum.random(1..100_000)), "ethan_test") end @@ -57,7 +57,7 @@ defmodule SplitThinElixirTest do describe "get_treatments/2" do test "returns expected map with structs" do - assert %{"ethan_test" => %Treatment{treatment: "on"}} = + assert %{"ethan_test" => "on"} = Split.get_treatments("user-id-" <> to_string(Enum.random(1..100_000)), [ "ethan_test" ]) From 2291bf35dd43fe321eb8fe4493c1ef3123a20820 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 Feb 2025 19:45:47 -0300 Subject: [PATCH 17/46] Remove treatment_with_config module and associated mapping functions --- lib/split/treatment.ex | 29 ----------------------------- lib/split/treatment_with_config.ex | 9 --------- 2 files changed, 38 deletions(-) delete mode 100644 lib/split/treatment_with_config.ex diff --git a/lib/split/treatment.ex b/lib/split/treatment.ex index 5fc1475..6cea07b 100644 --- a/lib/split/treatment.ex +++ b/lib/split/treatment.ex @@ -29,33 +29,4 @@ defmodule Split.Treatment do timestamp: timestamp } end - - @spec map_to_treatment_with_config(t()) :: Split.TreatmentWithConfig.t() - def map_to_treatment_with_config(treatment) do - %Split.TreatmentWithConfig{ - treatment: treatment.treatment, - config: treatment.config - } - end - - @spec map_to_treatment_string(t()) :: String.t() - def map_to_treatment_string(treatment) do - treatment.treatment - end - - @spec map_treatments_to_treatments_string({:ok, %{String.t() => Split.Treatment.t()}}) :: - %{String.t() => String.t()} - def map_treatments_to_treatments_string(treatments) do - treatments - |> Enum.map(fn {key, treatment} -> {key, treatment.treatment} end) - |> Enum.into(%{}) - end - - @spec map_treatments_to_treatments_with_config({:ok, %{String.t() => Split.Treatment.t()}}) :: - %{String.t() => Split.TreatmentWithConfig.t()} - def map_treatments_to_treatments_with_config(treatments) do - treatments - |> Enum.map(fn {key, treatment} -> {key, map_to_treatment_with_config(treatment)} end) - |> Enum.into(%{}) - end end diff --git a/lib/split/treatment_with_config.ex b/lib/split/treatment_with_config.ex deleted file mode 100644 index cac3bdd..0000000 --- a/lib/split/treatment_with_config.ex +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Split.TreatmentWithConfig do - defstruct treatment: "control", - config: nil - - @type t :: %__MODULE__{ - treatment: String.t(), - config: String.t() | nil - } -end From 9c0d0d2392a4d9b378a8c715c0170a90bbd81f6d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 Feb 2025 19:55:52 -0300 Subject: [PATCH 18/46] Update changelog entry --- CHANGES.txt | 2 ++ lib/split.ex | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index e641326..dd13eab 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,6 +2,8 @@ - BREAKING CHANGES: - Removed the `fallback_enabled` option from the `Split.Supervisor.start_link/1` function. The fallback behavior is now always enabled. - Updated the return type of `Split` functions to never return `{:error, _}` tuples. Instead, they will use the fallback value when an error occurs. + - Updated the return types of `Split.get_treatment/3` and `Split.get_treatments/3` to return the treatment string value and a map of treatment string values, respectively. + - Updated the `Split` struct: renamed the `configurations` field to `configs`, the `flag_sets` field to `sets`, and added the `impressions_disabled` field. 0.1.0 (January 27, 2025): - BREAKING CHANGES: diff --git a/lib/split.ex b/lib/split.ex index 703fe80..ab8838e 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -55,7 +55,6 @@ defmodule Split do alias Split.RPC.Message alias Split.RPC.ResponseParser - # @TODO move struct to Split.SplitView module and document it @type t :: %Split{ name: String.t(), traffic_type: String.t(), From c74037bda7a15de00ef22852ef441701449b7bd8 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 Feb 2025 20:07:23 -0300 Subject: [PATCH 19/46] Moved the Split struct into the new Split.SplitView module. --- CHANGES.txt | 1 + lib/split.ex | 29 +++-------------------------- lib/split/rpc/response_parser.ex | 3 ++- lib/split/split_view.ex | 25 +++++++++++++++++++++++++ test/rpc/response_parser_test.exs | 7 ++++--- test/split_test.exs | 5 +++-- 6 files changed, 38 insertions(+), 32 deletions(-) create mode 100644 lib/split/split_view.ex diff --git a/CHANGES.txt b/CHANGES.txt index dd13eab..cef547c 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -4,6 +4,7 @@ - Updated the return type of `Split` functions to never return `{:error, _}` tuples. Instead, they will use the fallback value when an error occurs. - Updated the return types of `Split.get_treatment/3` and `Split.get_treatments/3` to return the treatment string value and a map of treatment string values, respectively. - Updated the `Split` struct: renamed the `configurations` field to `configs`, the `flag_sets` field to `sets`, and added the `impressions_disabled` field. + - Moved the `Split` struct into the new `Split.SplitView` module. 0.1.0 (January 27, 2025): - BREAKING CHANGES: diff --git a/lib/split.ex b/lib/split.ex index ab8838e..1447a4a 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -52,21 +52,10 @@ defmodule Split do alias Split.Telemetry alias Split.Sockets.Pool alias Split.Treatment + alias Split.SplitView alias Split.RPC.Message alias Split.RPC.ResponseParser - @type t :: %Split{ - name: String.t(), - traffic_type: String.t(), - killed: boolean(), - treatments: [String.t()], - change_number: integer(), - configs: map(), - default_treatment: String.t(), - sets: [String.t()], - impressions_disabled: boolean() - } - @typedoc "An option that can be provided when starting `Split`." @type option :: {:socket_path, String.t()} @@ -75,18 +64,6 @@ defmodule Split do @type options :: [option()] - defstruct [ - :name, - :traffic_type, - :killed, - :treatments, - :change_number, - :configs, - :default_treatment, - :sets, - :impressions_disabled - ] - @doc """ Builds a child specification to use in a Supervisor. @@ -166,14 +143,14 @@ defmodule Split do execute_rpc(request) end - @spec split(String.t()) :: Split.t() | nil + @spec split(String.t()) :: SplitView.t() | nil def split(name) do request = Message.split(name) execute_rpc(request) end - @spec splits() :: [Split.t()] + @spec splits() :: [SplitView.t()] def splits do request = Message.splits() execute_rpc(request) diff --git a/lib/split/rpc/response_parser.ex b/lib/split/rpc/response_parser.ex index 4a2ef6e..be2a708 100644 --- a/lib/split/rpc/response_parser.ex +++ b/lib/split/rpc/response_parser.ex @@ -9,6 +9,7 @@ defmodule Split.RPC.ResponseParser do alias Split.RPC.Message alias Split.Telemetry alias Split.Treatment + alias Split.SplitView @type splitd_response :: {:ok, map()} | {:error, term()} @@ -151,7 +152,7 @@ defmodule Split.RPC.ResponseParser do end defp parse_split(payload) do - %Split{ + %SplitView{ name: Map.get(payload, "n", nil), traffic_type: payload["t"], killed: payload["k"], diff --git a/lib/split/split_view.ex b/lib/split/split_view.ex new file mode 100644 index 0000000..312e7e7 --- /dev/null +++ b/lib/split/split_view.ex @@ -0,0 +1,25 @@ +defmodule Split.SplitView do + defstruct [ + :name, + :traffic_type, + :killed, + :treatments, + :change_number, + :configs, + :default_treatment, + :sets, + :impressions_disabled + ] + + @type t :: %__MODULE__{ + name: String.t(), + traffic_type: String.t(), + killed: boolean(), + treatments: [String.t()], + change_number: integer(), + configs: map(), + default_treatment: String.t(), + sets: [String.t()], + impressions_disabled: boolean() + } +end diff --git a/test/rpc/response_parser_test.exs b/test/rpc/response_parser_test.exs index 8a7c776..c185a9d 100644 --- a/test/rpc/response_parser_test.exs +++ b/test/rpc/response_parser_test.exs @@ -6,6 +6,7 @@ defmodule Split.RPC.ResponseParserTest do alias Split.RPC.ResponseParser alias Split.RPC.Message alias Split.Treatment + alias Split.SplitView import ExUnit.CaptureLog @@ -166,7 +167,7 @@ defmodule Split.RPC.ResponseParserTest do }} assert ResponseParser.parse_response(response, message) == - %Split{ + %SplitView{ name: "feature_name", traffic_type: "user", killed: false, @@ -222,7 +223,7 @@ defmodule Split.RPC.ResponseParserTest do # Order of splits is not guaranteed assert ResponseParser.parse_response(response, message) |> Enum.sort_by(& &1.name) == [ - %Split{ + %SplitView{ name: "feature_a", traffic_type: "user", killed: false, @@ -233,7 +234,7 @@ defmodule Split.RPC.ResponseParserTest do sets: [], impressions_disabled: false }, - %Split{ + %SplitView{ name: "feature_b", traffic_type: "user", killed: false, diff --git a/test/split_test.exs b/test/split_test.exs index 36f440d..f279948 100644 --- a/test/split_test.exs +++ b/test/split_test.exs @@ -3,6 +3,7 @@ defmodule SplitThinElixirTest do alias Split.Impression alias Split.Treatment + alias Split.SplitView setup_all context do test_id = :erlang.phash2(context.case) @@ -104,12 +105,12 @@ defmodule SplitThinElixirTest do end test "split/1" do - assert %Split{name: "test-split"} = + assert %SplitView{name: "test-split"} = Split.split("test-split") end test "splits/0" do - assert [%Split{name: "test-split"}] = Split.splits() + assert [%SplitView{name: "test-split"}] = Split.splits() end describe "telemetry" do From 66b85cb29e667e53a25c668504c82779ed073333 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 Feb 2025 21:08:32 -0300 Subject: [PATCH 20/46] Fix parse_response --- lib/split/rpc/response_parser.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/split/rpc/response_parser.ex b/lib/split/rpc/response_parser.ex index be2a708..2bae8bc 100644 --- a/lib/split/rpc/response_parser.ex +++ b/lib/split/rpc/response_parser.ex @@ -18,7 +18,7 @@ defmodule Split.RPC.ResponseParser do """ @spec parse_response(response :: splitd_response(), request :: Message.t(), [ {:span_context, reference()} | {:span_context, nil} - ]) :: map() | list() | Treatment.t() | Split.t() | boolean() | nil + ]) :: map() | list() | Treatment.t() | SplitView.t() | boolean() | nil def parse_response(response, original_request, opts \\ []) From 1d53ec70d5c650c11f9c88c34818ab18ba3915e6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 Feb 2025 21:28:22 -0300 Subject: [PATCH 21/46] Fixing tests --- lib/split/rpc/fallback.ex | 2 +- lib/split/rpc/response_parser.ex | 2 +- test/rpc/response_parser_test.exs | 132 +++++++++++++++--------------- test/split_test.exs | 8 +- 4 files changed, 70 insertions(+), 74 deletions(-) diff --git a/lib/split/rpc/fallback.ex b/lib/split/rpc/fallback.ex index 7a0fb11..86062bd 100644 --- a/lib/split/rpc/fallback.ex +++ b/lib/split/rpc/fallback.ex @@ -70,7 +70,7 @@ defmodule Split.RPC.Fallback do @get_treatments_with_config_by_flag_sets_opcode ] do - {:ok, %{}} # Empty map since we don't have a way to know the feature names + %{} # Empty map since we don't have a way to know the feature names end def fallback(%Message{o: @split_opcode}) do diff --git a/lib/split/rpc/response_parser.ex b/lib/split/rpc/response_parser.ex index 1176504..f1cfe3b 100644 --- a/lib/split/rpc/response_parser.ex +++ b/lib/split/rpc/response_parser.ex @@ -86,7 +86,7 @@ defmodule Split.RPC.ResponseParser do end) |> Map.new() - {:ok, mapped_treatments} + mapped_treatments end def parse_response( diff --git a/test/rpc/response_parser_test.exs b/test/rpc/response_parser_test.exs index 41ce7e9..018256d 100644 --- a/test/rpc/response_parser_test.exs +++ b/test/rpc/response_parser_test.exs @@ -168,23 +168,22 @@ defmodule Split.RPC.ResponseParserTest do }} assert ResponseParser.parse_response(response, message) == - {:ok, - %{ - "feature_name1" => %Split.Treatment{ - treatment: "on", - label: "test label 1", - config: nil, - change_number: 123, - timestamp: 1_723_742_604 - }, - "feature_name2" => %Split.Treatment{ - treatment: "off", - label: "test label 2", - config: nil, - change_number: 456, - timestamp: 1_723_742_604 - } - }} + %{ + "feature_name1" => %Split.Treatment{ + treatment: "on", + label: "test label 1", + config: nil, + change_number: 123, + timestamp: 1_723_742_604 + }, + "feature_name2" => %Split.Treatment{ + treatment: "off", + label: "test label 2", + config: nil, + change_number: 456, + timestamp: 1_723_742_604 + } + } end test "parses get_treatments_with_config_by_flag_set RPC response" do @@ -214,23 +213,22 @@ defmodule Split.RPC.ResponseParserTest do }} assert ResponseParser.parse_response(response, message) == - {:ok, - %{ - "feature_name1" => %Split.Treatment{ - treatment: "on", - label: "test label 1", - config: "{\"foo\": \"bar\"}", - change_number: 123, - timestamp: 1_723_742_604 - }, - "feature_name2" => %Split.Treatment{ - treatment: "off", - label: "test label 2", - config: "{\"baz\": \"qux\"}", - change_number: 456, - timestamp: 1_723_742_604 - } - }} + %{ + "feature_name1" => %Split.Treatment{ + treatment: "on", + label: "test label 1", + config: "{\"foo\": \"bar\"}", + change_number: 123, + timestamp: 1_723_742_604 + }, + "feature_name2" => %Split.Treatment{ + treatment: "off", + label: "test label 2", + config: "{\"baz\": \"qux\"}", + change_number: 456, + timestamp: 1_723_742_604 + } + } end test "parses get_treatments_by_flag_sets RPC response" do @@ -258,23 +256,22 @@ defmodule Split.RPC.ResponseParserTest do }} assert ResponseParser.parse_response(response, message) == - {:ok, - %{ - "feature_name1" => %Split.Treatment{ - treatment: "on", - label: "test label 1", - config: nil, - change_number: 123, - timestamp: 1_723_742_604 - }, - "feature_name2" => %Split.Treatment{ - treatment: "off", - label: "test label 2", - config: nil, - change_number: 456, - timestamp: 1_723_742_604 - } - }} + %{ + "feature_name1" => %Split.Treatment{ + treatment: "on", + label: "test label 1", + config: nil, + change_number: 123, + timestamp: 1_723_742_604 + }, + "feature_name2" => %Split.Treatment{ + treatment: "off", + label: "test label 2", + config: nil, + change_number: 456, + timestamp: 1_723_742_604 + } + } end test "parses get_treatments_with_config_by_flag_sets RPC response" do @@ -304,23 +301,22 @@ defmodule Split.RPC.ResponseParserTest do }} assert ResponseParser.parse_response(response, message) == - {:ok, - %{ - "feature_name1" => %Split.Treatment{ - treatment: "on", - label: "test label 1", - config: "{\"foo\": \"bar\"}", - change_number: 123, - timestamp: 1_723_742_604 - }, - "feature_name2" => %Split.Treatment{ - treatment: "off", - label: "test label 2", - config: "{\"baz\": \"qux\"}", - change_number: 456, - timestamp: 1_723_742_604 - } - }} + %{ + "feature_name1" => %Split.Treatment{ + treatment: "on", + label: "test label 1", + config: "{\"foo\": \"bar\"}", + change_number: 123, + timestamp: 1_723_742_604 + }, + "feature_name2" => %Split.Treatment{ + treatment: "off", + label: "test label 2", + config: "{\"baz\": \"qux\"}", + change_number: 456, + timestamp: 1_723_742_604 + } + } end test "parses split RPC call" do diff --git a/test/split_test.exs b/test/split_test.exs index d27c373..24e011d 100644 --- a/test/split_test.exs +++ b/test/split_test.exs @@ -97,7 +97,7 @@ defmodule SplitThinElixirTest do describe "get_treatments_by_flag_set/2" do test "returns expected map with structs" do - assert {:ok, %{"emi_test" => %Treatment{treatment: "on"}}} = + assert %{"emi_test" => "on"} = Split.get_treatments_by_flag_set( "user-id-" <> to_string(Enum.random(1..100_000)), "flag_set_name" @@ -118,7 +118,7 @@ defmodule SplitThinElixirTest do describe "get_treatments_with_config_by_flag_set/2" do test "returns expected struct" do - assert {:ok, %{"emi_test" => %Treatment{treatment: "on", config: %{"foo" => "bar"}}}} = + assert %{"emi_test" => %Treatment{treatment: "on", config: %{"foo" => "bar"}}} = Split.get_treatments_with_config_by_flag_set( "user-id-" <> to_string(Enum.random(1..100_000)), "flag_set_name" @@ -139,7 +139,7 @@ defmodule SplitThinElixirTest do describe "get_treatments_by_flag_sets/2" do test "returns expected map with structs" do - assert {:ok, %{"emi_test" => %Treatment{treatment: "on"}}} = + assert %{"emi_test" => "on"} = Split.get_treatments_by_flag_sets( "user-id-" <> to_string(Enum.random(1..100_000)), [ @@ -161,7 +161,7 @@ defmodule SplitThinElixirTest do describe "get_treatments_with_config_by_flag_sets/2" do test "returns expected struct" do - assert {:ok, %{"emi_test" => %Treatment{treatment: "on", config: %{"foo" => "bar"}}}} = + assert %{"emi_test" => %Treatment{treatment: "on", config: %{"foo" => "bar"}}} = Split.get_treatments_with_config_by_flag_sets( "user-id-" <> to_string(Enum.random(1..100_000)), [ From 9d70f2d55deff9c1ddf18ac9d2efe202e9ae4fb1 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 Feb 2025 21:31:50 -0300 Subject: [PATCH 22/46] Update changelog entry --- CHANGES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.txt b/CHANGES.txt index cef547c..3617c36 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 0.2.0 (February XX, 2025): + - Added new variations of the get treatment methods to support evaluating flags in given flag set/s: `Split.get_treatments_by_flag_set`, `Split.get_treatments_by_flag_sets`, `Split.get_treatments_with_config_by_flag_set`, and `Split.get_treatments_with_config_by_flag_sets`. - BREAKING CHANGES: - Removed the `fallback_enabled` option from the `Split.Supervisor.start_link/1` function. The fallback behavior is now always enabled. - Updated the return type of `Split` functions to never return `{:error, _}` tuples. Instead, they will use the fallback value when an error occurs. From ca99a9e49385c5be7ca175820e83d57d76b17c56 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Wed, 5 Feb 2025 21:45:56 -0300 Subject: [PATCH 23/46] Update .gitignore and improve CONTRIBUTORS-GUIDE and README links --- .gitignore | 3 +++ CONTRIBUTORS-GUIDE.md | 2 +- README.md | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index f1a03a3..e13d5e3 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ split_thin_elixir-*.tar splitd # Ignore the splitd configuration file support/splitd.yaml + +# IDE files +/.vscode/ diff --git a/CONTRIBUTORS-GUIDE.md b/CONTRIBUTORS-GUIDE.md index b5a4d55..0f014b8 100644 --- a/CONTRIBUTORS-GUIDE.md +++ b/CONTRIBUTORS-GUIDE.md @@ -9,7 +9,7 @@ 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 make sure it runs with no errors. +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. diff --git a/README.md b/README.md index 8d0fb3b..1009be6 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ case Split.get_treatment(user_id, feature_flag_name) do end ``` -Please refer to [our official docs](https://help.split.io/hc/en-us/articles/@TODO-Elixir-Thin-Client-SDK) to learn about all the functionality provided by our SDK and the configuration options available for tailoring it to your current application setup. +Please refer to [our official docs](https://help.split.io/hc/en-us/articles/26988707417869-Elixir-Thin-Client-SDK) to learn about all the functionality provided by our SDK and the configuration options available for tailoring it to your current application setup. ## Submitting issues @@ -70,7 +70,7 @@ Split has built and maintains SDKs for: * .NET [Github](https://github.com/splitio/dotnet-client) [Docs](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK) * Android [Github](https://github.com/splitio/android-client) [Docs](https://help.split.io/hc/en-us/articles/360020343291-Android-SDK) * Angular [Github](https://github.com/splitio/angular-sdk-plugin) [Docs](https://help.split.io/hc/en-us/articles/6495326064397-Angular-utilities) -* Elixir thin-client [Github](https://github.com/splitio/elixir-thin-client) [Docs](https://help.split.io/hc/en-us/articles/@TODO-Elixir-Thin-Client-SDK) +* Elixir thin-client [Github](https://github.com/splitio/elixir-thin-client) [Docs](https://help.split.io/hc/en-us/articles/26988707417869-Elixir-Thin-Client-SDK) * Flutter [Github](https://github.com/splitio/flutter-sdk-plugin) [Docs](https://help.split.io/hc/en-us/articles/8096158017165-Flutter-plugin) * GO [Github](https://github.com/splitio/go-client) [Docs](https://help.split.io/hc/en-us/articles/360020093652-Go-SDK) * iOS [Github](https://github.com/splitio/ios-client) [Docs](https://help.split.io/hc/en-us/articles/360020401491-iOS-SDK) From a5a25e8541413639b4615ec81c40ccd33878d00b Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 6 Feb 2025 14:11:56 -0300 Subject: [PATCH 24/46] Polish tests --- test/split_test.exs | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/test/split_test.exs b/test/split_test.exs index 24e011d..cf0c0ba 100644 --- a/test/split_test.exs +++ b/test/split_test.exs @@ -20,10 +20,16 @@ defmodule SplitThinElixirTest do :ok end - describe "get_treatment/2" do + describe "get_treatment/3" do test "returns expected struct" do assert "on" = - Split.get_treatment("user-id-" <> to_string(Enum.random(1..100_000)), "ethan_test") + Split.get_treatment( + "user-id-" <> to_string(Enum.random(1..100_000)), + "ethan_test", + %{ + :some_attribute => "some_value" + } + ) end test "emits telemetry event for impression listening" do @@ -35,7 +41,7 @@ defmodule SplitThinElixirTest do end end - describe "get_treatment_with_config/2" do + describe "get_treatment_with_config/3" do test "returns expected struct" do assert %Treatment{treatment: "on", config: %{"foo" => "bar"}} = Split.get_treatment_with_config( @@ -56,7 +62,7 @@ defmodule SplitThinElixirTest do end end - describe "get_treatments/2" do + describe "get_treatments/3" do test "returns expected map with structs" do assert %{"ethan_test" => "on"} = Split.get_treatments("user-id-" <> to_string(Enum.random(1..100_000)), [ @@ -73,7 +79,7 @@ defmodule SplitThinElixirTest do end end - describe "get_treatments_with_config/2" do + describe "get_treatments_with_config/3" do test "returns expected struct" do assert %{"ethan_test" => %Treatment{treatment: "on", config: %{"foo" => "bar"}}} = Split.get_treatments_with_config( @@ -95,7 +101,7 @@ defmodule SplitThinElixirTest do end end - describe "get_treatments_by_flag_set/2" do + describe "get_treatments_by_flag_set/3" do test "returns expected map with structs" do assert %{"emi_test" => "on"} = Split.get_treatments_by_flag_set( @@ -116,7 +122,7 @@ defmodule SplitThinElixirTest do end end - describe "get_treatments_with_config_by_flag_set/2" do + describe "get_treatments_with_config_by_flag_set/3" do test "returns expected struct" do assert %{"emi_test" => %Treatment{treatment: "on", config: %{"foo" => "bar"}}} = Split.get_treatments_with_config_by_flag_set( @@ -137,7 +143,7 @@ defmodule SplitThinElixirTest do end end - describe "get_treatments_by_flag_sets/2" do + describe "get_treatments_by_flag_sets/3" do test "returns expected map with structs" do assert %{"emi_test" => "on"} = Split.get_treatments_by_flag_sets( @@ -159,7 +165,7 @@ defmodule SplitThinElixirTest do end end - describe "get_treatments_with_config_by_flag_sets/2" do + describe "get_treatments_with_config_by_flag_sets/3" do test "returns expected struct" do assert %{"emi_test" => %Treatment{treatment: "on", config: %{"foo" => "bar"}}} = Split.get_treatments_with_config_by_flag_sets( @@ -184,9 +190,15 @@ defmodule SplitThinElixirTest do end end - test "track/3" do + test "track/5" do assert true = - Split.track("user-id-" <> to_string(Enum.random(1..100_000)), "account", "purchase") + Split.track( + "user-id-" <> to_string(Enum.random(1..100_000)), + "account", + "purchase", + 100, + %{"currency" => "USD"} + ) end test "split_names/0" do From 84af3e62bcf52b8ed50066e03b0dcf7a9bcc5c7f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 6 Feb 2025 17:43:20 -0300 Subject: [PATCH 25/46] Remove Split.Treatment module, added Split.TreatmentWithConfig and updated Split.Impression --- lib/split.ex | 60 +++++++++++++++-------- lib/split/impression.ex | 44 ++++++++++++----- lib/split/rpc/fallback.ex | 22 ++++----- lib/split/rpc/response_parser.ex | 41 +++++++++------- lib/split/telemetry.ex | 15 ++---- lib/split/treatment.ex | 32 ------------- lib/split/treatment_with_config.ex | 9 ++++ test/rpc/fallback_test.exs | 2 +- test/rpc/response_parser_test.exs | 76 +++++++++++++++++++++++------- test/split/impression_test.exs | 51 ++++++++++++++++++++ test/split/treatment_test.exs | 45 ------------------ test/split_test.exs | 10 ++-- 12 files changed, 238 insertions(+), 169 deletions(-) delete mode 100644 lib/split/treatment.ex create mode 100644 lib/split/treatment_with_config.ex create mode 100644 test/split/impression_test.exs delete mode 100644 test/split/treatment_test.exs diff --git a/lib/split.ex b/lib/split.ex index d6d6e95..e41e74f 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -49,10 +49,10 @@ defmodule Split do Split.get_treatment("user_key", "feature_name") ``` """ + alias Split.SplitView alias Split.Telemetry + alias Split.TreatmentWithConfig alias Split.Sockets.Pool - alias Split.Treatment - alias Split.SplitView alias Split.RPC.Message alias Split.RPC.ResponseParser @@ -85,10 +85,10 @@ defmodule Split do attributes: attributes ) - execute_rpc(request).treatment + execute_rpc(request) |> impression_to_treatment() end - @spec get_treatment_with_config(split_key(), String.t(), map() | nil) :: Treatment.t() + @spec get_treatment_with_config(split_key(), String.t(), map() | nil) :: TreatmentWithConfig.t() def get_treatment_with_config(key, feature_name, attributes \\ %{}) do request = Message.get_treatment_with_config( @@ -97,12 +97,12 @@ defmodule Split do attributes: attributes ) - execute_rpc(request) + execute_rpc(request) |> impression_to_treatment_with_config() end @spec get_treatments(split_key(), [String.t()], map() | nil) :: %{ - String.t() => String.t() - } + String.t() => String.t() + } def get_treatments(key, feature_names, attributes \\ %{}) do request = Message.get_treatments( @@ -111,12 +111,12 @@ defmodule Split do attributes: attributes ) - execute_rpc(request) |> Enum.into(%{}, fn {key, treatment} -> {key, treatment.treatment} end) + execute_rpc(request) |> impressions_to_treatments() end @spec get_treatments_with_config(split_key(), [String.t()], map() | nil) :: %{ - String.t() => Treatment.t() - } + String.t() => TreatmentWithConfig.t() + } def get_treatments_with_config(key, feature_names, attributes \\ %{}) do request = Message.get_treatments_with_config( @@ -125,7 +125,7 @@ defmodule Split do attributes: attributes ) - execute_rpc(request) + execute_rpc(request) |> impressions_to_treatments_with_config() end @spec get_treatments_by_flag_set(split_key(), String.t(), map() | nil) :: %{ @@ -139,15 +139,15 @@ defmodule Split do attributes: attributes ) - execute_rpc(request) |> Enum.into(%{}, fn {key, treatment} -> {key, treatment.treatment} end) + execute_rpc(request) |> impressions_to_treatments() end @spec get_treatments_with_config_by_flag_set( - split_key(), + split_key(), String.t(), map() | nil ) :: - %{String.t() => Treatment.t()} + %{String.t() => TreatmentWithConfig.t()} def get_treatments_with_config_by_flag_set( key, flag_set_name, @@ -160,7 +160,7 @@ defmodule Split do attributes: attributes ) - execute_rpc(request) + execute_rpc(request) |> impressions_to_treatments_with_config() end @spec get_treatments_by_flag_sets(split_key(), [String.t()], map() | nil) :: @@ -177,17 +177,17 @@ defmodule Split do attributes: attributes ) - execute_rpc(request) |> Enum.into(%{}, fn {key, treatment} -> {key, treatment.treatment} end) + execute_rpc(request) |> impressions_to_treatments() end @spec get_treatments_with_config_by_flag_sets( - split_key(), + split_key(), [String.t()], map() | nil ) :: - %{String.t() => Treatment.t()} + %{String.t() => TreatmentWithConfig.t()} def get_treatments_with_config_by_flag_sets( - key, + key, flag_set_names, attributes \\ %{} ) do @@ -198,7 +198,7 @@ defmodule Split do attributes: attributes ) - execute_rpc(request) + execute_rpc(request) |> impressions_to_treatments_with_config() end @spec track(split_key(), String.t(), String.t(), number() | nil, map() | nil) :: boolean() @@ -244,4 +244,24 @@ defmodule Split do end end) end + + defp impression_to_treatment(impression) do + impression.treatment + end + + defp impression_to_treatment_with_config(impression) do + %TreatmentWithConfig{treatment: impression.treatment, config: impression.config} + end + + defp impressions_to_treatments(impressions) do + Enum.into(impressions, %{}, fn {key, impression} -> + {key, impression_to_treatment(impression)} + end) + end + + defp impressions_to_treatments_with_config(impressions) do + Enum.into(impressions, %{}, fn {key, impression} -> + {key, impression_to_treatment_with_config(impression)} + end) + end end diff --git a/lib/split/impression.ex b/lib/split/impression.ex index 228f8ed..f1f68fa 100644 --- a/lib/split/impression.ex +++ b/lib/split/impression.ex @@ -1,19 +1,41 @@ defmodule Split.Impression do - defstruct [ - :key, - :feature, - :treatment, - :label, - :change_number, - :timestamp - ] + defstruct key: nil, + bucketing_key: nil, + feature: nil, + treatment: "control", + config: nil, + label: nil, + change_number: nil, + timestamp: nil @type t :: %__MODULE__{ key: String.t(), + bucketing_key: String.t() | nil, feature: String.t(), treatment: String.t(), - label: String.t(), - change_number: integer(), - timestamp: integer() + config: String.t() | nil, + label: String.t() | nil, + change_number: integer() | nil, + timestamp: integer() | nil } + + @spec build_from_daemon_response(map(), String.t(), String.t() | nil, String.t()) :: t + def build_from_daemon_response(treatment_payload, key, bucketing_key, feature) do + treatment = treatment_payload["t"] + config = treatment_payload["c"] + label = get_in(treatment_payload, ["l", "l"]) + change_number = get_in(treatment_payload, ["l", "c"]) + timestamp = get_in(treatment_payload, ["l", "m"]) + + %Split.Impression{ + key: key, + bucketing_key: bucketing_key, + feature: feature, + treatment: treatment, + label: label, + config: config, + change_number: change_number, + timestamp: timestamp + } + end end diff --git a/lib/split/rpc/fallback.ex b/lib/split/rpc/fallback.ex index 86062bd..19bb59a 100644 --- a/lib/split/rpc/fallback.ex +++ b/lib/split/rpc/fallback.ex @@ -7,7 +7,7 @@ defmodule Split.RPC.Fallback do use Split.RPC.Opcodes alias Split.RPC.Message - alias Split.Treatment + alias Split.Impression @doc """ Provides a default value for the given RPC message. @@ -15,22 +15,22 @@ defmodule Split.RPC.Fallback do ## Examples iex> Fallback.fallback(%Message{o: 0x11}) - %Treatment{treatment: "control", label: "exception"} + %Impression{treatment: "control", label: "exception"} iex> Fallback.fallback(%Message{o: 0x13}) - %Treatment{treatment: "control", label: "exception", config: nil} + %Impression{treatment: "control", label: "exception", config: nil} iex> Fallback.fallback(%Message{ ...> o: 0x12, ...> a: ["user_key", "bucketing_key", ["feature_1", "feature_2"], %{}] ...> }) %{ - "feature_1" => %Treatment{treatment: "control", label: "exception"}, - "feature_2" => %Treatment{treatment: "control", label: "exception"} + "feature_1" => %Impression{treatment: "control", label: "exception"}, + "feature_2" => %Impression{treatment: "control", label: "exception"} } iex> Fallback.fallback(%Message{o: 0x14, a: ["user_key", "bucketing_key", ["feature_a"], %{}]}) - %{"feature_a" => %Treatment{treatment: "control", label: "exception", config: nil}} + %{"feature_a" => %Impression{treatment: "control", label: "exception", config: nil}} iex> Fallback.fallback(%Message{o: 0xA1}) nil @@ -44,10 +44,10 @@ defmodule Split.RPC.Fallback do iex> Fallback.fallback(%Message{o: 0x80}) false """ - @spec fallback(Message.t()) :: map() | Treatment.t() | list() | boolean() | nil + @spec fallback(Message.t()) :: map() | Impression.t() | list() | boolean() | nil def fallback(%Message{o: opcode}) when opcode in [@get_treatment_opcode, @get_treatment_with_config_opcode] do - %Treatment{label: "exception"} + %Impression{label: "exception"} end def fallback(%Message{o: opcode, a: args}) @@ -56,7 +56,7 @@ defmodule Split.RPC.Fallback do treatments = Enum.reduce(feature_names, %{}, fn feature_name, acc -> - Map.put(acc, feature_name, %Treatment{label: "exception"}) + Map.put(acc, feature_name, %Impression{label: "exception"}) end) treatments @@ -69,8 +69,8 @@ defmodule Split.RPC.Fallback do @get_treatments_by_flag_sets_opcode, @get_treatments_with_config_by_flag_sets_opcode ] do - - %{} # Empty map since we don't have a way to know the feature names + # Empty map since we don't have a way to know the feature names + %{} end def fallback(%Message{o: @split_opcode}) do diff --git a/lib/split/rpc/response_parser.ex b/lib/split/rpc/response_parser.ex index 8418310..b2f747c 100644 --- a/lib/split/rpc/response_parser.ex +++ b/lib/split/rpc/response_parser.ex @@ -8,7 +8,7 @@ defmodule Split.RPC.ResponseParser do alias Split.RPC.Fallback alias Split.RPC.Message alias Split.Telemetry - alias Split.Treatment + alias Split.Impression alias Split.SplitView @type splitd_response :: {:ok, map()} | {:error, term()} @@ -18,7 +18,7 @@ defmodule Split.RPC.ResponseParser do """ @spec parse_response(response :: splitd_response(), request :: Message.t(), [ {:span_context, reference()} | {:span_context, nil} - ]) :: map() | list() | Treatment.t() | SplitView.t() | boolean() | nil + ]) :: map() | list() | Impression.t() | SplitView.t() | boolean() | nil def parse_response(response, original_request, opts \\ []) @@ -31,12 +31,15 @@ defmodule Split.RPC.ResponseParser do _opts ) when opcode in [@get_treatment_opcode, @get_treatment_with_config_opcode] do - treatment = Treatment.build_from_daemon_response(treatment_data) key = Enum.at(args, 0) + bucketing_key = Enum.at(args, 1) feature_name = Enum.at(args, 2) - Telemetry.send_impression(key, feature_name, treatment) - treatment + impression = + Impression.build_from_daemon_response(treatment_data, key, bucketing_key, feature_name) + + Telemetry.send_impression(impression) + impression end def parse_response( @@ -48,17 +51,20 @@ defmodule Split.RPC.ResponseParser do _opts ) when opcode in [@get_treatments_opcode, @get_treatments_with_config_opcode] do - treatments = Enum.map(treatments, &Treatment.build_from_daemon_response/1) key = Enum.at(args, 0) + bucketing_key = Enum.at(args, 1) feature_names = Enum.at(args, 2) - mapped_treatments = + impressions = Enum.zip_reduce(feature_names, treatments, %{}, fn feature_name, treatment, acc -> - Telemetry.send_impression(key, feature_name, treatment) - Map.put(acc, feature_name, treatment) + impression = + Impression.build_from_daemon_response(treatment, key, bucketing_key, feature_name) + + Telemetry.send_impression(impression) + Map.put(acc, feature_name, impression) end) - mapped_treatments + impressions end def parse_response( @@ -75,18 +81,21 @@ defmodule Split.RPC.ResponseParser do @get_treatments_by_flag_sets_opcode, @get_treatments_with_config_by_flag_sets_opcode ] do - user_key = Enum.at(args, 0) + key = Enum.at(args, 0) + bucketing_key = Enum.at(args, 1) - mapped_treatments = + impressions = treatments |> Enum.map(fn {feature_name, treatment} -> - processed_treatment = Treatment.build_from_daemon_response(treatment) - Telemetry.send_impression(user_key, feature_name, processed_treatment) - {feature_name, processed_treatment} + impression = + Impression.build_from_daemon_response(treatment, key, bucketing_key, feature_name) + + Telemetry.send_impression(impression) + {feature_name, impression} end) |> Map.new() - mapped_treatments + impressions end def parse_response( diff --git a/lib/split/telemetry.ex b/lib/split/telemetry.ex index 74d9cbe..b4b7a1f 100644 --- a/lib/split/telemetry.ex +++ b/lib/split/telemetry.ex @@ -184,7 +184,7 @@ defmodule Split.Telemetry do * `change_number` - The change number of the treatment. * `timestamp` - The timestamp of the treatment assignment. """ - alias Split.Treatment + alias Split.Impression defstruct span_name: nil, telemetry_span_context: nil, start_time: nil, start_metadata: nil @@ -300,17 +300,10 @@ defmodule Split.Telemetry do @doc """ Emits a telemetry `impression` event when a Split treatment has been evaluated. """ - @spec send_impression(String.t(), String.t(), Treatment.t()) :: :ok - def send_impression(key, feature_name, %Treatment{} = treatment) do + @spec send_impression(Impression.t()) :: :ok + def send_impression(%Impression{} = impression) do :telemetry.execute([@app_name, :impression], %{}, %{ - impression: %Split.Impression{ - key: key, - feature: feature_name, - treatment: treatment.treatment, - label: treatment.label, - change_number: treatment.change_number, - timestamp: treatment.timestamp - } + impression: impression }) end diff --git a/lib/split/treatment.ex b/lib/split/treatment.ex deleted file mode 100644 index 6cea07b..0000000 --- a/lib/split/treatment.ex +++ /dev/null @@ -1,32 +0,0 @@ -defmodule Split.Treatment do - defstruct treatment: "control", - label: nil, - config: nil, - change_number: nil, - timestamp: nil - - @type t :: %__MODULE__{ - treatment: String.t(), - label: String.t() | nil, - config: String.t() | nil, - change_number: integer() | nil, - timestamp: integer() | nil - } - - @spec build_from_daemon_response(map()) :: t - def build_from_daemon_response(treatment_payload) do - treatment = treatment_payload["t"] - config = treatment_payload["c"] - label = get_in(treatment_payload, ["l", "l"]) - change_number = get_in(treatment_payload, ["l", "c"]) - timestamp = get_in(treatment_payload, ["l", "m"]) - - %Split.Treatment{ - treatment: treatment, - label: label, - config: config, - change_number: change_number, - timestamp: timestamp - } - end -end diff --git a/lib/split/treatment_with_config.ex b/lib/split/treatment_with_config.ex new file mode 100644 index 0000000..cac3bdd --- /dev/null +++ b/lib/split/treatment_with_config.ex @@ -0,0 +1,9 @@ +defmodule Split.TreatmentWithConfig do + defstruct treatment: "control", + config: nil + + @type t :: %__MODULE__{ + treatment: String.t(), + config: String.t() | nil + } +end diff --git a/test/rpc/fallback_test.exs b/test/rpc/fallback_test.exs index b764328..8a23758 100644 --- a/test/rpc/fallback_test.exs +++ b/test/rpc/fallback_test.exs @@ -3,7 +3,7 @@ defmodule Split.RPC.FallbackTest do alias Split.RPC.Fallback alias Split.RPC.Message - alias Split.Treatment + alias Split.Impression doctest Split.RPC.Fallback end diff --git a/test/rpc/response_parser_test.exs b/test/rpc/response_parser_test.exs index 018256d..ea1e5bb 100644 --- a/test/rpc/response_parser_test.exs +++ b/test/rpc/response_parser_test.exs @@ -5,7 +5,7 @@ defmodule Split.RPC.ResponseParserTest do alias Split.RPC.Fallback alias Split.RPC.ResponseParser alias Split.RPC.Message - alias Split.Treatment + alias Split.Impression alias Split.SplitView import ExUnit.CaptureLog @@ -25,7 +25,10 @@ defmodule Split.RPC.ResponseParserTest do }} assert ResponseParser.parse_response(response, message) == - %Treatment{ + %Impression{ + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name", change_number: 123, config: nil, label: "test label", @@ -52,7 +55,10 @@ defmodule Split.RPC.ResponseParserTest do }} assert ResponseParser.parse_response(response, message) == - %Treatment{ + %Impression{ + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name", change_number: 123, config: "{\"foo\": \"bar\"}", label: "test label", @@ -81,14 +87,20 @@ defmodule Split.RPC.ResponseParserTest do assert ResponseParser.parse_response(response, message) == %{ - "feature_name1" => %Split.Treatment{ + "feature_name1" => %Impression{ + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name1", treatment: "on", label: "test label 1", config: nil, change_number: 123, timestamp: 1_723_742_604 }, - "feature_name2" => %Split.Treatment{ + "feature_name2" => %Impression{ + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name2", treatment: "off", label: "test label 2", config: nil, @@ -126,14 +138,20 @@ defmodule Split.RPC.ResponseParserTest do assert ResponseParser.parse_response(response, message) == %{ - "feature_name1" => %Split.Treatment{ + "feature_name1" => %Impression{ + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name1", treatment: "on", label: "test label 1", config: "{\"foo\": \"bar\"}", change_number: 123, timestamp: 1_723_742_604 }, - "feature_name2" => %Split.Treatment{ + "feature_name2" => %Impression{ + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name2", treatment: "off", label: "test label 2", config: "{\"baz\": \"qux\"}", @@ -169,14 +187,20 @@ defmodule Split.RPC.ResponseParserTest do assert ResponseParser.parse_response(response, message) == %{ - "feature_name1" => %Split.Treatment{ + "feature_name1" => %Impression{ + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name1", treatment: "on", label: "test label 1", config: nil, change_number: 123, timestamp: 1_723_742_604 }, - "feature_name2" => %Split.Treatment{ + "feature_name2" => %Impression{ + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name2", treatment: "off", label: "test label 2", config: nil, @@ -214,14 +238,20 @@ defmodule Split.RPC.ResponseParserTest do assert ResponseParser.parse_response(response, message) == %{ - "feature_name1" => %Split.Treatment{ + "feature_name1" => %Impression{ + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name1", treatment: "on", label: "test label 1", config: "{\"foo\": \"bar\"}", change_number: 123, timestamp: 1_723_742_604 }, - "feature_name2" => %Split.Treatment{ + "feature_name2" => %Impression{ + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name2", treatment: "off", label: "test label 2", config: "{\"baz\": \"qux\"}", @@ -257,14 +287,20 @@ defmodule Split.RPC.ResponseParserTest do assert ResponseParser.parse_response(response, message) == %{ - "feature_name1" => %Split.Treatment{ + "feature_name1" => %Impression{ + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name1", treatment: "on", label: "test label 1", config: nil, change_number: 123, timestamp: 1_723_742_604 }, - "feature_name2" => %Split.Treatment{ + "feature_name2" => %Impression{ + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name2", treatment: "off", label: "test label 2", config: nil, @@ -302,14 +338,20 @@ defmodule Split.RPC.ResponseParserTest do assert ResponseParser.parse_response(response, message) == %{ - "feature_name1" => %Split.Treatment{ + "feature_name1" => %Impression{ + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name1", treatment: "on", label: "test label 1", config: "{\"foo\": \"bar\"}", change_number: 123, timestamp: 1_723_742_604 }, - "feature_name2" => %Split.Treatment{ + "feature_name2" => %Impression{ + key: "user_key", + bucketing_key: "bucketing_key", + feature: "feature_name2", treatment: "off", label: "test label 2", config: "{\"baz\": \"qux\"}", @@ -524,7 +566,7 @@ defmodule Split.RPC.ResponseParserTest do assert capture_log(fn -> assert ResponseParser.parse_response(response, message) == - %Split.Treatment{ + %Impression{ change_number: nil, config: nil, label: "exception", @@ -545,7 +587,7 @@ defmodule Split.RPC.ResponseParserTest do assert capture_log(fn -> assert ResponseParser.parse_response(response, message) == %{ - "feature_name1" => %Split.Treatment{ + "feature_name1" => %Impression{ treatment: "control", label: "exception", config: nil, diff --git a/test/split/impression_test.exs b/test/split/impression_test.exs new file mode 100644 index 0000000..06ce010 --- /dev/null +++ b/test/split/impression_test.exs @@ -0,0 +1,51 @@ +defmodule Split.ImpressionTest do + use ExUnit.Case + + alias Split.Impression + + describe "build_from_daemon_response/1" do + test "builds an impression struct from a daemon response" do + treatment_payload = %{ + "t" => "treatment", + "c" => "{\"field\": \"value\"}", + "l" => %{ + "l" => "label", + "c" => 1, + "m" => 2 + } + } + + expected = %Impression{ + key: 'user_key', + bucketing_key: 'bucketing_key', + feature: 'feature_name', + treatment: "treatment", + label: "label", + config: "{\"field\": \"value\"}", + change_number: 1, + timestamp: 2 + } + + 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 + treatment_payload = %{ + "t" => "treatment" + } + + expected = %Impression{ + key: 'user_key', + bucketing_key: nil, + feature: 'feature_name', + treatment: "treatment", + label: nil, + config: nil, + change_number: nil, + timestamp: nil + } + + assert expected == Impression.build_from_daemon_response(treatment_payload, 'user_key', nil, 'feature_name') + end + end +end diff --git a/test/split/treatment_test.exs b/test/split/treatment_test.exs deleted file mode 100644 index da4c369..0000000 --- a/test/split/treatment_test.exs +++ /dev/null @@ -1,45 +0,0 @@ -defmodule Split.TreatmentTest do - use ExUnit.Case - - alias Split.Treatment - - describe "build_from_daemon_response/1" do - test "builds a treatment struct from a daemon response" do - treatment_payload = %{ - "t" => "treatment", - "c" => nil, - "l" => %{ - "l" => "label", - "c" => 1, - "m" => 2 - } - } - - expected = %Treatment{ - treatment: "treatment", - label: "label", - config: nil, - change_number: 1, - timestamp: 2 - } - - assert expected == Treatment.build_from_daemon_response(treatment_payload) - end - - test "builds a treatment struct with nil values" do - treatment_payload = %{ - "t" => "treatment" - } - - expected = %Treatment{ - treatment: "treatment", - label: nil, - config: nil, - change_number: nil, - timestamp: nil - } - - assert expected == Treatment.build_from_daemon_response(treatment_payload) - end - end -end diff --git a/test/split_test.exs b/test/split_test.exs index cf0c0ba..a402969 100644 --- a/test/split_test.exs +++ b/test/split_test.exs @@ -2,7 +2,7 @@ defmodule SplitThinElixirTest do use ExUnit.Case alias Split.Impression - alias Split.Treatment + alias Split.TreatmentWithConfig alias Split.SplitView setup_all context do @@ -43,7 +43,7 @@ defmodule SplitThinElixirTest do describe "get_treatment_with_config/3" do test "returns expected struct" do - assert %Treatment{treatment: "on", config: %{"foo" => "bar"}} = + assert %TreatmentWithConfig{treatment: "on", config: %{"foo" => "bar"}} = Split.get_treatment_with_config( "user-id-" <> to_string(Enum.random(1..100_000)), "ethan_test" @@ -81,7 +81,7 @@ defmodule SplitThinElixirTest do describe "get_treatments_with_config/3" do test "returns expected struct" do - assert %{"ethan_test" => %Treatment{treatment: "on", config: %{"foo" => "bar"}}} = + assert %{"ethan_test" => %TreatmentWithConfig{treatment: "on", config: %{"foo" => "bar"}}} = Split.get_treatments_with_config( "user-id-" <> to_string(Enum.random(1..100_000)), [ @@ -124,7 +124,7 @@ defmodule SplitThinElixirTest do describe "get_treatments_with_config_by_flag_set/3" do test "returns expected struct" do - assert %{"emi_test" => %Treatment{treatment: "on", config: %{"foo" => "bar"}}} = + assert %{"emi_test" => %TreatmentWithConfig{treatment: "on", config: %{"foo" => "bar"}}} = Split.get_treatments_with_config_by_flag_set( "user-id-" <> to_string(Enum.random(1..100_000)), "flag_set_name" @@ -167,7 +167,7 @@ defmodule SplitThinElixirTest do describe "get_treatments_with_config_by_flag_sets/3" do test "returns expected struct" do - assert %{"emi_test" => %Treatment{treatment: "on", config: %{"foo" => "bar"}}} = + assert %{"emi_test" => %TreatmentWithConfig{treatment: "on", config: %{"foo" => "bar"}}} = Split.get_treatments_with_config_by_flag_sets( "user-id-" <> to_string(Enum.random(1..100_000)), [ From 4434391bb618d1461d5c4fd23faa078a3b817a30 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 6 Feb 2025 18:17:13 -0300 Subject: [PATCH 26/46] Update changelog entry --- CHANGES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.txt b/CHANGES.txt index 0f2759e..80d48e7 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,6 +2,7 @@ - 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. + - 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}`). From 215cc35b5f2679c9d47c6574caa8524068b10e9f Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 7 Feb 2025 12:05:02 -0300 Subject: [PATCH 27/46] Polishing --- lib/split.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/split.ex b/lib/split.ex index e41e74f..ad29534 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -49,10 +49,10 @@ defmodule Split do Split.get_treatment("user_key", "feature_name") ``` """ - alias Split.SplitView alias Split.Telemetry - alias Split.TreatmentWithConfig alias Split.Sockets.Pool + alias Split.TreatmentWithConfig + alias Split.SplitView alias Split.RPC.Message alias Split.RPC.ResponseParser From 744c28972bf073903f445710e116ce023403029a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 7 Feb 2025 12:13:33 -0300 Subject: [PATCH 28/46] 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 29/46] 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 8ca4d64d11a4bb9f73461d92adbc81616deb5d53 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 7 Feb 2025 13:05:02 -0300 Subject: [PATCH 30/46] rc --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 1184023..9166947 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule SplitThinElixir.MixProject do def project do [ app: :split, - version: "0.1.0", + version: "0.2.0-rc.0", elixir: "~> 1.14", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, From 44738a15954db6aed78415172b65cd3b9c6bc8ec Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 7 Feb 2025 18:25:43 -0300 Subject: [PATCH 31/46] 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 32/46] 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 33/46] 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 34/46] 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 35/46] 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 36/46] 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 37/46] 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 38/46] 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 39/46] 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 40/46] 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, From e2364834a03c24a27104e99f0b0482825af4cbd1 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 14 Feb 2025 11:59:11 -0300 Subject: [PATCH 41/46] refactor: simplify start_link function by removing map clause --- lib/split.ex | 2 +- lib/split/sockets/pool.ex | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/split.ex b/lib/split.ex index 6380993..17891b7 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -32,7 +32,7 @@ defmodule Split do `Split` takes a number of keyword arguments as options when starting. The following options are available: - - `:socket_path`: **REQUIRED** The path to the splitd socket file. For example `/var/run/splitd.sock`. + - `:socket_path`: **OPTIONAL** The path to the splitd socket file. Default is `/var/run/splitd.sock`. - `:pool_size`: **OPTIONAL** The size of the pool of connections to the splitd daemon. Default is the number of online schedulers in the Erlang VM (See: https://www.erlang.org/doc/apps/erts/erl_cmd.html). - `:connect_timeout`: **OPTIONAL** The timeout in milliseconds to connect to the splitd daemon. Default is `1000`. diff --git a/lib/split/sockets/pool.ex b/lib/split/sockets/pool.ex index 7eb7328..328b812 100644 --- a/lib/split/sockets/pool.ex +++ b/lib/split/sockets/pool.ex @@ -16,11 +16,7 @@ defmodule Split.Sockets.Pool do } end - def start_link(opts) when is_map(opts) do - start_link(Map.to_list(opts)) - end - - def start_link(opts) when is_list(opts) do + def start_link(opts) do socket_path = Keyword.get(opts, :socket_path, "/var/run/splitd.sock") pool_name = Keyword.get(opts, :pool_name, __MODULE__) pool_size = Keyword.get(opts, :pool_size, System.schedulers_online()) From facd45e1f5a8bd7a0ccda2f592e780b25cce6d84 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 14 Feb 2025 12:05:04 -0300 Subject: [PATCH 42/46] Update CHANGELOG entry --- CHANGES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.txt b/CHANGES.txt index eaff78b..fb909ee 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,6 @@ 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`. + - Updated the `:socket_path` option for `Split.Supervisor.start_link/1` to be optional, defaulting to `"/var/run/splitd.sock"`. - 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. - Renamed the `Split.Treatment` struct to `Split.TreatmentWithConfig` and removed the `label`, `change_number`, and `timestamp` fields. From 02ad93cc1f476d8dcd3ef8f181e459f4be83cf85 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 14 Feb 2025 13:02:40 -0300 Subject: [PATCH 43/46] Update start_link function --- lib/split/supervisor.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/split/supervisor.ex b/lib/split/supervisor.ex index 999e182..41ff8cf 100644 --- a/lib/split/supervisor.ex +++ b/lib/split/supervisor.ex @@ -11,8 +11,9 @@ defmodule Split.Supervisor do {:ok, init_arg} end + @spec start_link() :: Supervisor.on_start() @spec start_link(Split.options()) :: Supervisor.on_start() - def start_link(opts) do + def start_link(opts \\ []) do child = {Pool, opts} Supervisor.start_link([child], strategy: :one_for_one) end From e59bed4bff8c6c45bc80283900c82adf967f50f6 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 14 Feb 2025 14:27:10 -0300 Subject: [PATCH 44/46] Fix optional arguments in method specs --- lib/split.ex | 46 +++++++++++++++++++++++++++++++--------- lib/split/rpc/message.ex | 6 ++++-- 2 files changed, 40 insertions(+), 12 deletions(-) diff --git a/lib/split.ex b/lib/split.ex index 17891b7..ed66c32 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -98,7 +98,8 @@ defmodule Split do iex> Split.get_treatment("user_id", "located_in_usa", %{country: "USA"}) "on" """ - @spec get_treatment(split_key(), String.t(), attributes() | nil) :: String.t() + @spec get_treatment(split_key(), String.t()) :: String.t() + @spec get_treatment(split_key(), String.t(), attributes()) :: String.t() def get_treatment(key, feature_name, attributes \\ %{}) do request = Message.get_treatment( @@ -120,7 +121,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(), attributes() | nil) :: + @spec get_treatment_with_config(split_key(), String.t()) :: TreatmentWithConfig.t() + @spec get_treatment_with_config(split_key(), String.t(), attributes()) :: TreatmentWithConfig.t() def get_treatment_with_config(key, feature_name, attributes \\ %{}) do request = @@ -143,7 +145,10 @@ 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()], attributes() | nil) :: %{ + @spec get_treatments(split_key(), [String.t()]) :: %{ + String.t() => String.t() + } + @spec get_treatments(split_key(), [String.t()], attributes()) :: %{ String.t() => String.t() } def get_treatments(key, feature_names, attributes \\ %{}) do @@ -167,7 +172,10 @@ 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()], attributes() | nil) :: %{ + @spec get_treatments_with_config(split_key(), [String.t()]) :: %{ + String.t() => TreatmentWithConfig.t() + } + @spec get_treatments_with_config(split_key(), [String.t()], attributes()) :: %{ String.t() => TreatmentWithConfig.t() } def get_treatments_with_config(key, feature_names, attributes \\ %{}) do @@ -191,7 +199,10 @@ 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(), attributes() | nil) :: %{ + @spec get_treatments_by_flag_set(split_key(), String.t()) :: %{ + String.t() => String.t() + } + @spec get_treatments_by_flag_set(split_key(), String.t(), attributes()) :: %{ String.t() => String.t() } def get_treatments_by_flag_set(key, flag_set_name, attributes \\ %{}) do @@ -215,10 +226,15 @@ defmodule Split do 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() + ) :: + %{String.t() => TreatmentWithConfig.t()} @spec get_treatments_with_config_by_flag_set( split_key(), String.t(), - attributes() | nil + attributes() ) :: %{String.t() => TreatmentWithConfig.t()} def get_treatments_with_config_by_flag_set( @@ -246,7 +262,9 @@ 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()], attributes() | nil) :: + @spec get_treatments_by_flag_sets(split_key(), [String.t()]) :: + %{String.t() => String.t()} + @spec get_treatments_by_flag_sets(split_key(), [String.t()], attributes()) :: %{String.t() => String.t()} def get_treatments_by_flag_sets( key, @@ -273,10 +291,15 @@ defmodule Split do 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()] + ) :: + %{String.t() => TreatmentWithConfig.t()} @spec get_treatments_with_config_by_flag_sets( split_key(), [String.t()], - attributes() | nil + attributes() ) :: %{String.t() => TreatmentWithConfig.t()} def get_treatments_with_config_by_flag_sets( @@ -306,11 +329,14 @@ defmodule Split do true iex> Split.track("user_id", "user", "my-event", 42) true + iex> Split.track("user_id", "user", "my-event", nil, %{property1: "value1"}) + true iex> Split.track("user_id", "user", "my-event", 42, %{property1: "value1"}) true """ - @spec track(split_key(), String.t(), String.t(), number() | nil, properties() | nil) :: - boolean() + @spec track(split_key(), String.t(), String.t()) :: boolean() + @spec track(split_key(), String.t(), String.t(), number() | nil) :: boolean() + @spec track(split_key(), String.t(), String.t(), number() | nil, properties()) :: 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/message.ex b/lib/split/rpc/message.ex index c0584d7..3a74147 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, Split.attributes() | nil} + | {:attributes, Split.attributes()} @type get_treatments_args :: {:key, Split.split_key()} | {:feature_names, list(String.t())} - | {:attributes, Split.attributes() | nil} + | {:attributes, Split.attributes()} @doc """ Builds a message to register a client in splitd. @@ -301,6 +301,8 @@ 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(Split.split_key(), String.t(), String.t()) :: t() + @spec track(Split.split_key(), String.t(), String.t(), number() | nil) :: 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 From f451ffdb2078e1439bd66dc26a4f211942be636a Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 14 Feb 2025 14:27:24 -0300 Subject: [PATCH 45/46] rc --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 2caeccf..647c50d 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule SplitThinElixir.MixProject do def project do [ app: :split, - version: "0.2.0", + version: "0.2.0-rc.1", elixir: "~> 1.14", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, From 09a1456d32e39350dd4a388f98d8715731805e7d Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Fri, 14 Feb 2025 16:20:36 -0300 Subject: [PATCH 46/46] stable version --- README.md | 2 +- lib/split.ex | 2 +- mix.exs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9d2d47a..d0a9e81 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ by adding `split_thin_elixir` to your list of dependencies in `mix.exs`: ```elixir def deps do [ - {:split, "~> 0.1.0", hex: :split_thin_sdk} + {:split, "~> 0.2.0", hex: :split_thin_sdk} ] end ``` diff --git a/lib/split.ex b/lib/split.ex index ed66c32..94c2569 100644 --- a/lib/split.ex +++ b/lib/split.ex @@ -32,7 +32,7 @@ defmodule Split do `Split` takes a number of keyword arguments as options when starting. The following options are available: - - `:socket_path`: **OPTIONAL** The path to the splitd socket file. Default is `/var/run/splitd.sock`. + - `:socket_path`: **OPTIONAL** The path to the splitd socket file. Default is `"/var/run/splitd.sock"`. - `:pool_size`: **OPTIONAL** The size of the pool of connections to the splitd daemon. Default is the number of online schedulers in the Erlang VM (See: https://www.erlang.org/doc/apps/erts/erl_cmd.html). - `:connect_timeout`: **OPTIONAL** The timeout in milliseconds to connect to the splitd daemon. Default is `1000`. diff --git a/mix.exs b/mix.exs index 647c50d..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.1", + version: "0.2.0", elixir: "~> 1.14", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod,