Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/ldclient_config.erl
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@
datasource => poll | stream | file | testdata | undefined,
http_options => http_options(),
stream_initial_retry_delay_ms => non_neg_integer(),
application => app_info()
application => app_info(),
instance_id => binary()
}.
% Settings stored for each running SDK instance

Expand Down Expand Up @@ -203,6 +204,14 @@ parse_options(SdkKey, Options) when is_list(SdkKey), is_map(Options) ->
HttpOptions = parse_http_options(maps:get(http_options, Options, undefined)),
AppInfo = parse_application_info(maps:get(application, Options, ?APPLICATION_DEFAULT_OPTIONS)),
RedisTls = maps:get(redis_tls, Options, ?DEFAULT_REDIS_TLS),
%% Per SCMP-server-connection-minutes-polling, each SDK instance gets a
%% stable v4 UUID that is sent as the X-LaunchDarkly-Instance-Id header on
%% every outbound request (polling, streaming, and events). It is
%% generated once here in parse_options/2, which is called exactly once
%% per ldclient_instance:start/3, and then stored in the per-instance
%% settings so ldclient_headers can pick it up alongside the other
%% default headers.
InstanceId = uuid:uuid_to_string(uuid:get_v4(), binary_standard),
#{
sdk_key => SdkKey,
base_uri => BaseUri,
Expand Down Expand Up @@ -237,7 +246,8 @@ parse_options(SdkKey, Options) when is_list(SdkKey), is_map(Options) ->
testdata_tag => TestDataTag,
datasource => DataSource,
stream_initial_retry_delay_ms => StreamInitialRetryDelayMs,
application => AppInfo
application => AppInfo,
instance_id => InstanceId
}.

%% @doc Get all registered tags
Expand Down
8 changes: 7 additions & 1 deletion src/ldclient_headers.erl
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ get_default_headers(Tag, _Format = string_pairs) ->
get_default_headers(Tag) ->
with_tags(Tag, #{
<<"authorization">> => list_to_binary(ldclient_config:get_value(Tag, sdk_key)),
<<"user-agent">> => list_to_binary(ldclient_config:get_user_agent())
<<"user-agent">> => list_to_binary(ldclient_config:get_user_agent()),
%% Per SCMP-server-connection-minutes-polling, every outbound request
%% carries a per-instance v4 UUID. It is generated once per SDK
%% instance in ldclient_config:parse_options/2 and stored under the
%% instance_id key in the instance settings, so this lookup returns
%% the same value for the lifetime of the instance.
<<"x-launchdarkly-instance-id">> => ldclient_config:get_value(Tag, instance_id)
}).

%% @doc Append the tags header to the given map.
Expand Down
3 changes: 2 additions & 1 deletion test-service/src/ts_service_request_handler.erl
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ get_service_detail(Req, State) ->
<<"tls:skip-verify-peer">>,
<<"tls:verify-peer">>,
<<"client-prereq-events">>,
<<"all-flags-client-side-only">>
<<"all-flags-client-side-only">>,
<<"instance-id">>
],
<<"clientVersion">> => ldclient_config:get_version()
}),
Expand Down
84 changes: 82 additions & 2 deletions test/ldclient_headers_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
-export([
can_sort_tags/1,
can_combine_tags/1,
can_get_default_headers/1
can_get_default_headers/1,
instance_id_header_is_uuid_v4/1,
instance_id_header_is_stable_across_calls/1,
instance_id_header_differs_between_instances/1,
instance_id_header_in_string_pairs_format/1
]).

%%====================================================================
Expand All @@ -24,7 +28,11 @@ all() ->
[
can_sort_tags,
can_combine_tags,
can_get_default_headers
can_get_default_headers,
instance_id_header_is_uuid_v4,
instance_id_header_is_stable_across_calls,
instance_id_header_differs_between_instances,
instance_id_header_in_string_pairs_format
].

init_per_suite(Config) ->
Expand Down Expand Up @@ -135,3 +143,75 @@ can_get_default_headers(_) ->
} = ldclient_headers:get_default_headers(empty_application),

application:stop(ldclient).

instance_id_header_is_uuid_v4(_) ->
%% Every call to get_default_headers must produce an X-LaunchDarkly-Instance-Id header
%% containing a parseable v4 UUID. This is the spec contract for
%% SCMP-server-connection-minutes-polling section 1.1.
{ok, _} = application:ensure_all_started(ldclient),
ldclient:start_instance("an-sdk-key", instance_id_v4, #{
stream => false,
polling_update_requestor => ldclient_update_requestor_test
}),
Headers = ldclient_headers:get_default_headers(instance_id_v4),
InstanceId = maps:get(<<"x-launchdarkly-instance-id">>, Headers),
true = is_binary(InstanceId),
%% A standard UUID string is 36 characters: 8-4-4-4-12 hex with dashes.
36 = byte_size(InstanceId),
%% uuid:is_v4/1 takes the binary uuid representation. Parse the string
%% form back to that representation before validating the version bits.
Parsed = uuid:string_to_uuid(binary_to_list(InstanceId)),
true = uuid:is_v4(Parsed),
application:stop(ldclient).

instance_id_header_is_stable_across_calls(_) ->
%% The GUID must remain constant throughout the lifetime of the SDK
%% instance, so two successive calls to get_default_headers for the same
%% tag must yield the same value.
{ok, _} = application:ensure_all_started(ldclient),
ldclient:start_instance("an-sdk-key", instance_id_stable, #{
stream => false,
polling_update_requestor => ldclient_update_requestor_test
}),
H1 = ldclient_headers:get_default_headers(instance_id_stable),
H2 = ldclient_headers:get_default_headers(instance_id_stable),
Id1 = maps:get(<<"x-launchdarkly-instance-id">>, H1),
Id2 = maps:get(<<"x-launchdarkly-instance-id">>, H2),
Id1 = Id2,
application:stop(ldclient).

