Skip to content

Commit 03e2a59

Browse files
authored
feat: Add support for hooks. (#493)
There are two primary aspects to this PR. The first is support for hooks, and the second is support for a Go context like addition to the variation API. The reason for this addition is that it may not be possible in all environments to automatically get the current span parent using OTEL. The default context management strategy in OTEL uses thread-local storage, which can cause problems when an async framework is handling multiple requests in a single thread. In that scenario the application manually determines the parent-child relationship (unless they have async aware custom context management). BEGIN_COMMIT_OVERRIDE feat: Add support for hooks. fix: Discard track events when the associated context is invalid. END_COMMIT_OVERRIDE <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduce a full hooks system (with hook context) across C++ and C APIs, integrate it into evaluations/tracking, add WithHookContext overloads, drop invalid-context track events, and update build/deps with extensive tests. > > - **Hooks framework (C++ core)**: > - Add `hooks::Hook`, `HookContext`, `EvaluationSeriesContext/Data(+Builder)`, `TrackSeriesContext` and `hook_executor` for before/after evaluation and after track. > - Wire hooks into `ClientImpl` variation/track paths with method names; execute hooks synchronously. > - Extend `Config`/`ConfigBuilder` to accept `std::shared_ptr<hooks::Hook>` list. > - **C++ API**: > - Add `Track` and all `*Variation*` overloads accepting `hooks::HookContext`. > - **C bindings**: > - New headers/APIs: `hook.h`, `hook_context.h`, `evaluation_series_context.h`, `evaluation_series_data.h`, `track_series_context.h`. > - Register hooks via `LDServerConfigBuilder_Hooks`; bridge via `CHookWrapper`. > - Add `*_WithHookContext` functions for track and all variation variants. > - **Events behavior**: > - Discard track events when `Context` is invalid; log warning. > - **Build/Deps**: > - Include new hook sources in `CMakeLists.txt`. > - Update `redis-plus-plus` FetchContent tag and disable shallow clone. > - **Tests**: > - Add comprehensive C++ and C binding tests for hook ordering, data passing, error handling, and contexts. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 4773e9e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 53e5341 commit 03e2a59

32 files changed

+5232
-35
lines changed

cmake/redis-plus-plus.cmake

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ set(REDIS_PLUS_PLUS_BUILD_TEST OFF CACHE BOOL "" FORCE)
2424
FetchContent_Declare(redis-plus-plus
2525
GIT_REPOSITORY https://github.com/sewenew/redis-plus-plus.git
2626
# Post 1.3.15. Required to support FetchContent post 1.3.7 where it was broken.
27-
GIT_TAG 84f37e95d9112193fd433f65402d3d183f0b9cf7
28-
GIT_SHALLOW TRUE
27+
GIT_TAG fc67c2ebf929ae2cf3b31d959767233f39c5df6a
28+
GIT_SHALLOW FALSE
2929
)
3030

3131
FetchContent_MakeAvailable(redis-plus-plus)

libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#include <launchdarkly/server_side/bindings/c/config/config.h>
77
#include <launchdarkly/server_side/bindings/c/config/lazy_load_builder/lazy_load_builder.h>
8+
#include <launchdarkly/server_side/bindings/c/hook.h>
89

910
#include <launchdarkly/bindings/c/config/logging_builder.h>
1011
#include <launchdarkly/bindings/c/export.h>
@@ -514,6 +515,25 @@ LD_EXPORT(void)
514515
LDServerConfigBuilder_Logging_Custom(LDServerConfigBuilder b,
515516
LDLoggingCustomBuilder custom_builder);
516517

518+
/**
519+
* Registers a hook with the SDK.
520+
*
521+
* Hooks allow you to instrument SDK behavior for logging, analytics,
522+
* or distributed tracing (e.g. OpenTelemetry).
523+
*
524+
* Multiple hooks can be registered. They execute in the order registered.
525+
*
526+
* LIFETIME: The hook struct is copied during this call. The Name string
527+
* must remain valid until LDServerConfigBuilder_Build() is called.
528+
* UserData and function pointers must remain valid for the SDK lifetime.
529+
*
530+
* @param builder Configuration builder. Must not be NULL.
531+
* @param hook Hook to register. The struct is copied. Must not be NULL.
532+
*/
533+
LD_EXPORT(void)
534+
LDServerConfigBuilder_Hooks(LDServerConfigBuilder builder,
535+
struct LDServerSDKHook hook);
536+
517537
/**
518538
* Creates an LDClientConfig. The builder is automatically freed.
519539
*
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* @file evaluation_series_context.h
3+
* @brief C bindings for read-only evaluation context passed to hooks.
4+
*
5+
* EvaluationSeriesContext provides information about a flag evaluation
6+
* to hook callbacks. All data is read-only and valid only during the
7+
* callback execution.
8+
*
9+
* LIFETIME:
10+
* - All context parameters are temporary - valid only during callback
11+
* - Do not store pointers to context data
12+
* - Copy any needed data (strings, values) if you need to retain it
13+
*/
14+
15+
#pragma once
16+
17+
#include <launchdarkly/bindings/c/export.h>
18+
#include <launchdarkly/server_side/bindings/c/hook_context.h>
19+
#include <launchdarkly/bindings/c/value.h>
20+
#include <launchdarkly/bindings/c/context.h>
21+
22+
// No effect in C++, but we want it for C.
23+
// ReSharper disable once CppUnusedIncludeDirective
24+
#include <stdbool.h> // NOLINT(*-deprecated-headers)
25+
26+
#ifdef __cplusplus
27+
extern "C" {
28+
#endif
29+
30+
typedef struct p_LDServerSDKEvaluationSeriesContext* LDServerSDKEvaluationSeriesContext;
31+
32+
/**
33+
* @brief Get the flag key being evaluated.
34+
*
35+
* @param eval_context Evaluation context. Must not be NULL.
36+
* @return Flag key as null-terminated UTF-8 string. Valid only during
37+
* the callback execution. Must not be freed.
38+
*/
39+
LD_EXPORT(char const*)
40+
LDEvaluationSeriesContext_FlagKey(LDServerSDKEvaluationSeriesContext eval_context);
41+
42+
/**
43+
* @brief Get the context (user/organization) being evaluated.
44+
*
45+
* @param eval_context Evaluation context. Must not be NULL.
46+
* @return Context object. Valid only during the callback execution.
47+
* Must not be freed. Do not call LDContext_Free() on this.
48+
*/
49+
LD_EXPORT(LDContext)
50+
LDEvaluationSeriesContext_Context(LDServerSDKEvaluationSeriesContext eval_context);
51+
52+
/**
53+
* @brief Get the default value provided to the variation call.
54+
*
55+
* @param eval_context Evaluation context. Must not be NULL.
56+
* @return Default value. Valid only during the callback execution.
57+
* Must not be freed. Do not call LDValue_Free() on this.
58+
*/
59+
LD_EXPORT(LDValue)
60+
LDEvaluationSeriesContext_DefaultValue(
61+
LDServerSDKEvaluationSeriesContext eval_context);
62+
63+
/**
64+
* @brief Get the name of the variation method called.
65+
*
66+
* Examples: "BoolVariation", "StringVariationDetail", "JsonVariation"
67+
*
68+
* @param eval_context Evaluation context. Must not be NULL.
69+
* @return Method name as null-terminated UTF-8 string. Valid only during
70+
* the callback execution. Must not be freed.
71+
*/
72+
LD_EXPORT(char const*)
73+
LDEvaluationSeriesContext_Method(LDServerSDKEvaluationSeriesContext eval_context);
74+
75+
/**
76+
* @brief Get the hook context provided by the caller.
77+
*
78+
* This contains application-specific data passed to the variation call,
79+
* such as OpenTelemetry span parents.
80+
*
81+
* @param eval_context Evaluation context. Must not be NULL.
82+
* @return Hook context. Valid only during the callback execution.
83+
* Must not be freed. Do not call LDHookContext_Free() on this.
84+
*/
85+
LD_EXPORT(LDHookContext)
86+
LDEvaluationSeriesContext_HookContext(
87+
LDServerSDKEvaluationSeriesContext eval_context);
88+
89+
/**
90+
* @brief Get the environment ID, if available.
91+
*
92+
* The environment ID is only available after SDK initialization completes.
93+
* Returns NULL if not yet available.
94+
*
95+
* @param eval_context Evaluation context. Must not be NULL.
96+
* @return Environment ID as null-terminated UTF-8 string, or NULL if not
97+
* available. Valid only during the callback execution. Must not
98+
* be freed.
99+
*/
100+
LD_EXPORT(char const*)
101+
LDEvaluationSeriesContext_EnvironmentId(
102+
LDServerSDKEvaluationSeriesContext eval_context);
103+
104+
#ifdef __cplusplus
105+
}
106+
#endif
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/**
2+
* @file evaluation_series_data.h
3+
* @brief C bindings for hook data passed between evaluation stages.
4+
*
5+
* EvaluationSeriesData is mutable data that hooks can use to pass information
6+
* from beforeEvaluation to afterEvaluation. This is useful for:
7+
* - Storing timing information
8+
* - Passing span contexts for distributed tracing
9+
* - Accumulating custom metrics
10+
*
11+
* LIFETIME AND OWNERSHIP:
12+
* - Data returned from hook callbacks transfers ownership to the SDK
13+
* - Data received in callbacks can be modified and returned (ownership transfer)
14+
* - Keys are copied by the SDK
15+
* - Values retrieved with GetValue() are temporary - valid only during callback
16+
* - Do not call LDValue_Free() on values retrieved with GetValue()
17+
* - Values stored with SetValue() are copied into the data object
18+
* - Pointers (void*) lifetime is managed by the application
19+
*
20+
* BUILDER PATTERN:
21+
* To modify data, use LDEvaluationSeriesData_NewBuilder() to create a builder,
22+
* make changes, then build a new data object.
23+
*/
24+
25+
#pragma once
26+
27+
#include <launchdarkly/bindings/c/export.h>
28+
#include <launchdarkly/bindings/c/context.h>
29+
30+
// No effect in C++, but we want it for C.
31+
// ReSharper disable once CppUnusedIncludeDirective
32+
#include <stdbool.h> // NOLINT(*-deprecated-headers)
33+
// ReSharper disable once CppUnusedIncludeDirective
34+
#include <stddef.h> // NOLINT(*-deprecated-headers)
35+
36+
#ifdef __cplusplus
37+
extern "C" {
38+
#endif
39+
40+
typedef struct p_LDServerSDKEvaluationSeriesData* LDServerSDKEvaluationSeriesData;
41+
typedef struct p_LDEvaluationSeriesDataBuilder* LDServerSDKEvaluationSeriesDataBuilder;
42+
43+
44+
/**
45+
* @brief Create a new empty evaluation series data.
46+
*
47+
* @return New data object. Must be freed with LDEvaluationSeriesData_Free()
48+
* or transferred to SDK via hook callback return.
49+
*/
50+
LD_EXPORT(LDServerSDKEvaluationSeriesData)
51+
LDEvaluationSeriesData_New(void);
52+
53+
/**
54+
* @brief Get a Value from the evaluation series data.
55+
*
56+
* LIFETIME: Returns a temporary value valid only during the callback.
57+
* Do not call LDValue_Free() on the returned value.
58+
*
59+
* USAGE:
60+
* @code
61+
* LDValue value;
62+
* if (LDEvaluationSeriesData_GetValue(data, "timestamp", &value)) {
63+
* // Use value (valid only during callback)
64+
* double timestamp = LDValue_GetNumber(value);
65+
* // Do NOT call LDValue_Free(value)
66+
* }
67+
* @endcode
68+
*
69+
* @param data Data object. Must not be NULL.
70+
* @param key Key to look up. Must be null-terminated UTF-8 string.
71+
* Must not be NULL.
72+
* @param out_value Pointer to receive the value. Must not be NULL.
73+
* Set to a temporary LDValue (valid only during callback).
74+
* Do not call LDValue_Free() on this value.
75+
* @return true if key was found and contains a Value, false otherwise.
76+
*/
77+
LD_EXPORT(bool)
78+
LDEvaluationSeriesData_GetValue(LDServerSDKEvaluationSeriesData data,
79+
char const* key,
80+
LDValue* out_value);
81+
82+
/**
83+
* @brief Get a pointer from the evaluation series data.
84+
*
85+
* Retrieves a pointer previously stored with
86+
* LDEvaluationSeriesDataBuilder_SetPointer().
87+
*
88+
* USAGE - OpenTelemetry span:
89+
* @code
90+
* void* span;
91+
* if (LDEvaluationSeriesData_GetPointer(data, "span", &span)) {
92+
* // Cast and use span
93+
* MySpan* typed_span = (MySpan*)span;
94+
* }
95+
* @endcode
96+
*
97+
* @param data Data object. Must not be NULL.
98+
* @param key Key to look up. Must be null-terminated UTF-8 string.
99+
* Must not be NULL.
100+
* @param out_pointer Pointer to receive the pointer value. Must not be NULL.
101+
* Set to NULL if key not found.
102+
* @return true if key was found and contains a pointer, false otherwise.
103+
*/
104+
LD_EXPORT(bool)
105+
LDEvaluationSeriesData_GetPointer(LDServerSDKEvaluationSeriesData data,
106+
char const* key,
107+
void** out_pointer);
108+
109+
/**
110+
* @brief Create a builder from existing data.
111+
*
112+
* Creates a builder initialized with the contents of the data object.
113+
* Use this to add or modify entries in the data.
114+
*
115+
* USAGE:
116+
* @code
117+
* LDServerSDKEvaluationSeriesDataBuilder builder =
118+
* LDEvaluationSeriesData_NewBuilder(input_data);
119+
* LDEvaluationSeriesDataBuilder_SetValue(builder, "key", value);
120+
* return LDEvaluationSeriesDataBuilder_Build(builder);
121+
* @endcode
122+
*
123+
* @param data Data to copy into builder. May be NULL (creates empty builder).
124+
* @return Builder object. Must be freed with
125+
* LDEvaluationSeriesDataBuilder_Free() or consumed with
126+
* LDEvaluationSeriesDataBuilder_Build().
127+
*/
128+
LD_EXPORT(LDServerSDKEvaluationSeriesDataBuilder)
129+
LDEvaluationSeriesData_NewBuilder(LDServerSDKEvaluationSeriesData data);
130+
131+
/**
132+
* @brief Free evaluation series data.
133+
*
134+
* Only call this if you created the data and are not returning it from
135+
* a hook callback. Data returned from callbacks is owned by the SDK.
136+
*
137+
* @param data Data to free. May be NULL (no-op).
138+
*/
139+
LD_EXPORT(void)
140+
LDEvaluationSeriesData_Free(LDServerSDKEvaluationSeriesData data);
141+
142+
/**
143+
* @brief Set a Value in the builder.
144+
*
145+
* OWNERSHIP: The value is copied/moved into the builder. You are responsible
146+
* for freeing the original value if needed.
147+
*
148+
* @param builder Builder object. Must not be NULL.
149+
* @param key Key for the value. Must be null-terminated UTF-8 string.
150+
* Must not be NULL. The key is copied.
151+
* @param value Value to store. Must not be NULL. The value is copied/moved.
152+
*/
153+
LD_EXPORT(void)
154+
LDEvaluationSeriesDataBuilder_SetValue(LDServerSDKEvaluationSeriesDataBuilder builder,
155+
char const* key,
156+
LDValue value);
157+
158+
/**
159+
* @brief Set a pointer in the builder.
160+
*
161+
* Stores an application-specific pointer. Useful for storing objects like
162+
* OpenTelemetry spans that need to be passed from beforeEvaluation to
163+
* afterEvaluation.
164+
*
165+
* LIFETIME: The pointer lifetime must extend through the evaluation series
166+
* (from beforeEvaluation through afterEvaluation).
167+
*
168+
* EXAMPLE - OpenTelemetry span:
169+
* @code
170+
* MySpan* span = start_span();
171+
* LDEvaluationSeriesDataBuilder_SetPointer(builder, "span", span);
172+
* @endcode
173+
*
174+
* @param builder Builder object. Must not be NULL.
175+
* @param key Key for the pointer. Must be null-terminated UTF-8 string.
176+
* Must not be NULL. The key is copied.
177+
* @param pointer Pointer to store. May be NULL. Lifetime managed by caller.
178+
*/
179+
LD_EXPORT(void)
180+
LDEvaluationSeriesDataBuilder_SetPointer(
181+
LDServerSDKEvaluationSeriesDataBuilder builder,
182+
char const* key,
183+
void* pointer);
184+
185+
/**
186+
* @brief Build the evaluation series data.
187+
*
188+
* Consumes the builder and creates a data object. After calling this,
189+
* do not call LDEvaluationSeriesDataBuilder_Free() on the builder.
190+
*
191+
* @param builder Builder to consume. Must not be NULL.
192+
* @return Data object. Must be freed with LDEvaluationSeriesData_Free()
193+
* or transferred to SDK via hook callback return.
194+
*/
195+
LD_EXPORT(LDServerSDKEvaluationSeriesData)
196+
LDEvaluationSeriesDataBuilder_Build(LDServerSDKEvaluationSeriesDataBuilder builder);
197+
198+
/**
199+
* @brief Free a builder without building.
200+
*
201+
* Only call this if you did not call LDEvaluationSeriesDataBuilder_Build().
202+
*
203+
* @param builder Builder to free. May be NULL (no-op).
204+
*/
205+
LD_EXPORT(void)
206+
LDEvaluationSeriesDataBuilder_Free(LDServerSDKEvaluationSeriesDataBuilder builder);
207+
208+
#ifdef __cplusplus
209+
}
210+
#endif

0 commit comments

Comments
 (0)