Skip to content

Commit fd0c761

Browse files
authored
feat: add X-LaunchDarkly-Instance-Id header to server SDK (SDK-2353) (#532)
1 parent a0c2790 commit fd0c761

6 files changed

Lines changed: 130 additions & 0 deletions

File tree

contract-tests/server-contract-tests/src/main.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ int main(int argc, char* argv[]) {
5151
srv.add_capability("evaluation-hooks");
5252
srv.add_capability("track-hooks");
5353
srv.add_capability("wrapper");
54+
srv.add_capability("instance-id");
5455

5556
net::signal_set signals{ioc, SIGINT, SIGTERM};
5657

libs/server-sdk/src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ target_sources(${LIBNAME}
2020
client.cpp
2121
client_impl.cpp
2222
data_source_status.cpp
23+
instance_id.hpp
24+
instance_id.cpp
2325
config/config.cpp
2426
config/config_builder.cpp
2527
config/builders/data_system/background_sync_builder.cpp

libs/server-sdk/src/client_impl.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include "data_systems/lazy_load/lazy_load_system.hpp"
66
#include "data_systems/offline.hpp"
77
#include "evaluation/evaluation_stack.hpp"
8+
#include "instance_id.hpp"
89
#include "prereq_event_recorder/prereq_event_recorder.hpp"
910

1011
#include "data_interfaces/system/idata_system.hpp"
@@ -119,6 +120,13 @@ ClientImpl::ClientImpl(Config config, std::string const& version)
119120
.Header("user-agent", "CPPServer/" + version)
120121
.Header("authorization", config.SdkKey())
121122
.Header("x-launchdarkly-tags", config.ApplicationTag())
123+
// Per SCMP-server-connection-minutes-polling, every polling
124+
// request must carry a per-instance GUID v4. We attach it to the
125+
// shared HTTP properties so it's also present on streaming and
126+
// event requests, and we generate it here (once during
127+
// ClientImpl construction) so it remains stable for the lifetime
128+
// of the SDK instance.
129+
.Header(kInstanceIdHeader, MakeInstanceId())
122130
.Build()),
123131
logger_(MakeLogger(config.Logging())),
124132
ioc_(kAsioConcurrencyHint),
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#include "instance_id.hpp"
2+
3+
#include <boost/uuid/uuid.hpp>
4+
#include <boost/uuid/uuid_generators.hpp>
5+
#include <boost/uuid/uuid_io.hpp>
6+
7+
namespace launchdarkly::server_side {
8+
9+
std::string MakeInstanceId() {
10+
// boost::uuids::random_generator emits a version 4 (random) UUID, which is
11+
// what the SCMP spec requires. The generator carries state (an internal RNG
12+
// seeded from system entropy on construction), so we cache it per thread to
13+
// avoid redundant entropy draws on repeated calls.
14+
static thread_local boost::uuids::random_generator generator;
15+
return boost::uuids::to_string(generator());
16+
}
17+
18+
} // namespace launchdarkly::server_side
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#pragma once
2+
3+
#include <string>
4+
5+
namespace launchdarkly::server_side {
6+
7+
/**
8+
* Name of the HTTP header used to identify this SDK instance for the purpose of
9+
* estimating server-connection-minutes when polling. The value is a v4 UUID
10+
* that is generated once per SDK instance and remains constant for the
11+
* lifetime of the client.
12+
*/
13+
inline constexpr char const* kInstanceIdHeader = "X-LaunchDarkly-Instance-Id";
14+
15+
/**
16+
* Generate a fresh v4 UUID suitable for use as the value of the
17+
* X-LaunchDarkly-Instance-Id header. Each call returns a new identifier;
18+
* callers are expected to generate the value exactly once per SDK instance
19+
* and reuse it for the lifetime of that instance.
20+
*
21+
* @return A string formatted as a lowercase v4 UUID, e.g.
22+
* "550e8400-e29b-41d4-a716-446655440000".
23+
*/
24+
[[nodiscard]] std::string MakeInstanceId();
25+
26+
} // namespace launchdarkly::server_side
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#include <gtest/gtest.h>
2+
3+
#include "instance_id.hpp"
4+
5+
#include <launchdarkly/server_side/client.hpp>
6+
#include <launchdarkly/server_side/config/config_builder.hpp>
7+
8+
#include <cstddef>
9+
#include <regex>
10+
#include <set>
11+
#include <string>
12+
13+
using namespace launchdarkly;
14+
using namespace launchdarkly::server_side;
15+
16+
namespace {
17+
18+
// Matches a canonical UUID v4 in lowercase hex:
19+
// xxxxxxxx-xxxx-4xxx-Yxxx-xxxxxxxxxxxx
20+
// where 'Y' is one of 8, 9, a, or b (RFC 4122 variant).
21+
bool IsUuidV4(std::string const& s) {
22+
static std::regex const re(
23+
"^[0-9a-f]{8}-"
24+
"[0-9a-f]{4}-"
25+
"4[0-9a-f]{3}-"
26+
"[89ab][0-9a-f]{3}-"
27+
"[0-9a-f]{12}$");
28+
return std::regex_match(s, re);
29+
}
30+
31+
} // namespace
32+
33+
// Spec: SCMP-server-connection-minutes-polling section 1.1 requires the
34+
// X-LaunchDarkly-Instance-Id value to be a v4 UUID.
35+
TEST(InstanceIdTest, GeneratedValueIsUuidV4) {
36+
auto id = MakeInstanceId();
37+
ASSERT_FALSE(id.empty()) << "MakeInstanceId returned an empty string";
38+
EXPECT_TRUE(IsUuidV4(id))
39+
<< "MakeInstanceId returned " << id << " which is not a v4 UUID";
40+
}
41+
42+
// Each invocation must yield a different value; spec requires "the GUID MUST
43+
// be used uniquely for this purpose".
44+
TEST(InstanceIdTest, GeneratedValuesAreUnique) {
45+
constexpr int kSamples = 100;
46+
std::set<std::string> seen;
47+
for (int i = 0; i < kSamples; ++i) {
48+
auto id = MakeInstanceId();
49+
ASSERT_FALSE(id.empty());
50+
EXPECT_TRUE(seen.insert(id).second)
51+
<< "duplicate UUID emitted from MakeInstanceId: " << id;
52+
}
53+
EXPECT_EQ(seen.size(), static_cast<std::size_t>(kSamples));
54+
}
55+
56+
// The header name constant must match the spec verbatim. This guards against
57+
// accidental renaming (the header name is part of the wire contract).
58+
TEST(InstanceIdTest, HeaderNameMatchesSpec) {
59+
EXPECT_STREQ(kInstanceIdHeader, "X-LaunchDarkly-Instance-Id");
60+
}
61+
62+
// Sanity-check that a Client can be constructed; the integration that the
63+
// instance-id header actually ends up on outbound requests is covered by the
64+
// cross-SDK contract test harness (capability: "instance-id").
65+
TEST(InstanceIdTest, ClientConstructsWithInstanceIdHeader) {
66+
// Building a Client exercises the code path that stamps the instance-id
67+
// header into the shared HttpProperties. We can't observe the header
68+
// directly from the public API, so this test simply asserts that the
69+
// construction succeeds; the spec-level guarantees about the header value
70+
// are exercised by GeneratedValueIsUuidV4 and GeneratedValuesAreUnique,
71+
// and the on-the-wire guarantee is covered by the cross-SDK contract test
72+
// harness (capability: "instance-id").
73+
Client client(ConfigBuilder("sdk-123").Build().value());
74+
EXPECT_NE(client.Version(), nullptr);
75+
}

0 commit comments

Comments
 (0)