instance_id_header_differs_between_instances(_) ->
%% Different SDK instances must get different GUIDs.
{ok, _} = application:ensure_all_started(ldclient),
ldclient:start_instance("an-sdk-key", instance_id_a, #{
stream => false,
polling_update_requestor => ldclient_update_requestor_test
}),
ldclient:start_instance("an-sdk-key", instance_id_b, #{
stream => false,
polling_update_requestor => ldclient_update_requestor_test
}),
IdA = maps:get(<<"x-launchdarkly-instance-id">>,
ldclient_headers:get_default_headers(instance_id_a)),
IdB = maps:get(<<"x-launchdarkly-instance-id">>,
ldclient_headers:get_default_headers(instance_id_b)),
true = IdA =/= IdB,
application:stop(ldclient).

instance_id_header_in_string_pairs_format(_) ->
%% The string_pairs format is used by httpc-based clients (polling and
%% events). Confirm the instance id header survives the binary->string
%% conversion and the value is still a 36-character UUID string.
{ok, _} = application:ensure_all_started(ldclient),
ldclient:start_instance("an-sdk-key", instance_id_string_pairs, #{
stream => false,
polling_update_requestor => ldclient_update_requestor_test
}),
Pairs = ldclient_headers:get_default_headers(instance_id_string_pairs, string_pairs),
{value, {_, InstanceIdStr}} = lists:search(
fun({K, _V}) -> K =:= "x-launchdarkly-instance-id" end,
Pairs),
true = is_list(InstanceIdStr),
36 = length(InstanceIdStr),
true = uuid:is_v4(uuid:string_to_uuid(InstanceIdStr)),
application:stop(ldclient).
49 changes: 49 additions & 0 deletions test/ldclient_update_requestor_httpc_SUITE.erl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
authorization_header_set_on_request/1,
user_agent_header_set_on_request/1,
event_schema_set_on_request/1,
instance_id_header_set_on_request/1,
instance_id_header_stable_across_requests/1,
instance_id_header_differs_between_instances/1,
none_match_is_not_set_with_empty_state/1,
none_match_is_set_with_state/1,
etag_response_recorded/1,
Expand All @@ -30,6 +33,9 @@ all() ->
authorization_header_set_on_request,
user_agent_header_set_on_request,
event_schema_set_on_request,
instance_id_header_set_on_request,
instance_id_header_stable_across_requests,
instance_id_header_differs_between_instances,
none_match_is_not_set_with_empty_state,
none_match_is_set_with_state,
etag_response_recorded,
Expand Down Expand Up @@ -157,3 +163,46 @@ custom_headers_appended(_Config) ->
%% The custom headers are there as well.
"String" = bookish_spork_request:header(Request, "basic-string-header"),
"Binary" = bookish_spork_request:header(Request, "binary-string-header").

%% End-to-end check that polling requests carry the spec-required
%% X-LaunchDarkly-Instance-Id header and that the value is a v4 UUID.
instance_id_header_set_on_request(_Config) ->
State = ldclient_update_requestor_httpc:init(default, "sdk-key"),
bookish_spork:stub_request([200, #{}, <<>>]),
{{ok, <<>>}, _} = ldclient_update_requestor_httpc:all(?MOCK_URI, State),
{ok, Request} = bookish_spork:capture_request(),
InstanceId = bookish_spork_request:header(Request, "x-launchdarkly-instance-id"),
true = is_list(InstanceId),
36 = length(InstanceId),
true = uuid:is_v4(uuid:string_to_uuid(InstanceId)).

%% The instance id must remain constant across multiple polling requests
%% for the same SDK instance.
instance_id_header_stable_across_requests(_Config) ->
State0 = ldclient_update_requestor_httpc:init(default, "sdk-key"),
bookish_spork:stub_request([200, #{}, <<>>]),
{{ok, <<>>}, State1} = ldclient_update_requestor_httpc:all(?MOCK_URI, State0),
{ok, Req1} = bookish_spork:capture_request(),
bookish_spork:stub_request([200, #{}, <<>>]),
{{ok, <<>>}, _} = ldclient_update_requestor_httpc:all(?MOCK_URI, State1),
{ok, Req2} = bookish_spork:capture_request(),
Id1 = bookish_spork_request:header(Req1, "x-launchdarkly-instance-id"),
Id2 = bookish_spork_request:header(Req2, "x-launchdarkly-instance-id"),
Id1 = Id2.

%% Different SDK instances (different tags) must produce different
%% instance ids on their polling requests.
instance_id_header_differs_between_instances(_Config) ->
OtherSettings = ldclient_config:parse_options("sdk-key", #{}),
ok = ldclient_config:register(other_instance, OtherSettings),
StateA = ldclient_update_requestor_httpc:init(default, "sdk-key"),
StateB = ldclient_update_requestor_httpc:init(other_instance, "sdk-key"),
bookish_spork:stub_request([200, #{}, <<>>]),
{{ok, <<>>}, _} = ldclient_update_requestor_httpc:all(?MOCK_URI, StateA),
{ok, ReqA} = bookish_spork:capture_request(),
bookish_spork:stub_request([200, #{}, <<>>]),
{{ok, <<>>}, _} = ldclient_update_requestor_httpc:all(?MOCK_URI, StateB),
{ok, ReqB} = bookish_spork:capture_request(),
IdA = bookish_spork_request:header(ReqA, "x-launchdarkly-instance-id"),
IdB = bookish_spork_request:header(ReqB, "x-launchdarkly-instance-id"),
true = IdA =/= IdB.
Loading