diff --git a/cmake/redis-plus-plus.cmake b/cmake/redis-plus-plus.cmake index 79bd2a27d..62b88625a 100644 --- a/cmake/redis-plus-plus.cmake +++ b/cmake/redis-plus-plus.cmake @@ -24,8 +24,8 @@ set(REDIS_PLUS_PLUS_BUILD_TEST OFF CACHE BOOL "" FORCE) FetchContent_Declare(redis-plus-plus GIT_REPOSITORY https://github.com/sewenew/redis-plus-plus.git # Post 1.3.15. Required to support FetchContent post 1.3.7 where it was broken. - GIT_TAG 84f37e95d9112193fd433f65402d3d183f0b9cf7 - GIT_SHALLOW TRUE + GIT_TAG fc67c2ebf929ae2cf3b31d959767233f39c5df6a + GIT_SHALLOW FALSE ) FetchContent_MakeAvailable(redis-plus-plus) diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h index 1e4cbba53..64c0dd698 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/config/builder.h @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -514,6 +515,25 @@ LD_EXPORT(void) LDServerConfigBuilder_Logging_Custom(LDServerConfigBuilder b, LDLoggingCustomBuilder custom_builder); +/** + * Registers a hook with the SDK. + * + * Hooks allow you to instrument SDK behavior for logging, analytics, + * or distributed tracing (e.g. OpenTelemetry). + * + * Multiple hooks can be registered. They execute in the order registered. + * + * LIFETIME: The hook struct is copied during this call. The Name string + * must remain valid until LDServerConfigBuilder_Build() is called. + * UserData and function pointers must remain valid for the SDK lifetime. + * + * @param builder Configuration builder. Must not be NULL. + * @param hook Hook to register. The struct is copied. Must not be NULL. + */ +LD_EXPORT(void) +LDServerConfigBuilder_Hooks(LDServerConfigBuilder builder, + struct LDServerSDKHook hook); + /** * Creates an LDClientConfig. The builder is automatically freed. * diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/evaluation_series_context.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/evaluation_series_context.h new file mode 100644 index 000000000..2228f07d5 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/evaluation_series_context.h @@ -0,0 +1,106 @@ +/** + * @file evaluation_series_context.h + * @brief C bindings for read-only evaluation context passed to hooks. + * + * EvaluationSeriesContext provides information about a flag evaluation + * to hook callbacks. All data is read-only and valid only during the + * callback execution. + * + * LIFETIME: + * - All context parameters are temporary - valid only during callback + * - Do not store pointers to context data + * - Copy any needed data (strings, values) if you need to retain it + */ + +#pragma once + +#include +#include +#include +#include + +// No effect in C++, but we want it for C. +// ReSharper disable once CppUnusedIncludeDirective +#include // NOLINT(*-deprecated-headers) + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct p_LDServerSDKEvaluationSeriesContext* LDServerSDKEvaluationSeriesContext; + +/** + * @brief Get the flag key being evaluated. + * + * @param eval_context Evaluation context. Must not be NULL. + * @return Flag key as null-terminated UTF-8 string. Valid only during + * the callback execution. Must not be freed. + */ +LD_EXPORT(char const*) +LDEvaluationSeriesContext_FlagKey(LDServerSDKEvaluationSeriesContext eval_context); + +/** + * @brief Get the context (user/organization) being evaluated. + * + * @param eval_context Evaluation context. Must not be NULL. + * @return Context object. Valid only during the callback execution. + * Must not be freed. Do not call LDContext_Free() on this. + */ +LD_EXPORT(LDContext) +LDEvaluationSeriesContext_Context(LDServerSDKEvaluationSeriesContext eval_context); + +/** + * @brief Get the default value provided to the variation call. + * + * @param eval_context Evaluation context. Must not be NULL. + * @return Default value. Valid only during the callback execution. + * Must not be freed. Do not call LDValue_Free() on this. + */ +LD_EXPORT(LDValue) +LDEvaluationSeriesContext_DefaultValue( + LDServerSDKEvaluationSeriesContext eval_context); + +/** + * @brief Get the name of the variation method called. + * + * Examples: "BoolVariation", "StringVariationDetail", "JsonVariation" + * + * @param eval_context Evaluation context. Must not be NULL. + * @return Method name as null-terminated UTF-8 string. Valid only during + * the callback execution. Must not be freed. + */ +LD_EXPORT(char const*) +LDEvaluationSeriesContext_Method(LDServerSDKEvaluationSeriesContext eval_context); + +/** + * @brief Get the hook context provided by the caller. + * + * This contains application-specific data passed to the variation call, + * such as OpenTelemetry span parents. + * + * @param eval_context Evaluation context. Must not be NULL. + * @return Hook context. Valid only during the callback execution. + * Must not be freed. Do not call LDHookContext_Free() on this. + */ +LD_EXPORT(LDHookContext) +LDEvaluationSeriesContext_HookContext( + LDServerSDKEvaluationSeriesContext eval_context); + +/** + * @brief Get the environment ID, if available. + * + * The environment ID is only available after SDK initialization completes. + * Returns NULL if not yet available. + * + * @param eval_context Evaluation context. Must not be NULL. + * @return Environment ID as null-terminated UTF-8 string, or NULL if not + * available. Valid only during the callback execution. Must not + * be freed. + */ +LD_EXPORT(char const*) +LDEvaluationSeriesContext_EnvironmentId( + LDServerSDKEvaluationSeriesContext eval_context); + +#ifdef __cplusplus +} +#endif diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/evaluation_series_data.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/evaluation_series_data.h new file mode 100644 index 000000000..51b719c45 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/evaluation_series_data.h @@ -0,0 +1,210 @@ +/** + * @file evaluation_series_data.h + * @brief C bindings for hook data passed between evaluation stages. + * + * EvaluationSeriesData is mutable data that hooks can use to pass information + * from beforeEvaluation to afterEvaluation. This is useful for: + * - Storing timing information + * - Passing span contexts for distributed tracing + * - Accumulating custom metrics + * + * LIFETIME AND OWNERSHIP: + * - Data returned from hook callbacks transfers ownership to the SDK + * - Data received in callbacks can be modified and returned (ownership transfer) + * - Keys are copied by the SDK + * - Values retrieved with GetValue() are temporary - valid only during callback + * - Do not call LDValue_Free() on values retrieved with GetValue() + * - Values stored with SetValue() are copied into the data object + * - Pointers (void*) lifetime is managed by the application + * + * BUILDER PATTERN: + * To modify data, use LDEvaluationSeriesData_NewBuilder() to create a builder, + * make changes, then build a new data object. + */ + +#pragma once + +#include +#include + +// No effect in C++, but we want it for C. +// ReSharper disable once CppUnusedIncludeDirective +#include // NOLINT(*-deprecated-headers) +// ReSharper disable once CppUnusedIncludeDirective +#include // NOLINT(*-deprecated-headers) + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct p_LDServerSDKEvaluationSeriesData* LDServerSDKEvaluationSeriesData; +typedef struct p_LDEvaluationSeriesDataBuilder* LDServerSDKEvaluationSeriesDataBuilder; + + +/** + * @brief Create a new empty evaluation series data. + * + * @return New data object. Must be freed with LDEvaluationSeriesData_Free() + * or transferred to SDK via hook callback return. + */ +LD_EXPORT(LDServerSDKEvaluationSeriesData) +LDEvaluationSeriesData_New(void); + +/** + * @brief Get a Value from the evaluation series data. + * + * LIFETIME: Returns a temporary value valid only during the callback. + * Do not call LDValue_Free() on the returned value. + * + * USAGE: + * @code + * LDValue value; + * if (LDEvaluationSeriesData_GetValue(data, "timestamp", &value)) { + * // Use value (valid only during callback) + * double timestamp = LDValue_GetNumber(value); + * // Do NOT call LDValue_Free(value) + * } + * @endcode + * + * @param data Data object. Must not be NULL. + * @param key Key to look up. Must be null-terminated UTF-8 string. + * Must not be NULL. + * @param out_value Pointer to receive the value. Must not be NULL. + * Set to a temporary LDValue (valid only during callback). + * Do not call LDValue_Free() on this value. + * @return true if key was found and contains a Value, false otherwise. + */ +LD_EXPORT(bool) +LDEvaluationSeriesData_GetValue(LDServerSDKEvaluationSeriesData data, + char const* key, + LDValue* out_value); + +/** + * @brief Get a pointer from the evaluation series data. + * + * Retrieves a pointer previously stored with + * LDEvaluationSeriesDataBuilder_SetPointer(). + * + * USAGE - OpenTelemetry span: + * @code + * void* span; + * if (LDEvaluationSeriesData_GetPointer(data, "span", &span)) { + * // Cast and use span + * MySpan* typed_span = (MySpan*)span; + * } + * @endcode + * + * @param data Data object. Must not be NULL. + * @param key Key to look up. Must be null-terminated UTF-8 string. + * Must not be NULL. + * @param out_pointer Pointer to receive the pointer value. Must not be NULL. + * Set to NULL if key not found. + * @return true if key was found and contains a pointer, false otherwise. + */ +LD_EXPORT(bool) +LDEvaluationSeriesData_GetPointer(LDServerSDKEvaluationSeriesData data, + char const* key, + void** out_pointer); + +/** + * @brief Create a builder from existing data. + * + * Creates a builder initialized with the contents of the data object. + * Use this to add or modify entries in the data. + * + * USAGE: + * @code + * LDServerSDKEvaluationSeriesDataBuilder builder = + * LDEvaluationSeriesData_NewBuilder(input_data); + * LDEvaluationSeriesDataBuilder_SetValue(builder, "key", value); + * return LDEvaluationSeriesDataBuilder_Build(builder); + * @endcode + * + * @param data Data to copy into builder. May be NULL (creates empty builder). + * @return Builder object. Must be freed with + * LDEvaluationSeriesDataBuilder_Free() or consumed with + * LDEvaluationSeriesDataBuilder_Build(). + */ +LD_EXPORT(LDServerSDKEvaluationSeriesDataBuilder) +LDEvaluationSeriesData_NewBuilder(LDServerSDKEvaluationSeriesData data); + +/** + * @brief Free evaluation series data. + * + * Only call this if you created the data and are not returning it from + * a hook callback. Data returned from callbacks is owned by the SDK. + * + * @param data Data to free. May be NULL (no-op). + */ +LD_EXPORT(void) +LDEvaluationSeriesData_Free(LDServerSDKEvaluationSeriesData data); + +/** + * @brief Set a Value in the builder. + * + * OWNERSHIP: The value is copied/moved into the builder. You are responsible + * for freeing the original value if needed. + * + * @param builder Builder object. Must not be NULL. + * @param key Key for the value. Must be null-terminated UTF-8 string. + * Must not be NULL. The key is copied. + * @param value Value to store. Must not be NULL. The value is copied/moved. + */ +LD_EXPORT(void) +LDEvaluationSeriesDataBuilder_SetValue(LDServerSDKEvaluationSeriesDataBuilder builder, + char const* key, + LDValue value); + +/** + * @brief Set a pointer in the builder. + * + * Stores an application-specific pointer. Useful for storing objects like + * OpenTelemetry spans that need to be passed from beforeEvaluation to + * afterEvaluation. + * + * LIFETIME: The pointer lifetime must extend through the evaluation series + * (from beforeEvaluation through afterEvaluation). + * + * EXAMPLE - OpenTelemetry span: + * @code + * MySpan* span = start_span(); + * LDEvaluationSeriesDataBuilder_SetPointer(builder, "span", span); + * @endcode + * + * @param builder Builder object. Must not be NULL. + * @param key Key for the pointer. Must be null-terminated UTF-8 string. + * Must not be NULL. The key is copied. + * @param pointer Pointer to store. May be NULL. Lifetime managed by caller. + */ +LD_EXPORT(void) +LDEvaluationSeriesDataBuilder_SetPointer( + LDServerSDKEvaluationSeriesDataBuilder builder, + char const* key, + void* pointer); + +/** + * @brief Build the evaluation series data. + * + * Consumes the builder and creates a data object. After calling this, + * do not call LDEvaluationSeriesDataBuilder_Free() on the builder. + * + * @param builder Builder to consume. Must not be NULL. + * @return Data object. Must be freed with LDEvaluationSeriesData_Free() + * or transferred to SDK via hook callback return. + */ +LD_EXPORT(LDServerSDKEvaluationSeriesData) +LDEvaluationSeriesDataBuilder_Build(LDServerSDKEvaluationSeriesDataBuilder builder); + +/** + * @brief Free a builder without building. + * + * Only call this if you did not call LDEvaluationSeriesDataBuilder_Build(). + * + * @param builder Builder to free. May be NULL (no-op). + */ +LD_EXPORT(void) +LDEvaluationSeriesDataBuilder_Free(LDServerSDKEvaluationSeriesDataBuilder builder); + +#ifdef __cplusplus +} +#endif diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/hook.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/hook.h new file mode 100644 index 000000000..c2ab7bab5 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/hook.h @@ -0,0 +1,187 @@ +/** + * @file hook.h + * @brief C bindings for LaunchDarkly SDK hooks. + * + * Hooks allow you to instrument the SDK's evaluation and tracking behavior + * for purposes like logging, analytics, or distributed tracing (e.g. OpenTelemetry). + * + * LIFETIME AND OWNERSHIP: + * - The LDServerSDKHook struct should be allocated by the caller (stack or heap) + * - The function pointers and UserData pointer are copied when the hook is + * registered with the config builder + * - UserData lifetime must extend for the entire lifetime of the SDK client + * - All context parameters passed to callbacks are temporary - valid only + * during the callback execution + * - EvaluationSeriesData returned from callbacks transfers ownership to the SDK + */ +// NOLINTBEGIN(modernize-use-using) +#pragma once + +#include +#include + +#ifdef __cplusplus +extern "C" { // only need to export C interface if used by C++ source code +#endif + +typedef struct p_LDServerSDKEvaluationSeriesContext* LDServerSDKEvaluationSeriesContext; +typedef struct p_LDServerSDKEvaluationSeriesData* LDServerSDKEvaluationSeriesData; +typedef struct p_LDServerSDKTrackSeriesContext* LDServerSDKTrackSeriesContext; + +/** + * @brief Callback invoked before a flag evaluation. + * + * Use this to instrument evaluations, such as starting a span for distributed + * tracing. + * + * PARAMETERS: + * @param series_context Read-only context about the evaluation. Valid only + * during this callback. + * @param data Mutable data that can be passed to afterEvaluation. Ownership + * transfers to the SDK. May be NULL initially. + * @param user_data Application-specific context pointer set when creating + * the hook. + * + * RETURNS: + * EvaluationSeriesData to pass to afterEvaluation. Return the input data + * unmodified if you don't need to add anything. Ownership transfers to SDK. + * If NULL is returned, an empty data object will be created. + * + * LIFETIME: + * - series_context: Valid only during callback execution - do not store + * - data: Ownership transfers to SDK + * - user_data: Managed by caller - must remain valid for SDK lifetime + */ +typedef LDServerSDKEvaluationSeriesData (*LDServerSDKHook_BeforeEvaluation)( + LDServerSDKEvaluationSeriesContext series_context, + LDServerSDKEvaluationSeriesData data, + void* user_data); + +/** + * @brief Callback invoked after a flag evaluation. + * + * Use this to instrument evaluation results, such as ending a span for + * distributed tracing or logging the result. + * + * PARAMETERS: + * @param series_context Read-only context about the evaluation. Valid only + * during this callback. + * @param data Mutable data passed from beforeEvaluation. Ownership transfers + * to SDK. May contain data from previous stages. + * @param detail The evaluation result. Valid only during this callback. + * @param user_data Application-specific context pointer. + * + * RETURNS: + * EvaluationSeriesData for potential future stages. Return the input data + * unmodified if you don't need to modify it. Ownership transfers to SDK. + * If NULL is returned, an empty data object will be created. + * + * LIFETIME: + * - series_context: Valid only during callback execution + * - data: Ownership transfers to SDK + * - detail: Valid only during callback execution + * - user_data: Managed by caller + */ +typedef LDServerSDKEvaluationSeriesData (*LDServerSDKHook_AfterEvaluation)( + LDServerSDKEvaluationSeriesContext series_context, + LDServerSDKEvaluationSeriesData data, + LDEvalDetail detail, + void* user_data); + +/** + * @brief Callback invoked after a track event. + * + * Use this to instrument custom events, such as logging or adding + * tracing information. + * + * PARAMETERS: + * @param series_context Read-only context about the track call. Valid only + * during this callback. + * @param user_data Application-specific context pointer. + * + * RETURNS: void (no data is passed between track stages) + * + * LIFETIME: + * - series_context: Valid only during callback execution + * - user_data: Managed by caller + */ +typedef void (*LDServerSDKHook_AfterTrack)(LDServerSDKTrackSeriesContext series_context, + void* user_data); + +/** + * @brief Hook structure containing callback function pointers. + * + * USAGE: + * 1. Allocate an LDServerSDKHook struct (stack or heap) + * 2. Call LDServerSDKHook_Init() to initialize it + * 3. Set the Name field (required, UTF-8 encoded, null-terminated) + * 4. Set any callback function pointers you need (NULL if not used) + * 5. Set UserData to application-specific context (NULL if not needed) + * 6. Register with LDServerConfigBuilder_Hooks() + * + * EXAMPLE: + * @code + * struct LDServerSDKHook my_hook; + * LDServerSDKHook_Init(&my_hook); + * my_hook.Name = "MyTracingHook"; + * my_hook.BeforeEvaluation = my_before_callback; + * my_hook.AfterEvaluation = my_after_callback; + * my_hook.UserData = my_context; + * LDServerConfigBuilder_Hooks(builder, my_hook); + * @endcode + * + * LIFETIME: + * - The LDServerSDKHook struct itself can be stack-allocated or freed after + * registration (the SDK copies it) + * - The Name string must remain valid until LDServerConfigBuilder_Build() + * - UserData must remain valid for the entire SDK client lifetime + */ +struct LDServerSDKHook { + /** + * Name of the hook. Required. Must be a null-terminated UTF-8 string. + * Must remain valid until LDServerConfigBuilder_Build() is called. + */ + char const* Name; + + /** + * Optional callback invoked before evaluations. + * Set to NULL if not needed. + */ + LDServerSDKHook_BeforeEvaluation BeforeEvaluation; + + /** + * Optional callback invoked after evaluations. + * Set to NULL if not needed. + */ + LDServerSDKHook_AfterEvaluation AfterEvaluation; + + /** + * Optional callback invoked after track calls. + * Set to NULL if not needed. + */ + LDServerSDKHook_AfterTrack AfterTrack; + + /** + * Application-specific context pointer passed to all callbacks. + * Must remain valid for the entire SDK client lifetime. + * May be NULL if not needed. + */ + void* UserData; +}; + +/** + * @brief Initialize a hook structure to safe defaults. + * + * Sets all function pointers and UserData to NULL, and Name to NULL. + * Must be called before setting any fields. + * + * @param hook Pointer to hook structure to initialize. Must not be NULL. + */ +LD_EXPORT(void) +LDServerSDKHook_Init(struct LDServerSDKHook* hook); + +#ifdef __cplusplus +} +#endif + +// NOLINTEND(modernize-use-using) diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/hook_context.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/hook_context.h new file mode 100644 index 000000000..a2eab8518 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/hook_context.h @@ -0,0 +1,108 @@ +/** + * @file hook_context.h + * @brief C bindings for passing caller data to hooks. + * + * HookContext allows application code to pass arbitrary data through to hooks. + * This is useful for propagating context like OpenTelemetry span parents in + * asynchronous web frameworks where thread-local storage doesn't work. + * + * USAGE: + * Most applications don't need HookContext. Only use it when you need to pass + * data from the evaluation call site to your hooks, such as: + * - OpenTelemetry span parents in async frameworks + * - Request IDs for distributed tracing + * - Custom metadata for logging + * + * LIFETIME AND OWNERSHIP: + * - Hook context must remain valid for the duration of the variation call + * - Data stored in hook context must be managed by the caller + * - Hook context can be stack-allocated and freed after the variation call + */ + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Opaque hook context handle. + * + * Created by LDHookContext_New(), must be freed with LDHookContext_Free(). + */ +typedef struct p_LDHookContext* LDHookContext; + +/** + * @brief Create a new hook context. + * + * @return New hook context. Must be freed with LDHookContext_Free(). + */ +LD_EXPORT(LDHookContext) +LDHookContext_New(void); + +/** + * @brief Set a pointer value in the hook context. + * + * Stores an application-specific pointer that can be retrieved by hooks. + * The lifetime of the pointed-to data must extend through the variation call. + * + * EXAMPLE - OpenTelemetry span parent: + * @code + * LDHookContext ctx = LDHookContext_New(); + * LDHookContext_Set(ctx, "span_parent", my_span_context); + * bool result = LDClientSDK_BoolVariation_WithHookContext( + * client, context, "flag-key", false, ctx); + * LDHookContext_Free(ctx); + * @endcode + * + * @param hook_context Hook context. Must not be NULL. + * @param key Key for the value. Must be null-terminated UTF-8 string. + * Must not be NULL. + * @param value Pointer to application data. May be NULL. + * Lifetime managed by caller - must remain valid through + * the variation call. + */ +LD_EXPORT(void) +LDHookContext_Set(LDHookContext hook_context, + char const* key, + void const* value); + +/** + * @brief Get a pointer value from the hook context. + * + * Retrieves an application-specific pointer previously stored with + * LDHookContext_Set(). + * + * USAGE IN HOOKS: + * @code + * void* span_parent; + * if (LDHookContext_Get(hook_ctx, "span_parent", &span_parent)) { + * // Use span_parent + * } + * @endcode + * + * @param hook_context Hook context. Must not be NULL. + * @param key Key to look up. Must be null-terminated UTF-8 string. + * Must not be NULL. + * @param out_value Pointer to receive the value. Must not be NULL. + * Set to NULL if key not found. + * @return true if key was found, false otherwise. + */ +LD_EXPORT(bool) +LDHookContext_Get(LDHookContext hook_context, + char const* key, + void const** out_value); + +/** + * @brief Free a hook context. + * + * @param hook_context Hook context to free. May be NULL (no-op). + */ +LD_EXPORT(void) +LDHookContext_Free(LDHookContext hook_context); + +#ifdef __cplusplus +} +#endif diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/sdk.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/sdk.h index 7627bdb82..ace156981 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/sdk.h +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/sdk.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -430,6 +431,304 @@ LDServerSDK_JsonVariationDetail(LDServerSDK sdk, LDValue default_value, LDEvalDetail* out_detail); +/** + * Tracks that the given context performed an event with the given event name, + * passing a hook context for custom hook data. + * + * @code + * LDHookContext hook_ctx = LDHookContext_New(); + * // Set custom data in hook_ctx... + * LDServerSDK_TrackEvent_WithHookContext(sdk, context, "my-event", hook_ctx); + * LDHookContext_Free(hook_ctx); + * @endcode + * + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param event_name Name of the event. Must not be NULL. + * @param hook_context Hook context for passing data to hooks. Ownership is NOT + * transferred. May be NULL. + */ +LD_EXPORT(void) +LDServerSDK_TrackEvent_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* event_name, + LDHookContext hook_context); + +/** + * Tracks that the given context performed an event with the given event + * name, associates it with a numeric metric and value, and passes a hook + * context for custom hook data. + * + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param event_name The name of the event. Must not be NULL. + * @param metric_value This value is used by the LaunchDarkly experimentation + * feature in numeric custom metrics, and will also be returned as part of the + * custom event for Data Export. + * @param data A JSON value containing additional data associated with the + * event. Ownership is transferred into the SDK. Must not be NULL. + * @param hook_context Hook context for passing data to hooks. Ownership is NOT + * transferred. May be NULL. + */ +LD_EXPORT(void) +LDServerSDK_TrackMetric_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* event_name, + double metric_value, + LDValue data, + LDHookContext hook_context); + +/** + * Tracks that the given context performed an event with the given event + * name, with additional JSON data, and passes a hook context for custom + * hook data. + * + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param event_name The name of the event. Must not be NULL. + * @param data A JSON value containing additional data associated with the + * event. Ownership is transferred. Must not be NULL. + * @param hook_context Hook context for passing data to hooks. Ownership is NOT + * transferred. May be NULL. + */ +LD_EXPORT(void) +LDServerSDK_TrackData_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* event_name, + LDValue data, + LDHookContext hook_context); + +/** + * Returns the boolean value of a feature flag for a given flag key and context, + * passing a hook context for custom hook data. + * + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @param hook_context Hook context for passing data to hooks. Ownership is NOT + * transferred. May be NULL. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(bool) +LDServerSDK_BoolVariation_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + bool default_value, + LDHookContext hook_context); + +/** + * Returns the boolean value of a feature flag for a given flag key and context, + * details that describe the way the value was determined, and passes a hook + * context for custom hook data. + * + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @param hook_context Hook context for passing data to hooks. Ownership is NOT + * transferred. May be NULL. + * @param out_detail Pointer to evaluation detail. Must not be NULL. The value + * written to the pointer must be freed with LDEvalDetail_Free when no longer + * needed. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(bool) +LDServerSDK_BoolVariationDetail_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + bool default_value, + LDHookContext hook_context, + LDEvalDetail* out_detail); + +/** + * Returns the string value of a feature flag for a given flag key and context, + * passing a hook context for custom hook data. + * + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. Must not be NULL. + * @param hook_context Hook context for passing data to hooks. Ownership is NOT + * transferred. May be NULL. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. Must be freed with + * LDMemory_FreeString when no longer needed. + */ +LD_EXPORT(char*) +LDServerSDK_StringVariation_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + char const* default_value, + LDHookContext hook_context); + +/** + * Returns the string value of a feature flag for a given flag key and context, + * details that describe the way the value was determined, and passes a hook + * context for custom hook data. + * + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. Must not be NULL. + * @param hook_context Hook context for passing data to hooks. Ownership is NOT + * transferred. May be NULL. + * @param out_detail Pointer to evaluation detail. Must not be NULL. The value + * written to the pointer must be freed with LDEvalDetail_Free when no longer + * needed. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. Must be freed with + * LDMemory_FreeString when no longer needed. + */ +LD_EXPORT(char*) +LDServerSDK_StringVariationDetail_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + char const* default_value, + LDHookContext hook_context, + LDEvalDetail* out_detail); + +/** + * Returns the integer value of a feature flag for a given flag key and context, + * passing a hook context for custom hook data. + * + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @param hook_context Hook context for passing data to hooks. Ownership is NOT + * transferred. May be NULL. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(int) +LDServerSDK_IntVariation_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + int default_value, + LDHookContext hook_context); + +/** + * Returns the integer value of a feature flag for a given flag key and context, + * details that describe the way the value was determined, and passes a hook + * context for custom hook data. + * + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @param hook_context Hook context for passing data to hooks. Ownership is NOT + * transferred. May be NULL. + * @param out_detail Pointer to evaluation detail. Must not be NULL. The value + * written to the pointer must be freed with LDEvalDetail_Free when no longer + * needed. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(int) +LDServerSDK_IntVariationDetail_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + int default_value, + LDHookContext hook_context, + LDEvalDetail* out_detail); + +/** + * Returns the double value of a feature flag for a given flag key and context, + * passing a hook context for custom hook data. + * + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @param hook_context Hook context for passing data to hooks. Ownership is NOT + * transferred. May be NULL. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(double) +LDServerSDK_DoubleVariation_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + double default_value, + LDHookContext hook_context); + +/** + * Returns the double value of a feature flag for a given flag key and context, + * details that describe the way the value was determined, and passes a hook + * context for custom hook data. + * + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. + * @param hook_context Hook context for passing data to hooks. Ownership is NOT + * transferred. May be NULL. + * @param out_detail Pointer to evaluation detail. Must not be NULL. The value + * written to the pointer must be freed with LDEvalDetail_Free when no longer + * needed. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. + */ +LD_EXPORT(double) +LDServerSDK_DoubleVariationDetail_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + double default_value, + LDHookContext hook_context, + LDEvalDetail* out_detail); + +/** + * Returns the JSON value of a feature flag for a given flag key and context, + * passing a hook context for custom hook data. + * + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. Ownership is NOT + * transferred. Must not be NULL. + * @param hook_context Hook context for passing data to hooks. Ownership is NOT + * transferred. May be NULL. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. Must be freed with + * LDValue_Free when no longer needed. + */ +LD_EXPORT(LDValue) +LDServerSDK_JsonVariation_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + LDValue default_value, + LDHookContext hook_context); + +/** + * Returns the JSON value of a feature flag for a given flag key and context, + * details that describe the way the value was determined, and passes a hook + * context for custom hook data. + * + * @param sdk SDK. Must not be NULL. + * @param context The context. Ownership is NOT transferred. Must not be NULL. + * @param flag_key The unique key for the feature flag. Must not be NULL. + * @param default_value The default value of the flag. Ownership is NOT + * transferred. Must not be NULL. + * @param hook_context Hook context for passing data to hooks. Ownership is NOT + * transferred. May be NULL. + * @param out_detail Pointer to evaluation detail. Must not be NULL. The value + * written to the pointer must be freed with LDEvalDetail_Free when no longer + * needed. + * @return The variation for the given context, or default_value if the + * flag is disabled in the LaunchDarkly control panel. Must be freed with + * LDValue_Free when no longer needed. + */ +LD_EXPORT(LDValue) +LDServerSDK_JsonVariationDetail_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + LDValue default_value, + LDHookContext hook_context, + LDEvalDetail* out_detail); + /** * Evaluates all flags for a context, returning a data structure containing * the results and additional flag metadata. diff --git a/libs/server-sdk/include/launchdarkly/server_side/bindings/c/track_series_context.h b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/track_series_context.h new file mode 100644 index 000000000..5c6aca086 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/bindings/c/track_series_context.h @@ -0,0 +1,121 @@ +/** + * @file track_series_context.h + * @brief C bindings for read-only track context passed to afterTrack hooks. + * + * TrackSeriesContext provides information about a track event to hook callbacks. + * Most data is read-only and valid only during the callback execution. + * + * LIFETIME AND OWNERSHIP: + * - All context parameters are temporary - valid only during callback + * - Do not store pointers to context, key, or environment ID strings + * - Data retrieved with LDTrackSeriesContext_Data() is temporary - valid only + * during callback + * - Do not call LDValue_Free() on data retrieved with LDTrackSeriesContext_Data() + * - Metric values and hook context are temporary - valid only during callback + */ + +#pragma once + +#include +#include +#include +#include + +// No effect in C++, but we want it for C. +// ReSharper disable once CppUnusedIncludeDirective +#include // NOLINT(*-deprecated-headers) + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct p_LDServerSDKTrackSeriesContext* LDServerSDKTrackSeriesContext; + +/** + * @brief Get the event key for the track call. + * + * @param track_context Track context. Must not be NULL. + * @return Event key as null-terminated UTF-8 string. Valid only during + * the callback execution. Must not be freed. + */ +LD_EXPORT(char const*) +LDTrackSeriesContext_Key(LDServerSDKTrackSeriesContext track_context); + +/** + * @brief Get the context (user/organization) associated with the track call. + * + * @param track_context Track context. Must not be NULL. + * @return Context object. Valid only during the callback execution. + * Must not be freed. Do not call LDContext_Free() on this. + */ +LD_EXPORT(LDContext) +LDTrackSeriesContext_Context(LDServerSDKTrackSeriesContext track_context); + +/** + * @brief Get the application-specified data for the track call, if any. + * + * LIFETIME: Returns a temporary value valid only during the callback. + * Do not call LDValue_Free() on the returned value. + * + * USAGE: + * @code + * LDValue data; + * if (LDTrackSeriesContext_Data(track_context, &data)) { + * // Use data (valid only during callback) + * char const* str = LDValue_GetString(data); + * // Do NOT call LDValue_Free(data) + * } + * @endcode + * + * @param track_context Track context. Must not be NULL. + * @param out_data Pointer to receive the data value. Must not be NULL. + * Set to a temporary LDValue (valid only during callback). + * Do not call LDValue_Free() on this value. Set to NULL if no + * data was provided. + * @return true if data was provided, false if no data. + */ +LD_EXPORT(bool) +LDTrackSeriesContext_Data(LDServerSDKTrackSeriesContext track_context, LDValue* out_data); + +/** + * @brief Get the metric value for the track call, if any. + * + * @param track_context Track context. Must not be NULL. + * @param out_metric_value Pointer to receive the metric value. Must not be + * NULL. Only set if a metric value was provided. + * @return true if a metric value was provided, false otherwise. + */ +LD_EXPORT(bool) +LDTrackSeriesContext_MetricValue(LDServerSDKTrackSeriesContext track_context, + double* out_metric_value); + +/** + * @brief Get the hook context provided by the caller. + * + * This contains application-specific data passed to the track call, + * such as OpenTelemetry span parents. + * + * @param track_context Track context. Must not be NULL. + * @return Hook context. Valid only during the callback execution. + * Must not be freed. Do not call LDHookContext_Free() on this. + */ +LD_EXPORT(LDHookContext) +LDTrackSeriesContext_HookContext(LDServerSDKTrackSeriesContext track_context); + +/** + * @brief Get the environment ID, if available. + * + * The environment ID is only available after SDK initialization completes. + * Returns NULL if not yet available. + * + * @param track_context Track context. Must not be NULL. + * @return Environment ID as null-terminated UTF-8 string, or NULL if not + * available. Valid only during the callback execution. Must not + * be freed. + */ +LD_EXPORT(char const*) +LDTrackSeriesContext_EnvironmentId(LDServerSDKTrackSeriesContext track_context); + +#ifdef __cplusplus +} +#endif diff --git a/libs/server-sdk/include/launchdarkly/server_side/client.hpp b/libs/server-sdk/include/launchdarkly/server_side/client.hpp index c74b48dc2..dba0247b7 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/client.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/client.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include @@ -107,6 +108,27 @@ class IClient { Value data, double metric_value) = 0; + /** + * Tracks that the current context performed an event for the given event + * name, and associates it with a numeric metric value. + * + * @param event_name The name of the event. + * @param data A JSON value containing additional data associated with the + * event. + * @param metric_value this value is used by the LaunchDarkly + * experimentation feature in numeric custom metrics, and will also be + * returned as part of the custom event for Data Export + * @param hook_context Additional context data to pass to hooks. This is + * only needed when propagating data to hooks, such as OpenTelemetry span + * parents in asynchronous web frameworks where thread-local storage does + * not work. Most applications do not need to use this parameter. + */ + virtual void Track(Context const& ctx, + std::string event_name, + Value data, + double metric_value, + hooks::HookContext const& hook_context) = 0; + /** * Tracks that the current context performed an event for the given event * name, with additional JSON data. @@ -119,6 +141,23 @@ class IClient { std::string event_name, Value data) = 0; + /** + * Tracks that the current context performed an event for the given event + * name, with additional JSON data. + * + * @param event_name The name of the event. + * @param data A JSON value containing additional data associated with the + * event. + * @param hook_context Additional context data to pass to hooks. This is + * only needed when propagating data to hooks, such as OpenTelemetry span + * parents in asynchronous web frameworks where thread-local storage does + * not work. Most applications do not need to use this parameter. + */ + virtual void Track(Context const& ctx, + std::string event_name, + Value data, + hooks::HookContext const& hook_context) = 0; + /** * Tracks that the current context performed an event for the given event * name. @@ -127,6 +166,20 @@ class IClient { */ virtual void Track(Context const& ctx, std::string event_name) = 0; + /** + * Tracks that the current context performed an event for the given event + * name. + * + * @param event_name The name of the event. + * @param hook_context Additional context data to pass to hooks. This is + * only needed when propagating data to hooks, such as OpenTelemetry span + * parents in asynchronous web frameworks where thread-local storage does + * not work. Most applications do not need to use this parameter. + */ + virtual void Track(Context const& ctx, + std::string event_name, + hooks::HookContext const& hook_context) = 0; + /** * Tells the client that all pending analytics events (if any) should be * delivered as soon as possible. @@ -153,6 +206,23 @@ class IClient { FlagKey const& key, bool default_value) = 0; + /** + * Returns the boolean value of a feature flag for a given flag key. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @param hook_context Additional context data to pass to hooks. This is + * only needed when propagating data to hooks, such as OpenTelemetry span + * parents in asynchronous web frameworks where thread-local storage does + * not work. Most applications do not need to use this parameter. + * @return The variation for the selected context, or default_value if the + * flag is disabled in the LaunchDarkly control panel + */ + virtual bool BoolVariation(Context const& ctx, + FlagKey const& key, + bool default_value, + hooks::HookContext const& hook_context) = 0; + /** * Returns the boolean value of a feature flag for a given flag key, in an * object that also describes the way the value was determined. @@ -165,6 +235,24 @@ class IClient { FlagKey const& key, bool default_value) = 0; + /** + * Returns the boolean value of a feature flag for a given flag key, in an + * object that also describes the way the value was determined. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @param hook_context Additional context data to pass to hooks. This is + * only needed when propagating data to hooks, such as OpenTelemetry span + * parents in asynchronous web frameworks where thread-local storage does + * not work. Most applications do not need to use this parameter. + * @return An evaluation detail object. + */ + virtual EvaluationDetail BoolVariationDetail( + Context const& ctx, + FlagKey const& key, + bool default_value, + hooks::HookContext const& hook_context) = 0; + /** * Returns the string value of a feature flag for a given flag key. * @@ -177,6 +265,23 @@ class IClient { FlagKey const& key, std::string default_value) = 0; + /** + * Returns the string value of a feature flag for a given flag key. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @param hook_context Additional context data to pass to hooks. This is + * only needed when propagating data to hooks, such as OpenTelemetry span + * parents in asynchronous web frameworks where thread-local storage does + * not work. Most applications do not need to use this parameter. + * @return The variation for the selected context, or default_value if the + * flag is disabled in the LaunchDarkly control panel + */ + virtual std::string StringVariation(Context const& ctx, + FlagKey const& key, + std::string default_value, + hooks::HookContext const& hook_context) = 0; + /** * Returns the string value of a feature flag for a given flag key, in an * object that also describes the way the value was determined. @@ -190,6 +295,24 @@ class IClient { FlagKey const& key, std::string default_value) = 0; + /** + * Returns the string value of a feature flag for a given flag key, in an + * object that also describes the way the value was determined. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @param hook_context Additional context data to pass to hooks. This is + * only needed when propagating data to hooks, such as OpenTelemetry span + * parents in asynchronous web frameworks where thread-local storage does + * not work. Most applications do not need to use this parameter. + * @return An evaluation detail object. + */ + virtual EvaluationDetail StringVariationDetail( + Context const& ctx, + FlagKey const& key, + std::string default_value, + hooks::HookContext const& hook_context) = 0; + /** * Returns the double value of a feature flag for a given flag key. * @@ -202,6 +325,23 @@ class IClient { FlagKey const& key, double default_value) = 0; + /** + * Returns the double value of a feature flag for a given flag key. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @param hook_context Additional context data to pass to hooks. This is + * only needed when propagating data to hooks, such as OpenTelemetry span + * parents in asynchronous web frameworks where thread-local storage does + * not work. Most applications do not need to use this parameter. + * @return The variation for the selected context, or default_value if the + * flag is disabled in the LaunchDarkly control panel + */ + virtual double DoubleVariation(Context const& ctx, + FlagKey const& key, + double default_value, + hooks::HookContext const& hook_context) = 0; + /** * Returns the double value of a feature flag for a given flag key, in an * object that also describes the way the value was determined. @@ -215,6 +355,24 @@ class IClient { FlagKey const& key, double default_value) = 0; + /** + * Returns the double value of a feature flag for a given flag key, in an + * object that also describes the way the value was determined. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @param hook_context Additional context data to pass to hooks. This is + * only needed when propagating data to hooks, such as OpenTelemetry span + * parents in asynchronous web frameworks where thread-local storage does + * not work. Most applications do not need to use this parameter. + * @return An evaluation detail object. + */ + virtual EvaluationDetail DoubleVariationDetail( + Context const& ctx, + FlagKey const& key, + double default_value, + hooks::HookContext const& hook_context) = 0; + /** * Returns the int value of a feature flag for a given flag key. * @@ -227,6 +385,23 @@ class IClient { FlagKey const& key, int default_value) = 0; + /** + * Returns the int value of a feature flag for a given flag key. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @param hook_context Additional context data to pass to hooks. This is + * only needed when propagating data to hooks, such as OpenTelemetry span + * parents in asynchronous web frameworks where thread-local storage does + * not work. Most applications do not need to use this parameter. + * @return The variation for the selected context, or default_value if the + * flag is disabled in the LaunchDarkly control panel + */ + virtual int IntVariation(Context const& ctx, + FlagKey const& key, + int default_value, + hooks::HookContext const& hook_context) = 0; + /** * Returns the int value of a feature flag for a given flag key, in an * object that also describes the way the value was determined. @@ -239,6 +414,24 @@ class IClient { FlagKey const& key, int default_value) = 0; + /** + * Returns the int value of a feature flag for a given flag key, in an + * object that also describes the way the value was determined. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @param hook_context Additional context data to pass to hooks. This is + * only needed when propagating data to hooks, such as OpenTelemetry span + * parents in asynchronous web frameworks where thread-local storage does + * not work. Most applications do not need to use this parameter. + * @return An evaluation detail object. + */ + virtual EvaluationDetail IntVariationDetail( + Context const& ctx, + FlagKey const& key, + int default_value, + hooks::HookContext const& hook_context) = 0; + /** * Returns the JSON value of a feature flag for a given flag key. * @@ -251,6 +444,23 @@ class IClient { FlagKey const& key, Value default_value) = 0; + /** + * Returns the JSON value of a feature flag for a given flag key. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @param hook_context Additional context data to pass to hooks. This is + * only needed when propagating data to hooks, such as OpenTelemetry span + * parents in asynchronous web frameworks where thread-local storage does + * not work. Most applications do not need to use this parameter. + * @return The variation for the selected context, or default_value if the + * flag is disabled in the LaunchDarkly control panel + */ + virtual Value JsonVariation(Context const& ctx, + FlagKey const& key, + Value default_value, + hooks::HookContext const& hook_context) = 0; + /** * Returns the JSON value of a feature flag for a given flag key, in an * object that also describes the way the value was determined. @@ -264,6 +474,24 @@ class IClient { FlagKey const& key, Value default_value) = 0; + /** + * Returns the JSON value of a feature flag for a given flag key, in an + * object that also describes the way the value was determined. + * + * @param key The unique feature key for the feature flag. + * @param default_value The default value of the flag. + * @param hook_context Additional context data to pass to hooks. This is + * only needed when propagating data to hooks, such as OpenTelemetry span + * parents in asynchronous web frameworks where thread-local storage does + * not work. Most applications do not need to use this parameter. + * @return An evaluation detail object. + */ + virtual EvaluationDetail JsonVariationDetail( + Context const& ctx, + FlagKey const& key, + Value default_value, + hooks::HookContext const& hook_context) = 0; + /** * Returns an interface which provides methods for subscribing to data * source status. @@ -300,10 +528,25 @@ class Client : public IClient { Value data, double metric_value) override; + void Track(Context const& ctx, + std::string event_name, + Value data, + double metric_value, + hooks::HookContext const& hook_context) override; + void Track(Context const& ctx, std::string event_name, Value data) override; + void Track(Context const& ctx, + std::string event_name, + Value data, + hooks::HookContext const& hook_context) override; + void Track(Context const& ctx, std::string event_name) override; + void Track(Context const& ctx, + std::string event_name, + hooks::HookContext const& hook_context) override; + void FlushAsync() override; void Identify(Context context) override; @@ -312,44 +555,99 @@ class Client : public IClient { FlagKey const& key, bool default_value) override; + bool BoolVariation(Context const& ctx, + FlagKey const& key, + bool default_value, + hooks::HookContext const& hook_context) override; + EvaluationDetail BoolVariationDetail(Context const& ctx, FlagKey const& key, bool default_value) override; + EvaluationDetail BoolVariationDetail( + Context const& ctx, + FlagKey const& key, + bool default_value, + hooks::HookContext const& hook_context) override; + std::string StringVariation(Context const& ctx, FlagKey const& key, std::string default_value) override; + std::string StringVariation(Context const& ctx, + FlagKey const& key, + std::string default_value, + hooks::HookContext const& hook_context) override; + EvaluationDetail StringVariationDetail( Context const& ctx, FlagKey const& key, std::string default_value) override; + EvaluationDetail StringVariationDetail( + Context const& ctx, + FlagKey const& key, + std::string default_value, + hooks::HookContext const& hook_context) override; + double DoubleVariation(Context const& ctx, FlagKey const& key, double default_value) override; + double DoubleVariation(Context const& ctx, + FlagKey const& key, + double default_value, + hooks::HookContext const& hook_context) override; + EvaluationDetail DoubleVariationDetail( Context const& ctx, FlagKey const& key, double default_value) override; + EvaluationDetail DoubleVariationDetail( + Context const& ctx, + FlagKey const& key, + double default_value, + hooks::HookContext const& hook_context) override; + int IntVariation(Context const& ctx, FlagKey const& key, int default_value) override; + int IntVariation(Context const& ctx, + FlagKey const& key, + int default_value, + hooks::HookContext const& hook_context) override; + EvaluationDetail IntVariationDetail(Context const& ctx, FlagKey const& key, int default_value) override; + EvaluationDetail IntVariationDetail( + Context const& ctx, + FlagKey const& key, + int default_value, + hooks::HookContext const& hook_context) override; + Value JsonVariation(Context const& ctx, FlagKey const& key, Value default_value) override; + Value JsonVariation(Context const& ctx, + FlagKey const& key, + Value default_value, + hooks::HookContext const& hook_context) override; + EvaluationDetail JsonVariationDetail(Context const& ctx, FlagKey const& key, Value default_value) override; + EvaluationDetail JsonVariationDetail( + Context const& ctx, + FlagKey const& key, + Value default_value, + hooks::HookContext const& hook_context) override; + IDataSourceStatusProvider& DataSourceStatus() override; /** diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp index 5fbdbea8f..ff5156255 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config.hpp @@ -2,6 +2,10 @@ #include #include +#include + +#include +#include namespace launchdarkly::server_side { @@ -13,7 +17,8 @@ struct Config { config::built::Events events, std::optional application_tag, config::built::DataSystemConfig data_system_config, - config::built::HttpProperties http_properties); + config::built::HttpProperties http_properties, + std::vector> hooks); [[nodiscard]] std::string const& SdkKey() const; @@ -30,6 +35,9 @@ struct Config { [[nodiscard]] config::built::Logging const& Logging() const; + [[nodiscard]] std::vector> const& Hooks() + const; + private: std::string sdk_key_; bool offline_; @@ -39,5 +47,6 @@ struct Config { config::built::Events events_; config::built::DataSystemConfig data_system_config_; config::built::HttpProperties http_properties_; + std::vector> hooks_; }; } // namespace launchdarkly::server_side diff --git a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp index 563578d42..2f768c1f6 100644 --- a/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp +++ b/libs/server-sdk/include/launchdarkly/server_side/config/config_builder.hpp @@ -2,6 +2,10 @@ #include #include +#include + +#include +#include namespace launchdarkly::server_side { @@ -73,6 +77,14 @@ class ConfigBuilder { */ ConfigBuilder& Offline(bool offline); + /** + * Adds a hook to the SDK configuration. + * + * @param hook A shared pointer to a hook implementation. + * @return Reference to this. + */ + ConfigBuilder& Hooks(std::shared_ptr hook); + /** * Builds a Configuration, suitable for passing into an instance of Client. * @return @@ -89,5 +101,6 @@ class ConfigBuilder { config::builders::DataSystemBuilder data_system_builder_; config::builders::HttpPropertiesBuilder http_properties_builder_; config::builders::LoggingBuilder logging_config_builder_; + std::vector> hooks_; }; } // namespace launchdarkly::server_side diff --git a/libs/server-sdk/include/launchdarkly/server_side/hooks/hook.hpp b/libs/server-sdk/include/launchdarkly/server_side/hooks/hook.hpp new file mode 100644 index 000000000..841c7f932 --- /dev/null +++ b/libs/server-sdk/include/launchdarkly/server_side/hooks/hook.hpp @@ -0,0 +1,507 @@ +#pragma once + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace launchdarkly::server_side::hooks { + +/** + * HookContext allows passing arbitrary data from the caller through to hooks. + * + * Example use case: + * @code + * // Caller creates context with span parent + * HookContext ctx; + * ctx.Set("otel_span_parent", std::make_shared(span_context)); + * + * // Pass to variation method + * client.BoolVariation(context, "flag-key", false, ctx); + * + * // Hook accesses the span parent + * auto span_parent = hook_context.Get("otel_span_parent"); + * @endcode + */ +class HookContext { + public: + /** + * Constructs an empty HookContext. + */ + HookContext() = default; + + /** + * Sets a value in the context. + * @param key The key to set. + * @param value The shared_ptr to any type to associate with the key. + * @return Reference to this context for chaining. + */ + HookContext& Set(std::string key, std::shared_ptr value); + + /** + * Retrieves a value from the context. + * @param key The key to look up. + * @return The shared_ptr if present, or std::nullopt if not found. + */ + [[nodiscard]] std::optional> Get( + std::string const& key) const; + + /** + * Checks if a key exists in the context. + * @param key The key to check. + * @return True if the key exists, false otherwise. + */ + [[nodiscard]] bool Has(std::string const& key) const; + + private: + std::map> data_; +}; + +/** + * Metadata about a hook implementation. + * + * Lifetime: Objects of this type are owned by the hook + * and remain valid for the lifetime of the hook. + */ +class HookMetadata { + public: + /** + * Constructs hook metadata. + * @param name The name of the hook. + */ + explicit HookMetadata(std::string name); + + /** + * Returns the name of the hook. + * + * Lifetime: The returned string_view is valid only for the lifetime + * of this metadata object. If you need the name beyond the immediate + * call, copy it to a std::string. + * + * @return The hook name as a string_view (immutable). + */ + [[nodiscard]] std::string_view Name() const; + + private: + std::string name_; +}; + +/** + * Immutable data that can be passed between hook stages. + * Provides readonly access to data passed between hook stages. + * + * Supports two types of data: + * - Value: LaunchDarkly Value types (strings, numbers, booleans, etc.) + * - std::shared_ptr: Arbitrary objects (e.g., OpenTelemetry spans) + * + * Lifetime: Objects of this type are valid only during + * the execution of a hook stage. Do not store references or pointers + * to this data. If you need data beyond the stage execution, copy it. + */ +class EvaluationSeriesData { + public: + /** + * Constructs empty series data. + */ + EvaluationSeriesData(); + + /** + * Retrieves a Value from the series data. + * + * Lifetime: The returned reference (if present) is valid only during + * the execution of the current hook stage. If you need the value + * beyond this call, make a copy. + * + * @param key The key to look up. + * @return Reference to the value if present, or std::nullopt if not found + * or if the key maps to a shared_ptr. + */ + [[nodiscard]] std::optional> Get(std::string const& key) const; + + /** + * Retrieves a shared_ptr to any type from the series data. + * + * Use this for storing arbitrary objects like OpenTelemetry spans + * that need to be passed from beforeEvaluation to afterEvaluation. + * + * Example: + * @code + * auto span = data.GetShared("span"); + * if (span) { + * auto typed_span = std::any_cast(*span); + * } + * @endcode + * + * @param key The key to look up. + * @return The shared_ptr if present, or std::nullopt if not found or if + * the key maps to a Value. + */ + [[nodiscard]] std::optional> GetShared( + std::string const& key) const; + + /** + * Checks if a key exists in the series data. + * @param key The key to check. + * @return True if the key exists, false otherwise. + */ + [[nodiscard]] bool Has(std::string const& key) const; + + /** + * Returns all keys in the series data. + * + * Lifetime: The returned vector contains copies of the keys and + * can be stored safely. + * + * @return Vector of all keys. + */ + [[nodiscard]] std::vector Keys() const; + + private: + friend class EvaluationSeriesDataBuilder; + + struct DataEntry { + std::optional value; + std::optional> shared; + }; + + explicit EvaluationSeriesData(std::map data); + + std::map data_; +}; + +/** + * Builder for creating evaluation series data. + * Allows hook stages to add data to be passed to subsequent stages. + */ +class EvaluationSeriesDataBuilder { + public: + /** + * Creates a new builder from existing data. + * @param data The existing data to copy. + */ + explicit EvaluationSeriesDataBuilder(EvaluationSeriesData const& data); + + /** + * Creates a new empty builder. + */ + EvaluationSeriesDataBuilder(); + + /** + * Sets a Value in the series data. + * @param key The key to set. + * @param value The value to associate with the key. + * @return Reference to this builder for chaining. + */ + EvaluationSeriesDataBuilder& Set(std::string key, Value value); + + /** + * Sets a shared_ptr to any type in the series data. + * + * Use this for storing arbitrary objects like OpenTelemetry spans + * that need to be passed from beforeEvaluation to afterEvaluation. + * + * Example: + * @code + * auto span = std::make_shared(MySpan{}); + * builder.SetShared("span", span); + * @endcode + * + * @param key The key to set. + * @param value The shared_ptr to associate with the key. + * @return Reference to this builder for chaining. + */ + EvaluationSeriesDataBuilder& SetShared(std::string key, + std::shared_ptr value); + + /** + * Builds the immutable series data. + * @return The built data. + */ + [[nodiscard]] EvaluationSeriesData Build() const; + + private: + std::map data_; +}; + +/** + * Context for evaluation series stages. + * Provides readonly information about the evaluation being performed. + * + * Lifetime: Objects of this type are valid only during + * the execution of a hook stage. Do not store references, pointers, or + * string_views from this context. If you need any data beyond the stage + * execution, copy it to owned types (e.g., std::string, Value). + */ +class EvaluationSeriesContext { + public: + /** + * Constructs an evaluation series context. + * @param flag_key The flag key being evaluated. + * @param context The context against which the flag is being evaluated. + * @param default_value The default value for the evaluation. + * @param method The method being executed. + * @param hook_context Additional context data provided by the caller. + * @param environment_id Optional environment ID. + */ + EvaluationSeriesContext(std::string flag_key, + Context const& context, + Value default_value, + std::string method, + HookContext const& hook_context, + std::optional environment_id); + + /** + * Returns the flag key being evaluated. + * + * Lifetime: The returned string_view is valid only during the + * execution of the current hook stage. If you need the flag key + * beyond this call, copy it to a std::string. + * + * @return The flag key as a string_view (immutable). + */ + [[nodiscard]] std::string_view FlagKey() const; + + /** + * Returns the context against which the flag is being evaluated. + * + * Lifetime: The returned Context reference is valid only during + * the execution of the current hook stage. If you need context + * data beyond this call, copy the necessary fields. + * + * @return The evaluation context. + */ + [[nodiscard]] Context const& EvaluationContext() const; + + /** + * Returns the default value provided to the variation method. + * + * Lifetime: The returned Value reference is valid only during + * the execution of the current hook stage. If you need the value + * beyond this call, make a copy. + * + * @return Reference to the default value. + */ + [[nodiscard]] Value const& DefaultValue() const; + + /** + * Returns the method being executed. + * Examples: "BoolVariation", "StringVariationDetail" + * + * Lifetime: The returned string_view is valid only during the + * execution of the current hook stage. If you need the method name + * beyond this call, copy it to a std::string. + * + * @return The method name as a string_view (immutable). + */ + [[nodiscard]] std::string_view Method() const; + + /** + * Returns the environment ID if available. + * Only available once initialization has completed. + * + * Lifetime: If present, the returned string_view is valid only during + * the execution of the current hook stage. If you need the environment + * ID beyond this call, copy it to a std::string. + * + * @return The environment ID as optional string_view, or std::nullopt + * if not available. + */ + [[nodiscard]] std::optional EnvironmentId() const; + + /** + * Returns the hook context provided by the caller. + * + * This contains arbitrary data that the caller wants to pass through + * to hooks, such as OpenTelemetry span parents. + * + * Lifetime: The returned reference is valid only during the execution + * of the current hook stage. + * + * @return Reference to the hook context. + */ + [[nodiscard]] HookContext const& HookCtx() const; + + private: + std::string flag_key_; + Context const& context_; + Value default_value_; + std::string method_; + HookContext const& hook_context_; + std::optional environment_id_; +}; + +/** + * Context for track series handlers. + * Provides readonly information about the track call being performed. + * + * Lifetime: Objects of this type are valid only during + * the execution of a hook stage. Do not store references, pointers, or + * string_views from this context. If you need any data beyond the stage + * execution, copy it to owned types (e.g., std::string, Value). + */ +class TrackSeriesContext { + public: + /** + * Constructs a track series context. + * @param context The context associated with the track call. + * @param key The event key. + * @param metric_value Optional metric value. + * @param data Optional reference to application-specified data. + * @param hook_context Additional context data provided by the caller. + * @param environment_id Optional environment ID. + */ + TrackSeriesContext(Context const& context, + std::string key, + std::optional metric_value, + std::optional> data, + HookContext const& hook_context, + std::optional environment_id); + + /** + * Returns the context associated with the track call. + * + * Lifetime: The returned Context reference is valid only during + * the execution of the current hook stage. If you need context + * data beyond this call, copy the necessary fields. + * + * @return The context. + */ + [[nodiscard]] Context const& TrackContext() const; + + /** + * Returns the key associated with the track call. + * + * Lifetime: The returned string_view is valid only during the + * execution of the current hook stage. If you need the key + * beyond this call, copy it to a std::string. + * + * @return The event key as a string_view (immutable). + */ + [[nodiscard]] std::string_view Key() const; + + /** + * Returns the optional metric value associated with the track call. + * @return The metric value, or std::nullopt if not provided. + */ + [[nodiscard]] std::optional MetricValue() const; + + /** + * Returns the application-specified data associated with the track call. + * + * Lifetime: The returned reference (if present) is valid only during + * the execution of the current hook stage. If you need the value + * beyond this call, make a copy. + * + * @return Reference to the data, or std::nullopt if not provided. + */ + [[nodiscard]] std::optional> Data() const; + + /** + * Returns the environment ID if available. + * Only available once initialization has completed. + * + * Lifetime: If present, the returned string_view is valid only during + * the execution of the current hook stage. If you need the environment + * ID beyond this call, copy it to a std::string. + * + * @return The environment ID as optional string_view, or std::nullopt + * if not available. + */ + [[nodiscard]] std::optional EnvironmentId() const; + + /** + * Returns the hook context provided by the caller. + * + * This contains arbitrary data that the caller wants to pass through + * to hooks, such as OpenTelemetry span parents. + * + * Lifetime: The returned reference is valid only during the execution + * of the current hook stage. + * + * @return Reference to the hook context. + */ + [[nodiscard]] HookContext const& HookCtx() const; + + private: + Context const& context_; + std::string key_; + std::optional metric_value_; + std::optional> data_; + HookContext const& hook_context_; + std::optional environment_id_; +}; + +/** + * Base interface for hook implementations. + * + * All stage methods have default implementations that take no action, + * allowing hook implementations to only override the stages they need. + * + * This interface is designed for forward compatibility - new stages can be + * added without breaking existing hook implementations. + * + * IMPORTANT LIFETIME CONSIDERATIONS: + * - All context objects passed to hook stages are valid only during the + * execution of that stage. + * - Do not store references, pointers, or string_views from context objects. + * - If you need data beyond the immediate execution of a stage, copy it to + * owned types (std::string, Value, etc.). + * - EvaluationSeriesData should only be used within the stage or returned + * to be passed to the next stage. + */ +class Hook { + public: + virtual ~Hook() = default; + + /** + * Returns metadata about this hook. + * @return Reference to hook metadata. + */ + [[nodiscard]] virtual HookMetadata const& Metadata() const = 0; + + /** + * Called before a flag is evaluated. + * + * @param series_context Context for this evaluation (valid only during + * this call). + * @param data Data from previous stage (empty for first stage). + * @return Data to pass to the next stage. + */ + virtual EvaluationSeriesData BeforeEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data); + + /** + * Called after a flag has been evaluated. + * + * @param series_context Context for this evaluation (valid only during + * this call). + * @param data Data from the before stage. + * @param detail The evaluation result. + * @return Data to pass to the next stage (currently unused). + */ + virtual EvaluationSeriesData AfterEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data, + EvaluationDetail const& detail); + + /** + * Called after a custom event has been enqueued via Track. + * + * @param series_context Context for this track call (valid only during + * this call). + */ + virtual void AfterTrack(TrackSeriesContext const& series_context); + + protected: + Hook() = default; +}; + +} // namespace launchdarkly::server_side::hooks diff --git a/libs/server-sdk/src/CMakeLists.txt b/libs/server-sdk/src/CMakeLists.txt index 15543fb4a..0453258fe 100644 --- a/libs/server-sdk/src/CMakeLists.txt +++ b/libs/server-sdk/src/CMakeLists.txt @@ -2,6 +2,7 @@ file(GLOB HEADER_LIST CONFIGURE_DEPENDS "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/*.hpp" "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/integrations/*.hpp" + "${LaunchDarklyCPPServer_SOURCE_DIR}/include/launchdarkly/server_side/hooks/*.hpp" ) if (LD_BUILD_SHARED_LIBS) @@ -68,10 +69,20 @@ target_sources(${LIBNAME} evaluation/detail/semver_operations.cpp evaluation/detail/timestamp_operations.cpp events/event_factory.cpp + hooks/hook.cpp + hooks/hook_executor.hpp + hooks/hook_executor.cpp bindings/c/sdk.cpp bindings/c/builder.cpp bindings/c/config.cpp bindings/c/all_flags_state/all_flags_state.cpp + bindings/c/hook.cpp + bindings/c/hook_context.cpp + bindings/c/hook_wrapper.hpp + bindings/c/hook_wrapper.cpp + bindings/c/evaluation_series_context.cpp + bindings/c/evaluation_series_data.cpp + bindings/c/track_series_context.cpp ) diff --git a/libs/server-sdk/src/bindings/c/builder.cpp b/libs/server-sdk/src/bindings/c/builder.cpp index 655113720..fc1d73cbf 100644 --- a/libs/server-sdk/src/bindings/c/builder.cpp +++ b/libs/server-sdk/src/bindings/c/builder.cpp @@ -6,6 +6,8 @@ #include #include +#include "hook_wrapper.hpp" + using namespace launchdarkly::server_side; using namespace launchdarkly::server_side::config::builders; @@ -426,5 +428,18 @@ LDServerConfigBuilder_Logging_Custom(LDServerConfigBuilder b, LDLoggingCustomBuilder_Free(custom_builder); } +LD_EXPORT(void) +LDServerConfigBuilder_Hooks(LDServerConfigBuilder builder, + struct LDServerSDKHook hook) { + LD_ASSERT_NOT_NULL(builder); + LD_ASSERT_NOT_NULL(hook.Name); + + // Create a wrapper that bridges C callbacks to C++ Hook interface + auto hook_wrapper = std::make_shared(hook); + + // Register the wrapper with the config builder + TO_BUILDER(builder)->Hooks(hook_wrapper); +} + // NOLINTEND cppcoreguidelines-pro-type-reinterpret-cast // NOLINTEND OCInconsistentNamingInspection diff --git a/libs/server-sdk/src/bindings/c/evaluation_series_context.cpp b/libs/server-sdk/src/bindings/c/evaluation_series_context.cpp new file mode 100644 index 000000000..80a635546 --- /dev/null +++ b/libs/server-sdk/src/bindings/c/evaluation_series_context.cpp @@ -0,0 +1,59 @@ +#include +#include + +#include +#include +#include + +#define AS_EVAL_SERIES_CONTEXT(ptr) \ + (reinterpret_cast(ptr)) + +#define AS_CONTEXT(ptr) \ + (reinterpret_cast(const_cast(ptr))) + +#define AS_VALUE(ptr) \ + (reinterpret_cast(const_cast(ptr))) + +#define AS_HOOK_CONTEXT(ptr) \ + (reinterpret_cast(const_cast(ptr))) + +LD_EXPORT(char const*) +LDEvaluationSeriesContext_FlagKey(LDServerSDKEvaluationSeriesContext eval_context) { + LD_ASSERT(eval_context != nullptr); + return AS_EVAL_SERIES_CONTEXT(eval_context)->FlagKey().data(); +} + +LD_EXPORT(LDContext) +LDEvaluationSeriesContext_Context(LDServerSDKEvaluationSeriesContext eval_context) { + LD_ASSERT(eval_context != nullptr); + return AS_CONTEXT(&AS_EVAL_SERIES_CONTEXT(eval_context)->EvaluationContext()); +} + +LD_EXPORT(LDValue) +LDEvaluationSeriesContext_DefaultValue( + LDServerSDKEvaluationSeriesContext eval_context) { + LD_ASSERT(eval_context != nullptr); + // Return pointer to the value in the C++ context (no copy needed) + return AS_VALUE(&AS_EVAL_SERIES_CONTEXT(eval_context)->DefaultValue()); +} + +LD_EXPORT(char const*) +LDEvaluationSeriesContext_Method(LDServerSDKEvaluationSeriesContext eval_context) { + LD_ASSERT(eval_context != nullptr); + return AS_EVAL_SERIES_CONTEXT(eval_context)->Method().data(); +} + +LD_EXPORT(LDHookContext) +LDEvaluationSeriesContext_HookContext( + LDServerSDKEvaluationSeriesContext eval_context) { + LD_ASSERT(eval_context != nullptr); + return AS_HOOK_CONTEXT(&AS_EVAL_SERIES_CONTEXT(eval_context)->HookCtx()); +} + +LD_EXPORT(char const*) +LDEvaluationSeriesContext_EnvironmentId( + LDServerSDKEvaluationSeriesContext eval_context) { + LD_ASSERT(eval_context != nullptr); + auto env_id = AS_EVAL_SERIES_CONTEXT(eval_context)->EnvironmentId(); + return env_id.has_value() ? env_id->data() : nullptr; +} diff --git a/libs/server-sdk/src/bindings/c/evaluation_series_data.cpp b/libs/server-sdk/src/bindings/c/evaluation_series_data.cpp new file mode 100644 index 000000000..ee12b9b94 --- /dev/null +++ b/libs/server-sdk/src/bindings/c/evaluation_series_data.cpp @@ -0,0 +1,133 @@ +#include +#include + +#include +#include +#include +#include + +#define AS_EVAL_SERIES_DATA(ptr) \ + (reinterpret_cast(ptr)) + +#define AS_EVAL_SERIES_DATA_CONST(ptr) \ + (reinterpret_cast(ptr)) + +#define AS_EVAL_SERIES_DATA_BUILDER(ptr) \ + (reinterpret_cast(ptr)) + +#define AS_VALUE(ptr) \ + (reinterpret_cast(const_cast(ptr))) + +#define AS_CPP_VALUE(ptr) \ + (reinterpret_cast(ptr)) + +using launchdarkly::Value; +using launchdarkly::server_side::hooks::EvaluationSeriesData; +using launchdarkly::server_side::hooks::EvaluationSeriesDataBuilder; + +LD_EXPORT(LDServerSDKEvaluationSeriesData) +LDEvaluationSeriesData_New(void) { + return reinterpret_cast( + new EvaluationSeriesData()); +} + +LD_EXPORT(bool) +LDEvaluationSeriesData_GetValue(LDServerSDKEvaluationSeriesData data, + char const* key, + LDValue* out_value) { + LD_ASSERT(data != nullptr); + LD_ASSERT(key != nullptr); + LD_ASSERT(out_value != nullptr); + + if (const auto result = AS_EVAL_SERIES_DATA_CONST(data)->Get(key); result.has_value()) { + // Return a pointer to the existing value - no heap allocation needed + *out_value = AS_VALUE(&result->get()); + return true; + } + *out_value = nullptr; + return false; +} + +LD_EXPORT(bool) +LDEvaluationSeriesData_GetPointer(LDServerSDKEvaluationSeriesData data, + char const* key, + void** out_pointer) { + LD_ASSERT(data != nullptr); + LD_ASSERT(key != nullptr); + LD_ASSERT(out_pointer != nullptr); + + const auto result = AS_EVAL_SERIES_DATA_CONST(data)->GetShared(key); + if (result.has_value()) { + // Extract the raw pointer from shared_ptr + if (*result != nullptr) { + try { + *out_pointer = std::any_cast(*result->get()); + return true; + } catch (std::bad_any_cast const&) { + // The stored value wasn't a void*, return false + } + } + } + *out_pointer = nullptr; + return false; +} + +LD_EXPORT(LDServerSDKEvaluationSeriesDataBuilder) +LDEvaluationSeriesData_NewBuilder(LDServerSDKEvaluationSeriesData data) { + if (data) { + return reinterpret_cast( + new EvaluationSeriesDataBuilder(*AS_EVAL_SERIES_DATA_CONST(data))); + } + return reinterpret_cast( + new EvaluationSeriesDataBuilder()); +} + +LD_EXPORT(void) +LDEvaluationSeriesData_Free(LDServerSDKEvaluationSeriesData data) { + delete AS_EVAL_SERIES_DATA(data); +} + +LD_EXPORT(void) +LDEvaluationSeriesDataBuilder_SetValue( + LDServerSDKEvaluationSeriesDataBuilder builder, + char const* key, + LDValue value) { + LD_ASSERT(builder != nullptr); + LD_ASSERT(key != nullptr); + LD_ASSERT(value != nullptr); + + // Copy the value into the builder + AS_EVAL_SERIES_DATA_BUILDER(builder)->Set(key, *AS_CPP_VALUE(value)); +} + +LD_EXPORT(void) +LDEvaluationSeriesDataBuilder_SetPointer( + LDServerSDKEvaluationSeriesDataBuilder builder, + char const* key, + void* pointer) { + LD_ASSERT(builder != nullptr); + LD_ASSERT(key != nullptr); + + const auto shared_any = std::make_shared( + pointer); + // The "any" wrapper will be allocated and deleted, but the contents + // of the "any" will not. + AS_EVAL_SERIES_DATA_BUILDER(builder)->SetShared(key, shared_any); +} + +LD_EXPORT(LDServerSDKEvaluationSeriesData) +LDEvaluationSeriesDataBuilder_Build( + LDServerSDKEvaluationSeriesDataBuilder builder) { + LD_ASSERT(builder != nullptr); + + auto* cpp_builder = AS_EVAL_SERIES_DATA_BUILDER(builder); + auto* result = new EvaluationSeriesData(std::move(*cpp_builder).Build()); + delete cpp_builder; + return reinterpret_cast(result); +} + +LD_EXPORT(void) +LDEvaluationSeriesDataBuilder_Free( + LDServerSDKEvaluationSeriesDataBuilder builder) { + delete AS_EVAL_SERIES_DATA_BUILDER(builder); +} diff --git a/libs/server-sdk/src/bindings/c/hook.cpp b/libs/server-sdk/src/bindings/c/hook.cpp new file mode 100644 index 000000000..1752906fd --- /dev/null +++ b/libs/server-sdk/src/bindings/c/hook.cpp @@ -0,0 +1,16 @@ +#include + +#include + +#include + +LD_EXPORT(void) +LDServerSDKHook_Init(LDServerSDKHook* hook) { + LD_ASSERT(hook != nullptr); + + hook->Name = nullptr; + hook->BeforeEvaluation = nullptr; + hook->AfterEvaluation = nullptr; + hook->AfterTrack = nullptr; + hook->UserData = nullptr; +} diff --git a/libs/server-sdk/src/bindings/c/hook_context.cpp b/libs/server-sdk/src/bindings/c/hook_context.cpp new file mode 100644 index 000000000..0b2d338c7 --- /dev/null +++ b/libs/server-sdk/src/bindings/c/hook_context.cpp @@ -0,0 +1,54 @@ +#include +#include + +#include + +#define AS_HOOK_CONTEXT(ptr) \ + (reinterpret_cast(ptr)) + +using launchdarkly::server_side::hooks::HookContext; + +LD_EXPORT(LDHookContext) +LDHookContext_New() { + return reinterpret_cast(new HookContext()); +} + +LD_EXPORT(void) +LDHookContext_Set(LDHookContext hook_context, + char const* key, + void const* value) { + LD_ASSERT(hook_context != nullptr); + LD_ASSERT(key != nullptr); + + const auto shared_any = std::make_shared(value); + // The "any" wrapper will be allocated and deleted, but the contents + // of the "any" will not. + AS_HOOK_CONTEXT(hook_context)->Set(key, shared_any); +} + +LD_EXPORT(bool) +LDHookContext_Get(LDHookContext hook_context, + char const* key, + void const** out_value) { + LD_ASSERT(hook_context != nullptr); + LD_ASSERT(key != nullptr); + LD_ASSERT(out_value != nullptr); + + const auto result = AS_HOOK_CONTEXT(hook_context)->Get(key); + if (result.has_value() && *result != nullptr) { + try { + *out_value = std::any_cast(*result->get()); + return true; + } catch (std::bad_any_cast const&) { + // The stored value wasn't a void*, return false + // Should not generally be possible. + } + } + *out_value = nullptr; + return false; +} + +LD_EXPORT(void) +LDHookContext_Free(LDHookContext hook_context) { + delete AS_HOOK_CONTEXT(hook_context); +} diff --git a/libs/server-sdk/src/bindings/c/hook_wrapper.cpp b/libs/server-sdk/src/bindings/c/hook_wrapper.cpp new file mode 100644 index 000000000..4979c3b6b --- /dev/null +++ b/libs/server-sdk/src/bindings/c/hook_wrapper.cpp @@ -0,0 +1,146 @@ +#include "hook_wrapper.hpp" + +#include +#include +#include + +#include + +// Helper macros for type conversions +#define AS_EVAL_SERIES_CONTEXT(ptr) \ + (reinterpret_cast(const_cast(ptr))) + +#define AS_EVAL_SERIES_DATA(ptr) \ + (reinterpret_cast( \ + &const_cast(ptr))) + +#define AS_TRACK_SERIES_CONTEXT(ptr) \ + (reinterpret_cast(const_cast(ptr))) + +#define AS_EVAL_DETAIL(ptr) \ + (reinterpret_cast( \ + const_cast*>(ptr))) + +#define AS_CPP_EVAL_SERIES_DATA(ptr) \ + (reinterpret_cast(ptr)) + +namespace launchdarkly::server_side::bindings { + +CHookWrapper::CHookWrapper(struct LDServerSDKHook const& c_hook) + : metadata_(c_hook.Name ? c_hook.Name : ""), + before_evaluation_(c_hook.BeforeEvaluation), + after_evaluation_(c_hook.AfterEvaluation), + after_track_(c_hook.AfterTrack), + user_data_(c_hook.UserData) { + assert(c_hook.Name != nullptr && "Hook name must not be NULL"); +} + +hooks::HookMetadata const& CHookWrapper::Metadata() const { + return metadata_; +} + +hooks::EvaluationSeriesData CHookWrapper::BeforeEvaluation( + hooks::EvaluationSeriesContext const& series_context, + hooks::EvaluationSeriesData data) { + // If no callback is set, return the data unmodified + if (!before_evaluation_) { + return data; + } + + // Convert to C types - cast context directly + const auto c_series_context = + AS_EVAL_SERIES_CONTEXT(&series_context); + + // Create a heap-allocated copy of the data to pass to C callback + // This gives the callback ownership that it can return or modify + const auto c_data_input = + reinterpret_cast( + new hooks::EvaluationSeriesData(data)); + + // Call the C callback - context stays alive for entire call + LDServerSDKEvaluationSeriesData result_data = + before_evaluation_(c_series_context, c_data_input, user_data_); + + // Convert result back to C++ + if (result_data) { + // Check if callback returned a different pointer than input + // If so, we need to free the unused input + if (result_data != c_data_input) { + delete AS_CPP_EVAL_SERIES_DATA(c_data_input); + } + + // Take ownership of the returned data + hooks::EvaluationSeriesData cpp_result = + std::move(*AS_CPP_EVAL_SERIES_DATA(result_data)); + // Free the C wrapper (but not the contents, which were moved) + delete AS_CPP_EVAL_SERIES_DATA(result_data); + return cpp_result; + } + + // If NULL was returned, free the input data and return empty data + delete AS_CPP_EVAL_SERIES_DATA(c_data_input); + return {}; +} + +hooks::EvaluationSeriesData CHookWrapper::AfterEvaluation( + hooks::EvaluationSeriesContext const& series_context, + hooks::EvaluationSeriesData data, + EvaluationDetail const& detail) { + // If no callback is set, return the data unmodified + if (!after_evaluation_) { + return data; + } + + // Convert to C types - cast context directly + const auto c_series_context = + AS_EVAL_SERIES_CONTEXT(&series_context); + + // Create a heap-allocated copy of the data to pass to C callback + // This gives the callback ownership that it can return or modify + const auto c_data_input = + reinterpret_cast( + new hooks::EvaluationSeriesData(data)); + + const auto c_detail = AS_EVAL_DETAIL(&detail); + + // Call the C callback - context stays alive for entire call + LDServerSDKEvaluationSeriesData result_data = + after_evaluation_(c_series_context, c_data_input, c_detail, user_data_); + + // Convert result back to C++ + if (result_data) { + // Check if callback returned a different pointer than input + // If so, we need to free the unused input + if (result_data != c_data_input) { + delete AS_CPP_EVAL_SERIES_DATA(c_data_input); + } + + // Take ownership of the returned data + hooks::EvaluationSeriesData cpp_result = + std::move(*AS_CPP_EVAL_SERIES_DATA(result_data)); + // Free the C wrapper (but not the contents, which were moved) + delete AS_CPP_EVAL_SERIES_DATA(result_data); + return cpp_result; + } + + // If NULL was returned, free the input data and return empty data + delete AS_CPP_EVAL_SERIES_DATA(c_data_input); + return {}; +} + +void CHookWrapper::AfterTrack( + hooks::TrackSeriesContext const& series_context) { + // If no callback is set, do nothing + if (!after_track_) { + return; + } + + // Convert to C type - cast context directly + const auto c_series_context = + AS_TRACK_SERIES_CONTEXT(&series_context); + + // Call the C callback - context stays alive for entire call + after_track_(c_series_context, user_data_); +} + +} // namespace launchdarkly::server_side::bindings diff --git a/libs/server-sdk/src/bindings/c/hook_wrapper.hpp b/libs/server-sdk/src/bindings/c/hook_wrapper.hpp new file mode 100644 index 000000000..7d455818f --- /dev/null +++ b/libs/server-sdk/src/bindings/c/hook_wrapper.hpp @@ -0,0 +1,76 @@ +/** + * @file hook_wrapper.hpp + * @brief C++ wrapper that bridges C hook callbacks to C++ Hook interface. + * + * This internal class captures C callback function pointers and user data, + * then forwards hook method calls from the C++ Hook interface to the C callbacks. + * + * OWNERSHIP MODEL: + * - The wrapper is created as a shared_ptr and registered with ConfigBuilder + * - The wrapper copies the Name string during construction + * - UserData lifetime is managed by the C application + * - Function pointers are copied and assumed valid for the SDK lifetime + */ + +#pragma once + +#include +#include + +#include +#include + +namespace launchdarkly::server_side::bindings { + +/** + * @brief Wrapper that adapts C hook callbacks to C++ Hook interface. + * + * This class implements the C++ Hook interface and forwards all calls + * to the C callback functions provided by the application. + */ +class CHookWrapper final : public hooks::Hook { + public: + /** + * @brief Construct a hook wrapper from C hook struct. + * + * @param c_hook C hook structure containing callbacks and metadata. + * The Name string is copied. UserData pointer and function + * pointers are copied but the pointed-to data lifetime is + * managed by the caller. + */ + explicit CHookWrapper(struct LDServerSDKHook const& c_hook); + + /** + * @brief Get hook metadata. + */ + [[nodiscard]] hooks::HookMetadata const& Metadata() const override; + + /** + * @brief Forward beforeEvaluation to C callback if set. + */ + hooks::EvaluationSeriesData BeforeEvaluation( + hooks::EvaluationSeriesContext const& series_context, + hooks::EvaluationSeriesData data) override; + + /** + * @brief Forward afterEvaluation to C callback if set. + */ + hooks::EvaluationSeriesData AfterEvaluation( + hooks::EvaluationSeriesContext const& series_context, + hooks::EvaluationSeriesData data, + EvaluationDetail const& detail) override; + + /** + * @brief Forward afterTrack to C callback if set. + */ + void AfterTrack(hooks::TrackSeriesContext const& series_context) override; + + private: + hooks::HookMetadata metadata_; + LDServerSDKHook_BeforeEvaluation before_evaluation_; + LDServerSDKHook_AfterEvaluation after_evaluation_; + LDServerSDKHook_AfterTrack after_track_; + void* user_data_; +}; + +} // namespace launchdarkly::server_side::bindings diff --git a/libs/server-sdk/src/bindings/c/sdk.cpp b/libs/server-sdk/src/bindings/c/sdk.cpp index 087a17eb7..1c46f7d9f 100644 --- a/libs/server-sdk/src/bindings/c/sdk.cpp +++ b/libs/server-sdk/src/bindings/c/sdk.cpp @@ -407,5 +407,305 @@ LD_EXPORT(void) LDServerDataSourceStatus_Free(LDServerDataSourceStatus status) { delete TO_DATASOURCESTATUS(status); } +// HookContext variations + +#define AS_HOOK_CONTEXT(ptr) \ + (reinterpret_cast(ptr)) + +LD_EXPORT(void) +LDServerSDK_TrackEvent_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* event_name, + LDHookContext hook_context) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(event_name); + + if (hook_context) { + TO_SDK(sdk)->Track(*TO_CONTEXT(context), event_name, + *AS_HOOK_CONTEXT(hook_context)); + } else { + TO_SDK(sdk)->Track(*TO_CONTEXT(context), event_name); + } +} + +LD_EXPORT(void) +LDServerSDK_TrackMetric_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* event_name, + double metric_value, + LDValue data, + LDHookContext hook_context) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(event_name); + LD_ASSERT_NOT_NULL(data); + + if (hook_context) { + TO_SDK(sdk)->Track(*TO_CONTEXT(context), event_name, + *TO_VALUE(data), metric_value, + *AS_HOOK_CONTEXT(hook_context)); + } else { + TO_SDK(sdk)->Track(*TO_CONTEXT(context), event_name, + *TO_VALUE(data), metric_value); + } + + LDValue_Free(data); +} + +LD_EXPORT(void) +LDServerSDK_TrackData_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* event_name, + LDValue data, + LDHookContext hook_context) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(event_name); + LD_ASSERT_NOT_NULL(data); + + if (hook_context) { + TO_SDK(sdk)->Track(*TO_CONTEXT(context), event_name, + *TO_VALUE(data), *AS_HOOK_CONTEXT(hook_context)); + } else { + TO_SDK(sdk)->Track(*TO_CONTEXT(context), event_name, + *TO_VALUE(data)); + } + + LDValue_Free(data); +} + +LD_EXPORT(bool) +LDServerSDK_BoolVariation_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + bool default_value, + LDHookContext hook_context) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + + if (hook_context) { + return TO_SDK(sdk)->BoolVariation(*TO_CONTEXT(context), flag_key, + default_value, + *AS_HOOK_CONTEXT(hook_context)); + } else { + return TO_SDK(sdk)->BoolVariation(*TO_CONTEXT(context), flag_key, + default_value); + } +} + +LD_EXPORT(bool) +LDServerSDK_BoolVariationDetail_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + bool default_value, + LDHookContext hook_context, + LDEvalDetail* out_detail) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + LD_ASSERT_NOT_NULL(out_detail); + + auto result = hook_context + ? TO_SDK(sdk)->BoolVariationDetail(*TO_CONTEXT(context), + flag_key, default_value, + *AS_HOOK_CONTEXT(hook_context)) + : TO_SDK(sdk)->BoolVariationDetail(*TO_CONTEXT(context), + flag_key, default_value); + + *out_detail = FROM_DETAIL(new CEvaluationDetail(result)); + return result.Value(); +} + +LD_EXPORT(char*) +LDServerSDK_StringVariation_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + char const* default_value, + LDHookContext hook_context) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + LD_ASSERT_NOT_NULL(default_value); + + std::string result; + if (hook_context) { + result = TO_SDK(sdk)->StringVariation(*TO_CONTEXT(context), flag_key, + default_value, + *AS_HOOK_CONTEXT(hook_context)); + } else { + result = TO_SDK(sdk)->StringVariation(*TO_CONTEXT(context), flag_key, + default_value); + } + + char* value = static_cast(malloc(result.size() + 1)); + std::strcpy(value, result.c_str()); + return value; +} + +LD_EXPORT(char*) +LDServerSDK_StringVariationDetail_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + char const* default_value, + LDHookContext hook_context, + LDEvalDetail* out_detail) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + LD_ASSERT_NOT_NULL(default_value); + LD_ASSERT_NOT_NULL(out_detail); + + auto result = hook_context + ? TO_SDK(sdk)->StringVariationDetail(*TO_CONTEXT(context), + flag_key, default_value, + *AS_HOOK_CONTEXT(hook_context)) + : TO_SDK(sdk)->StringVariationDetail(*TO_CONTEXT(context), + flag_key, default_value); + + *out_detail = FROM_DETAIL(new CEvaluationDetail(result)); + + char* value = static_cast(malloc(result.Value().size() + 1)); + std::strcpy(value, result.Value().c_str()); + return value; +} + +LD_EXPORT(int) +LDServerSDK_IntVariation_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + int default_value, + LDHookContext hook_context) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + + if (hook_context) { + return TO_SDK(sdk)->IntVariation(*TO_CONTEXT(context), flag_key, + default_value, + *AS_HOOK_CONTEXT(hook_context)); + } else { + return TO_SDK(sdk)->IntVariation(*TO_CONTEXT(context), flag_key, + default_value); + } +} + +LD_EXPORT(int) +LDServerSDK_IntVariationDetail_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + int default_value, + LDHookContext hook_context, + LDEvalDetail* out_detail) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + LD_ASSERT_NOT_NULL(out_detail); + + auto result = hook_context + ? TO_SDK(sdk)->IntVariationDetail(*TO_CONTEXT(context), + flag_key, default_value, + *AS_HOOK_CONTEXT(hook_context)) + : TO_SDK(sdk)->IntVariationDetail(*TO_CONTEXT(context), + flag_key, default_value); + + *out_detail = FROM_DETAIL(new CEvaluationDetail(result)); + return result.Value(); +} + +LD_EXPORT(double) +LDServerSDK_DoubleVariation_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + double default_value, + LDHookContext hook_context) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + + if (hook_context) { + return TO_SDK(sdk)->DoubleVariation(*TO_CONTEXT(context), flag_key, + default_value, + *AS_HOOK_CONTEXT(hook_context)); + } else { + return TO_SDK(sdk)->DoubleVariation(*TO_CONTEXT(context), flag_key, + default_value); + } +} + +LD_EXPORT(double) +LDServerSDK_DoubleVariationDetail_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + double default_value, + LDHookContext hook_context, + LDEvalDetail* out_detail) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + LD_ASSERT_NOT_NULL(out_detail); + + auto result = hook_context + ? TO_SDK(sdk)->DoubleVariationDetail(*TO_CONTEXT(context), + flag_key, default_value, + *AS_HOOK_CONTEXT(hook_context)) + : TO_SDK(sdk)->DoubleVariationDetail(*TO_CONTEXT(context), + flag_key, default_value); + + *out_detail = FROM_DETAIL(new CEvaluationDetail(result)); + return result.Value(); +} + +LD_EXPORT(LDValue) +LDServerSDK_JsonVariation_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + LDValue default_value, + LDHookContext hook_context) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + LD_ASSERT_NOT_NULL(default_value); + + Value result; + if (hook_context) { + result = TO_SDK(sdk)->JsonVariation(*TO_CONTEXT(context), flag_key, + *TO_VALUE(default_value), + *AS_HOOK_CONTEXT(hook_context)); + } else { + result = TO_SDK(sdk)->JsonVariation(*TO_CONTEXT(context), flag_key, + *TO_VALUE(default_value)); + } + + return FROM_VALUE(new Value(std::move(result))); +} + +LD_EXPORT(LDValue) +LDServerSDK_JsonVariationDetail_WithHookContext(LDServerSDK sdk, + LDContext context, + char const* flag_key, + LDValue default_value, + LDHookContext hook_context, + LDEvalDetail* out_detail) { + LD_ASSERT_NOT_NULL(sdk); + LD_ASSERT_NOT_NULL(context); + LD_ASSERT_NOT_NULL(flag_key); + LD_ASSERT_NOT_NULL(default_value); + LD_ASSERT_NOT_NULL(out_detail); + + auto result = hook_context + ? TO_SDK(sdk)->JsonVariationDetail(*TO_CONTEXT(context), + flag_key, + *TO_VALUE(default_value), + *AS_HOOK_CONTEXT(hook_context)) + : TO_SDK(sdk)->JsonVariationDetail(*TO_CONTEXT(context), + flag_key, + *TO_VALUE(default_value)); + + *out_detail = FROM_DETAIL(new CEvaluationDetail(result)); + return FROM_VALUE(new Value(std::move(result.Value()))); +} + // NOLINTEND cppcoreguidelines-pro-type-reinterpret-cast // NOLINTEND OCInconsistentNamingInspection diff --git a/libs/server-sdk/src/bindings/c/track_series_context.cpp b/libs/server-sdk/src/bindings/c/track_series_context.cpp new file mode 100644 index 000000000..84e2066e3 --- /dev/null +++ b/libs/server-sdk/src/bindings/c/track_series_context.cpp @@ -0,0 +1,73 @@ +#include +#include + +#include +#include +#include + +#define AS_TRACK_SERIES_CONTEXT(ptr) \ + (reinterpret_cast(ptr)) + +#define AS_CONTEXT(ptr) \ + (reinterpret_cast(const_cast(ptr))) + +#define AS_VALUE(ptr) \ + (reinterpret_cast(const_cast(ptr))) + +#define AS_HOOK_CONTEXT(ptr) \ + (reinterpret_cast(const_cast(ptr))) + +LD_EXPORT(char const*) +LDTrackSeriesContext_Key(LDServerSDKTrackSeriesContext track_context) { + LD_ASSERT(track_context != nullptr); + return AS_TRACK_SERIES_CONTEXT(track_context)->Key().data(); +} + +LD_EXPORT(LDContext) +LDTrackSeriesContext_Context(LDServerSDKTrackSeriesContext track_context) { + LD_ASSERT(track_context != nullptr); + return AS_CONTEXT(&AS_TRACK_SERIES_CONTEXT(track_context)->TrackContext()); +} + +LD_EXPORT(bool) +LDTrackSeriesContext_Data(LDServerSDKTrackSeriesContext track_context, + LDValue* out_data) { + LD_ASSERT(track_context != nullptr); + LD_ASSERT(out_data != nullptr); + + auto data = AS_TRACK_SERIES_CONTEXT(track_context)->Data(); + if (data.has_value()) { + // Return a pointer to the existing value - no heap allocation needed + *out_data = AS_VALUE(&data->get()); + return true; + } + *out_data = nullptr; + return false; +} + +LD_EXPORT(bool) +LDTrackSeriesContext_MetricValue(LDServerSDKTrackSeriesContext track_context, + double* out_metric_value) { + LD_ASSERT(track_context != nullptr); + LD_ASSERT(out_metric_value != nullptr); + + auto metric = AS_TRACK_SERIES_CONTEXT(track_context)->MetricValue(); + if (metric.has_value()) { + *out_metric_value = *metric; + return true; + } + return false; +} + +LD_EXPORT(LDHookContext) +LDTrackSeriesContext_HookContext(LDServerSDKTrackSeriesContext track_context) { + LD_ASSERT(track_context != nullptr); + return AS_HOOK_CONTEXT(&AS_TRACK_SERIES_CONTEXT(track_context)->HookCtx()); +} + +LD_EXPORT(char const*) +LDTrackSeriesContext_EnvironmentId(LDServerSDKTrackSeriesContext track_context) { + LD_ASSERT(track_context != nullptr); + auto env_id = AS_TRACK_SERIES_CONTEXT(track_context)->EnvironmentId(); + return env_id.has_value() ? env_id->data() : nullptr; +} diff --git a/libs/server-sdk/src/client.cpp b/libs/server-sdk/src/client.cpp index 11ead4f16..711b439e1 100644 --- a/libs/server-sdk/src/client.cpp +++ b/libs/server-sdk/src/client.cpp @@ -47,14 +47,36 @@ void Client::Track(Context const& ctx, client->Track(ctx, std::move(event_name), std::move(data), metric_value); } +void Client::Track(Context const& ctx, + std::string event_name, + Value data, + double metric_value, + hooks::HookContext const& hook_context) { + client->Track(ctx, std::move(event_name), std::move(data), metric_value, + hook_context); +} + void Client::Track(Context const& ctx, std::string event_name, Value data) { client->Track(ctx, std::move(event_name), std::move(data)); } +void Client::Track(Context const& ctx, + std::string event_name, + Value data, + hooks::HookContext const& hook_context) { + client->Track(ctx, std::move(event_name), std::move(data), hook_context); +} + void Client::Track(Context const& ctx, std::string event_name) { client->Track(ctx, std::move(event_name)); } +void Client::Track(Context const& ctx, + std::string event_name, + hooks::HookContext const& hook_context) { + client->Track(ctx, std::move(event_name), hook_context); +} + void Client::FlushAsync() { client->FlushAsync(); } @@ -69,18 +91,41 @@ bool Client::BoolVariation(Context const& ctx, return client->BoolVariation(ctx, key, default_value); } +bool Client::BoolVariation(Context const& ctx, + FlagKey const& key, + bool default_value, + hooks::HookContext const& hook_context) { + return client->BoolVariation(ctx, key, default_value, hook_context); +} + EvaluationDetail Client::BoolVariationDetail(Context const& ctx, FlagKey const& key, bool default_value) { return client->BoolVariationDetail(ctx, key, default_value); } +EvaluationDetail Client::BoolVariationDetail( + Context const& ctx, + FlagKey const& key, + bool default_value, + hooks::HookContext const& hook_context) { + return client->BoolVariationDetail(ctx, key, default_value, hook_context); +} + std::string Client::StringVariation(Context const& ctx, FlagKey const& key, std::string default_value) { return client->StringVariation(ctx, key, std::move(default_value)); } +std::string Client::StringVariation(Context const& ctx, + FlagKey const& key, + std::string default_value, + hooks::HookContext const& hook_context) { + return client->StringVariation(ctx, key, std::move(default_value), + hook_context); +} + EvaluationDetail Client::StringVariationDetail( Context const& ctx, FlagKey const& key, @@ -88,42 +133,99 @@ EvaluationDetail Client::StringVariationDetail( return client->StringVariationDetail(ctx, key, std::move(default_value)); } +EvaluationDetail Client::StringVariationDetail( + Context const& ctx, + FlagKey const& key, + std::string default_value, + hooks::HookContext const& hook_context) { + return client->StringVariationDetail(ctx, key, std::move(default_value), + hook_context); +} + double Client::DoubleVariation(Context const& ctx, FlagKey const& key, double default_value) { return client->DoubleVariation(ctx, key, default_value); } +double Client::DoubleVariation(Context const& ctx, + FlagKey const& key, + double default_value, + hooks::HookContext const& hook_context) { + return client->DoubleVariation(ctx, key, default_value, hook_context); +} + EvaluationDetail Client::DoubleVariationDetail(Context const& ctx, FlagKey const& key, double default_value) { return client->DoubleVariationDetail(ctx, key, default_value); } +EvaluationDetail Client::DoubleVariationDetail( + Context const& ctx, + FlagKey const& key, + double default_value, + hooks::HookContext const& hook_context) { + return client->DoubleVariationDetail(ctx, key, default_value, + hook_context); +} + int Client::IntVariation(Context const& ctx, FlagKey const& key, int default_value) { return client->IntVariation(ctx, key, default_value); } +int Client::IntVariation(Context const& ctx, + FlagKey const& key, + int default_value, + hooks::HookContext const& hook_context) { + return client->IntVariation(ctx, key, default_value, hook_context); +} + EvaluationDetail Client::IntVariationDetail(Context const& ctx, FlagKey const& key, int default_value) { return client->IntVariationDetail(ctx, key, default_value); } +EvaluationDetail Client::IntVariationDetail( + Context const& ctx, + FlagKey const& key, + int default_value, + hooks::HookContext const& hook_context) { + return client->IntVariationDetail(ctx, key, default_value, hook_context); +} + Value Client::JsonVariation(Context const& ctx, FlagKey const& key, Value default_value) { return client->JsonVariation(ctx, key, std::move(default_value)); } +Value Client::JsonVariation(Context const& ctx, + FlagKey const& key, + Value default_value, + hooks::HookContext const& hook_context) { + return client->JsonVariation(ctx, key, std::move(default_value), + hook_context); +} + EvaluationDetail Client::JsonVariationDetail(Context const& ctx, FlagKey const& key, Value default_value) { return client->JsonVariationDetail(ctx, key, std::move(default_value)); } +EvaluationDetail Client::JsonVariationDetail( + Context const& ctx, + FlagKey const& key, + Value default_value, + hooks::HookContext const& hook_context) { + return client->JsonVariationDetail(ctx, key, std::move(default_value), + hook_context); +} + IDataSourceStatusProvider& Client::DataSourceStatus() { return client->DataSourceStatus(); } diff --git a/libs/server-sdk/src/client_impl.cpp b/libs/server-sdk/src/client_impl.cpp index b09f85c48..0c53440d7 100644 --- a/libs/server-sdk/src/client_impl.cpp +++ b/libs/server-sdk/src/client_impl.cpp @@ -36,6 +36,19 @@ auto const kAsioConcurrencyHint = 1; // connection in this amount of time. auto const kDataSourceShutdownWait = std::chrono::milliseconds(100); +// Hook method names +// Method names for hooks +static const std::string kMethodBoolVariation = "BoolVariation"; +static const std::string kMethodBoolVariationDetail = "BoolVariationDetail"; +static const std::string kMethodStringVariation = "StringVariation"; +static const std::string kMethodStringVariationDetail = "StringVariationDetail"; +static const std::string kMethodDoubleVariation = "DoubleVariation"; +static const std::string kMethodDoubleVariationDetail = "DoubleVariationDetail"; +static const std::string kMethodIntVariation = "IntVariation"; +static const std::string kMethodIntVariationDetail = "IntVariationDetail"; +static const std::string kMethodJsonVariation = "JsonVariation"; +static const std::string kMethodJsonVariationDetail = "JsonVariationDetail"; + static std::unique_ptr MakeDataSystem( config::built::HttpProperties const& http_properties, Config const& config, @@ -224,7 +237,25 @@ AllFlagsState ClientImpl::AllFlagsState(Context const& context, void ClientImpl::TrackInternal(Context const& ctx, std::string event_name, std::optional data, - std::optional metric_value) { + std::optional metric_value, + hooks::HookContext const& hook_context) { + + if (!ctx.Valid()) { + LD_LOG(logger_, LogLevel::kWarn) << "Track method called with an invalid context"; + return; + } + // Execute afterTrack hooks before moving the data + // Typically we would execute this after the data has been enqueued. + // In this SDK doing so would introduce a performance penalty because we + // would need to copy the event_name and data. + // In this SDK the data is type-safe, and will be enqueued, so it makes + // minimal functional difference. + if (!config_.Hooks().empty()) { + hooks::TrackSeriesContext series_context(ctx, event_name, metric_value, + data, hook_context, std::nullopt); + hooks::ExecuteAfterTrack(config_.Hooks(), series_context, logger_); + } + events_default_.Send([&](EventFactory const& factory) { return factory.Custom(ctx, std::move(event_name), std::move(data), metric_value); @@ -235,17 +266,45 @@ void ClientImpl::Track(Context const& ctx, std::string event_name, Value data, double metric_value) { + static hooks::HookContext empty_hook_context; + this->TrackInternal(ctx, std::move(event_name), std::move(data), + metric_value, empty_hook_context); +} + +void ClientImpl::Track(Context const& ctx, + std::string event_name, + Value data, + double metric_value, + hooks::HookContext const& hook_context) { this->TrackInternal(ctx, std::move(event_name), std::move(data), - metric_value); + metric_value, hook_context); } void ClientImpl::Track(Context const& ctx, std::string event_name, Value data) { + static hooks::HookContext empty_hook_context; + this->TrackInternal(ctx, std::move(event_name), std::move(data), + std::nullopt, empty_hook_context); +} + +void ClientImpl::Track(Context const& ctx, + std::string event_name, + Value data, + hooks::HookContext const& hook_context) { this->TrackInternal(ctx, std::move(event_name), std::move(data), - std::nullopt); + std::nullopt, hook_context); } void ClientImpl::Track(Context const& ctx, std::string event_name) { - this->TrackInternal(ctx, std::move(event_name), std::nullopt, std::nullopt); + static hooks::HookContext empty_hook_context; + this->TrackInternal(ctx, std::move(event_name), std::nullopt, std::nullopt, + empty_hook_context); +} + +void ClientImpl::Track(Context const& ctx, + std::string event_name, + hooks::HookContext const& hook_context) { + this->TrackInternal(ctx, std::move(event_name), std::nullopt, std::nullopt, + hook_context); } void ClientImpl::FlushAsync() { @@ -278,8 +337,11 @@ void ClientImpl::LogVariationCall(std::string const& key, Value ClientImpl::Variation(Context const& ctx, enum Value::Type value_type, IClient::FlagKey const& key, - Value const& default_value) { - auto result = *VariationInternal(ctx, key, default_value, events_default_); + Value const& default_value, + hooks::HookContext const& hook_context, + std::string const& method_name) { + auto result = *VariationInternal(ctx, key, default_value, events_default_, + hook_context, method_name); if (result.Type() != value_type) { return default_value; } @@ -290,10 +352,31 @@ EvaluationDetail ClientImpl::VariationInternal( Context const& context, IClient::FlagKey const& key, Value const& default_value, - EventScope const& event_scope) { + EventScope const& event_scope, + hooks::HookContext const& hook_context, + std::string const& method_name) { + // Execute beforeEvaluation hooks + std::optional executor; + if (!config_.Hooks().empty()) { + hooks::EvaluationSeriesContext series_context( + key, context, default_value, method_name, hook_context, std::nullopt); + // Executor only created if there are hooks. + executor.emplace(config_.Hooks(), logger_); + executor->BeforeEvaluation(series_context); + } + if (auto error = PreEvaluationChecks(context)) { - return PostEvaluation(key, context, default_value, *error, event_scope, - std::nullopt); + auto detail = PostEvaluation(key, context, default_value, *error, + event_scope, std::nullopt); + + // Execute afterEvaluation hooks + if (executor) { + hooks::EvaluationSeriesContext series_context( + key, context, default_value, method_name, hook_context, std::nullopt); + executor->AfterEvaluation(series_context, detail); + } + + return detail; } auto flag_rule = data_system_->GetFlag(key); @@ -303,15 +386,33 @@ EvaluationDetail ClientImpl::VariationInternal( LogVariationCall(key, flag_present); if (!flag_present) { - return PostEvaluation(key, context, default_value, - EvaluationReason::ErrorKind::kFlagNotFound, - event_scope, std::nullopt); + auto detail = PostEvaluation(key, context, default_value, + EvaluationReason::ErrorKind::kFlagNotFound, + event_scope, std::nullopt); + + // Execute afterEvaluation hooks + if (executor) { + hooks::EvaluationSeriesContext series_context( + key, context, default_value, method_name, hook_context, std::nullopt); + executor->AfterEvaluation(series_context, detail); + } + + return detail; } EvaluationDetail result = evaluator_.Evaluate(*flag_rule->item, context, event_scope); - return PostEvaluation(key, context, default_value, result, event_scope, - flag_rule.get()->item); + auto detail = PostEvaluation(key, context, default_value, result, + event_scope, flag_rule.get()->item); + + // Execute afterEvaluation hooks + if (executor) { + hooks::EvaluationSeriesContext series_context( + key, context, default_value, method_name, hook_context, std::nullopt); + executor->AfterEvaluation(series_context, detail); + } + + return detail; } std::optional ClientImpl::PreEvaluationChecks( @@ -373,67 +474,174 @@ EvaluationDetail ClientImpl::BoolVariationDetail( Context const& ctx, IClient::FlagKey const& key, bool default_value) { - return VariationDetail(ctx, Value::Type::kBool, key, default_value); + static hooks::HookContext empty_hook_context; + return VariationDetail(ctx, Value::Type::kBool, key, default_value, + empty_hook_context, kMethodBoolVariationDetail); +} + +EvaluationDetail ClientImpl::BoolVariationDetail( + Context const& ctx, + IClient::FlagKey const& key, + bool default_value, + hooks::HookContext const& hook_context) { + return VariationDetail(ctx, Value::Type::kBool, key, default_value, + hook_context, kMethodBoolVariationDetail); } bool ClientImpl::BoolVariation(Context const& ctx, IClient::FlagKey const& key, bool default_value) { - return Variation(ctx, Value::Type::kBool, key, default_value); + static hooks::HookContext empty_hook_context; + return Variation(ctx, Value::Type::kBool, key, default_value, + empty_hook_context, kMethodBoolVariation); +} + +bool ClientImpl::BoolVariation(Context const& ctx, + IClient::FlagKey const& key, + bool default_value, + hooks::HookContext const& hook_context) { + return Variation(ctx, Value::Type::kBool, key, default_value, + hook_context, kMethodBoolVariation); } EvaluationDetail ClientImpl::StringVariationDetail( Context const& ctx, ClientImpl::FlagKey const& key, std::string default_value) { + static hooks::HookContext empty_hook_context; return VariationDetail(ctx, Value::Type::kString, key, - default_value); + default_value, empty_hook_context, + kMethodStringVariationDetail); +} + +EvaluationDetail ClientImpl::StringVariationDetail( + Context const& ctx, + ClientImpl::FlagKey const& key, + std::string default_value, + hooks::HookContext const& hook_context) { + return VariationDetail(ctx, Value::Type::kString, key, + default_value, hook_context, + kMethodStringVariationDetail); } std::string ClientImpl::StringVariation(Context const& ctx, IClient::FlagKey const& key, std::string default_value) { - return Variation(ctx, Value::Type::kString, key, default_value); + static hooks::HookContext empty_hook_context; + return Variation(ctx, Value::Type::kString, key, default_value, + empty_hook_context, kMethodStringVariation); +} + +std::string ClientImpl::StringVariation(Context const& ctx, + IClient::FlagKey const& key, + std::string default_value, + hooks::HookContext const& hook_context) { + return Variation(ctx, Value::Type::kString, key, default_value, + hook_context, kMethodStringVariation); } EvaluationDetail ClientImpl::DoubleVariationDetail( Context const& ctx, ClientImpl::FlagKey const& key, double default_value) { + static hooks::HookContext empty_hook_context; return VariationDetail(ctx, Value::Type::kNumber, key, - default_value); + default_value, empty_hook_context, + kMethodDoubleVariationDetail); +} + +EvaluationDetail ClientImpl::DoubleVariationDetail( + Context const& ctx, + ClientImpl::FlagKey const& key, + double default_value, + hooks::HookContext const& hook_context) { + return VariationDetail(ctx, Value::Type::kNumber, key, + default_value, hook_context, + kMethodDoubleVariationDetail); } double ClientImpl::DoubleVariation(Context const& ctx, IClient::FlagKey const& key, double default_value) { - return Variation(ctx, Value::Type::kNumber, key, default_value); + static hooks::HookContext empty_hook_context; + return Variation(ctx, Value::Type::kNumber, key, default_value, + empty_hook_context, kMethodDoubleVariation); +} + +double ClientImpl::DoubleVariation(Context const& ctx, + IClient::FlagKey const& key, + double default_value, + hooks::HookContext const& hook_context) { + return Variation(ctx, Value::Type::kNumber, key, default_value, + hook_context, kMethodDoubleVariation); } EvaluationDetail ClientImpl::IntVariationDetail( Context const& ctx, IClient::FlagKey const& key, int default_value) { - return VariationDetail(ctx, Value::Type::kNumber, key, default_value); + static hooks::HookContext empty_hook_context; + return VariationDetail(ctx, Value::Type::kNumber, key, default_value, + empty_hook_context, kMethodIntVariationDetail); +} + +EvaluationDetail ClientImpl::IntVariationDetail( + Context const& ctx, + IClient::FlagKey const& key, + int default_value, + hooks::HookContext const& hook_context) { + return VariationDetail(ctx, Value::Type::kNumber, key, default_value, + hook_context, kMethodIntVariationDetail); } int ClientImpl::IntVariation(Context const& ctx, IClient::FlagKey const& key, int default_value) { - return Variation(ctx, Value::Type::kNumber, key, default_value); + static hooks::HookContext empty_hook_context; + return Variation(ctx, Value::Type::kNumber, key, default_value, + empty_hook_context, kMethodIntVariation); +} + +int ClientImpl::IntVariation(Context const& ctx, + IClient::FlagKey const& key, + int default_value, + hooks::HookContext const& hook_context) { + return Variation(ctx, Value::Type::kNumber, key, default_value, + hook_context, kMethodIntVariation); } EvaluationDetail ClientImpl::JsonVariationDetail( Context const& ctx, IClient::FlagKey const& key, Value default_value) { - return VariationInternal(ctx, key, default_value, events_with_reasons_); + static hooks::HookContext empty_hook_context; + return VariationInternal(ctx, key, default_value, events_with_reasons_, + empty_hook_context, kMethodJsonVariationDetail); +} + +EvaluationDetail ClientImpl::JsonVariationDetail( + Context const& ctx, + IClient::FlagKey const& key, + Value default_value, + hooks::HookContext const& hook_context) { + return VariationInternal(ctx, key, default_value, events_with_reasons_, + hook_context, kMethodJsonVariationDetail); } Value ClientImpl::JsonVariation(Context const& ctx, IClient::FlagKey const& key, Value default_value) { - return *VariationInternal(ctx, key, default_value, events_default_); + static hooks::HookContext empty_hook_context; + return *VariationInternal(ctx, key, default_value, events_default_, + empty_hook_context, kMethodJsonVariation); +} + +Value ClientImpl::JsonVariation(Context const& ctx, + IClient::FlagKey const& key, + Value default_value, + hooks::HookContext const& hook_context) { + return *VariationInternal(ctx, key, default_value, events_default_, + hook_context, kMethodJsonVariation); } IDataSourceStatusProvider& ClientImpl::DataSourceStatus() { diff --git a/libs/server-sdk/src/client_impl.hpp b/libs/server-sdk/src/client_impl.hpp index 1543e4925..2c29cf0c6 100644 --- a/libs/server-sdk/src/client_impl.hpp +++ b/libs/server-sdk/src/client_impl.hpp @@ -4,6 +4,7 @@ #include "data_interfaces/system/idata_system.hpp" #include "evaluation/evaluator.hpp" #include "events/event_scope.hpp" +#include "hooks/hook_executor.hpp" #include #include @@ -51,10 +52,25 @@ class ClientImpl : public IClient { Value data, double metric_value) override; + void Track(Context const& ctx, + std::string event_name, + Value data, + double metric_value, + hooks::HookContext const& hook_context) override; + void Track(Context const& ctx, std::string event_name, Value data) override; + void Track(Context const& ctx, + std::string event_name, + Value data, + hooks::HookContext const& hook_context) override; + void Track(Context const& ctx, std::string event_name) override; + void Track(Context const& ctx, + std::string event_name, + hooks::HookContext const& hook_context) override; + void FlushAsync() override; void Identify(Context context) override; @@ -63,44 +79,99 @@ class ClientImpl : public IClient { FlagKey const& key, bool default_value) override; + bool BoolVariation(Context const& ctx, + FlagKey const& key, + bool default_value, + hooks::HookContext const& hook_context) override; + EvaluationDetail BoolVariationDetail(Context const& ctx, FlagKey const& key, bool default_value) override; + EvaluationDetail BoolVariationDetail( + Context const& ctx, + FlagKey const& key, + bool default_value, + hooks::HookContext const& hook_context) override; + std::string StringVariation(Context const& ctx, FlagKey const& key, std::string default_value) override; + std::string StringVariation(Context const& ctx, + FlagKey const& key, + std::string default_value, + hooks::HookContext const& hook_context) override; + EvaluationDetail StringVariationDetail( Context const& ctx, FlagKey const& key, std::string default_value) override; + EvaluationDetail StringVariationDetail( + Context const& ctx, + FlagKey const& key, + std::string default_value, + hooks::HookContext const& hook_context) override; + double DoubleVariation(Context const& ctx, FlagKey const& key, double default_value) override; + double DoubleVariation(Context const& ctx, + FlagKey const& key, + double default_value, + hooks::HookContext const& hook_context) override; + EvaluationDetail DoubleVariationDetail( Context const& ctx, FlagKey const& key, double default_value) override; + EvaluationDetail DoubleVariationDetail( + Context const& ctx, + FlagKey const& key, + double default_value, + hooks::HookContext const& hook_context) override; + int IntVariation(Context const& ctx, FlagKey const& key, int default_value) override; + int IntVariation(Context const& ctx, + FlagKey const& key, + int default_value, + hooks::HookContext const& hook_context) override; + EvaluationDetail IntVariationDetail(Context const& ctx, FlagKey const& key, int default_value) override; + EvaluationDetail IntVariationDetail( + Context const& ctx, + FlagKey const& key, + int default_value, + hooks::HookContext const& hook_context) override; + Value JsonVariation(Context const& ctx, FlagKey const& key, Value default_value) override; + Value JsonVariation(Context const& ctx, + FlagKey const& key, + Value default_value, + hooks::HookContext const& hook_context) override; + EvaluationDetail JsonVariationDetail(Context const& ctx, FlagKey const& key, Value default_value) override; + EvaluationDetail JsonVariationDetail( + Context const& ctx, + FlagKey const& key, + Value default_value, + hooks::HookContext const& hook_context) override; + IDataSourceStatusProvider& DataSourceStatus() override; ~ClientImpl(); @@ -112,16 +183,21 @@ class ClientImpl : public IClient { Context const& ctx, FlagKey const& key, Value const& default_value, - EventScope const& scope); + EventScope const& scope, + hooks::HookContext const& hook_context, + std::string const& method_name); template [[nodiscard]] EvaluationDetail VariationDetail( Context const& ctx, enum Value::Type value_type, IClient::FlagKey const& key, - Value const& default_value) { + Value const& default_value, + hooks::HookContext const& hook_context, + std::string const& method_name) { auto result = - VariationInternal(ctx, key, default_value, events_with_reasons_); + VariationInternal(ctx, key, default_value, events_with_reasons_, + hook_context, method_name); if (result.Value().Type() == value_type) { return EvaluationDetail{result.Value(), result.VariationIndex(), result.Reason()}; @@ -133,7 +209,9 @@ class ClientImpl : public IClient { [[nodiscard]] Value Variation(Context const& ctx, enum Value::Type value_type, std::string const& key, - Value const& default_value); + Value const& default_value, + hooks::HookContext const& hook_context, + std::string const& method_name); [[nodiscard]] EvaluationDetail PostEvaluation( std::string const& key, @@ -150,7 +228,8 @@ class ClientImpl : public IClient { void TrackInternal(Context const& ctx, std::string event_name, std::optional data, - std::optional metric_value); + std::optional metric_value, + hooks::HookContext const& hook_context); std::future StartAsyncInternal( std::function predicate); diff --git a/libs/server-sdk/src/config/config.cpp b/libs/server-sdk/src/config/config.cpp index ff863d36b..31d326366 100644 --- a/libs/server-sdk/src/config/config.cpp +++ b/libs/server-sdk/src/config/config.cpp @@ -10,14 +10,16 @@ Config::Config(std::string sdk_key, built::Events events, std::optional application_tag, config::built::DataSystemConfig data_system_config, - built::HttpProperties http_properties) + built::HttpProperties http_properties, + std::vector> hooks) : sdk_key_(std::move(sdk_key)), logging_(std::move(logging)), service_endpoints_(std::move(service_endpoints)), events_(std::move(events)), application_tag_(std::move(application_tag)), data_system_config_(std::move(data_system_config)), - http_properties_(std::move(http_properties)) {} + http_properties_(std::move(http_properties)), + hooks_(std::move(hooks)) {} std::string const& Config::SdkKey() const { return sdk_key_; @@ -47,4 +49,8 @@ built::Logging const& Config::Logging() const { return logging_; } +std::vector> const& Config::Hooks() const { + return hooks_; +} + } // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/config/config_builder.cpp b/libs/server-sdk/src/config/config_builder.cpp index 647c9429d..26efa2deb 100644 --- a/libs/server-sdk/src/config/config_builder.cpp +++ b/libs/server-sdk/src/config/config_builder.cpp @@ -34,6 +34,13 @@ ConfigBuilder& ConfigBuilder::Offline(bool const offline) { return *this; } +ConfigBuilder& ConfigBuilder::Hooks(std::shared_ptr hook) { + if (hook) { + hooks_.push_back(std::move(hook)); + } + return *this; +} + tl::expected ConfigBuilder::Build() const { auto sdk_key = sdk_key_; if (sdk_key.empty()) { @@ -76,7 +83,8 @@ tl::expected ConfigBuilder::Build() const { *events_config, app_tag, std::move(*data_system_config), - std::move(http_properties)}; + std::move(http_properties), + hooks_}; } } // namespace launchdarkly::server_side diff --git a/libs/server-sdk/src/hooks/hook.cpp b/libs/server-sdk/src/hooks/hook.cpp new file mode 100644 index 000000000..2f155bfe1 --- /dev/null +++ b/libs/server-sdk/src/hooks/hook.cpp @@ -0,0 +1,203 @@ +#include + +#include + +namespace launchdarkly::server_side::hooks { + +// HookContext implementation + +HookContext& HookContext::Set(std::string key, + std::shared_ptr value) { + data_[std::move(key)] = std::move(value); + return *this; +} + +std::optional> HookContext::Get( + std::string const& key) const { + if (const auto it = data_.find(key); it != data_.end()) { + return it->second; + } + return std::nullopt; +} + +bool HookContext::Has(std::string const& key) const { + return data_.find(key) != data_.end(); +} + +// HookMetadata implementation + +HookMetadata::HookMetadata(std::string name) : name_(std::move(name)) {} + +std::string_view HookMetadata::Name() const { + return name_; +} + +// EvaluationSeriesData implementation + +EvaluationSeriesData::EvaluationSeriesData() = default; + +EvaluationSeriesData::EvaluationSeriesData( + std::map data) + : data_(std::move(data)) {} + +std::optional> EvaluationSeriesData::Get(std::string const& key) const { + if (const auto it = data_.find(key); it != data_.end() && it->second.value) { + return it->second.value; + } + return std::nullopt; +} + +std::optional> EvaluationSeriesData::GetShared( + std::string const& key) const { + if (const auto it = data_.find(key); it != data_.end() && it->second.shared) { + return it->second.shared; + } + return std::nullopt; +} + +bool EvaluationSeriesData::Has(std::string const& key) const { + return data_.find(key) != data_.end(); +} + +std::vector EvaluationSeriesData::Keys() const { + std::vector keys; + keys.reserve(data_.size()); + for (auto const& [key, _] : data_) { + keys.push_back(key); + } + return keys; +} + +// EvaluationSeriesDataBuilder implementation + +EvaluationSeriesDataBuilder::EvaluationSeriesDataBuilder() = default; + +EvaluationSeriesDataBuilder::EvaluationSeriesDataBuilder( + EvaluationSeriesData const& data) + : data_(data.data_) {} + +EvaluationSeriesDataBuilder& EvaluationSeriesDataBuilder::Set(std::string key, + Value value) { + EvaluationSeriesData::DataEntry entry; + entry.value = std::move(value); + data_[std::move(key)] = std::move(entry); + return *this; +} + +EvaluationSeriesDataBuilder& EvaluationSeriesDataBuilder::SetShared( + std::string key, + std::shared_ptr value) { + EvaluationSeriesData::DataEntry entry; + entry.shared = std::move(value); + data_[std::move(key)] = std::move(entry); + return *this; +} + +EvaluationSeriesData EvaluationSeriesDataBuilder::Build() const { + return EvaluationSeriesData(data_); +} + +// EvaluationSeriesContext implementation + +EvaluationSeriesContext::EvaluationSeriesContext( + std::string flag_key, + Context const& context, + Value default_value, + std::string method, + HookContext const& hook_context, + std::optional environment_id) + : flag_key_(std::move(flag_key)), + context_(context), + default_value_(std::move(default_value)), + method_(std::move(method)), + hook_context_(hook_context), + environment_id_(std::move(environment_id)) {} + +std::string_view EvaluationSeriesContext::FlagKey() const { + return flag_key_; +} + +Context const& EvaluationSeriesContext::EvaluationContext() const { + return context_; +} + +Value const& EvaluationSeriesContext::DefaultValue() const { + return default_value_; +} + +std::string_view EvaluationSeriesContext::Method() const { + return method_; +} + +std::optional EvaluationSeriesContext::EnvironmentId() const { + if (environment_id_) { + return *environment_id_; + } + return std::nullopt; +} + +HookContext const& EvaluationSeriesContext::HookCtx() const { + return hook_context_; +} + +// TrackSeriesContext implementation + +TrackSeriesContext::TrackSeriesContext( + Context const& context, + std::string key, + std::optional metric_value, + std::optional> data, + HookContext const& hook_context, + std::optional environment_id) + : context_(context), + key_(std::move(key)), + metric_value_(metric_value), + data_(data), + hook_context_(hook_context), + environment_id_(std::move(environment_id)) {} + +Context const& TrackSeriesContext::TrackContext() const { + return context_; +} + +std::string_view TrackSeriesContext::Key() const { + return key_; +} + +std::optional TrackSeriesContext::MetricValue() const { + return metric_value_; +} + +std::optional> TrackSeriesContext::Data() const { + return data_; +} + +std::optional TrackSeriesContext::EnvironmentId() const { + if (environment_id_) { + return *environment_id_; + } + return std::nullopt; +} + +HookContext const& TrackSeriesContext::HookCtx() const { + return hook_context_; +} + +// Hook base implementation + +EvaluationSeriesData Hook::BeforeEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data) { + return data; +} + +EvaluationSeriesData Hook::AfterEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data, + EvaluationDetail const& detail) { + return data; +} + +void Hook::AfterTrack(TrackSeriesContext const& series_context) {} + +} // namespace launchdarkly::server_side::hooks diff --git a/libs/server-sdk/src/hooks/hook_executor.cpp b/libs/server-sdk/src/hooks/hook_executor.cpp new file mode 100644 index 000000000..3c81f910a --- /dev/null +++ b/libs/server-sdk/src/hooks/hook_executor.cpp @@ -0,0 +1,90 @@ +#include "hook_executor.hpp" + +#include + +namespace launchdarkly::server_side::hooks { + +EvaluationSeriesExecutor::EvaluationSeriesExecutor( + std::vector> const& hooks, + Logger& logger) + : hooks_(hooks), logger_(logger) { + // Initialize series data storage for each hook + series_data_.resize(hooks_.size()); +} + +void EvaluationSeriesExecutor::BeforeEvaluation( + EvaluationSeriesContext const& context) { + // Execute hooks in order of registration + for (std::size_t i = 0; i < hooks_.size(); ++i) { + try { + series_data_[i] = + hooks_[i]->BeforeEvaluation(context, series_data_[i]); + } catch (std::exception const& e) { + LogHookError("BeforeEvaluation", + std::string(hooks_[i]->Metadata().Name()), + std::string(context.FlagKey()), e.what()); + // On error, use data from previous successful stage + // (already stored in series_data_[i]) + } catch (...) { + LogHookError("BeforeEvaluation", + std::string(hooks_[i]->Metadata().Name()), + std::string(context.FlagKey()), "unknown error"); + } + } +} + +void EvaluationSeriesExecutor::AfterEvaluation( + EvaluationSeriesContext const& context, + EvaluationDetail const& detail) { + // Execute hooks in reverse order of registration + for (std::size_t i = hooks_.size(); i-- > 0;) { + try { + series_data_[i] = + hooks_[i]->AfterEvaluation(context, series_data_[i], detail); + } catch (std::exception const& e) { + LogHookError("AfterEvaluation", + std::string(hooks_[i]->Metadata().Name()), + std::string(context.FlagKey()), e.what()); + // On error, use data from previous successful stage + } catch (...) { + LogHookError("AfterEvaluation", + std::string(hooks_[i]->Metadata().Name()), + std::string(context.FlagKey()), "unknown error"); + } + } +} + +void EvaluationSeriesExecutor::LogHookError( + std::string const& stage, + std::string const& hook_name, + std::string const& flag_name, + std::string const& error_message) const { + LD_LOG(logger_, LogLevel::kError) + << "[hooks] During evaluation of flag \"" << flag_name << "\", stage \"" + << stage << "\" of hook \"" << hook_name + << "\" reported error: " << error_message; +} + +void ExecuteAfterTrack(std::vector> const& hooks, + TrackSeriesContext const& context, + const Logger& logger) { + // Execute hooks in order of registration + for (auto const& hook : hooks) { + try { + hook->AfterTrack(context); + } catch (std::exception const& e) { + LD_LOG(logger, LogLevel::kError) + << "[hooks] During track of event \"" << context.Key() + << "\", stage \"AfterTrack\" of hook \"" + << hook->Metadata().Name() + << "\" reported error: " << e.what(); + } catch (...) { + LD_LOG(logger, LogLevel::kError) + << "[hooks] During track of event \"" << context.Key() + << "\", stage \"AfterTrack\" of hook \"" + << hook->Metadata().Name() << "\" reported error: unknown error"; + } + } +} + +} // namespace launchdarkly::server_side::hooks diff --git a/libs/server-sdk/src/hooks/hook_executor.hpp b/libs/server-sdk/src/hooks/hook_executor.hpp new file mode 100644 index 000000000..e1b175972 --- /dev/null +++ b/libs/server-sdk/src/hooks/hook_executor.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace launchdarkly::server_side::hooks { + +/** + * Executes hooks for a single evaluation series. + * Each instance handles one evaluation from beforeEvaluation through + * afterEvaluation. + * + * Handles exceptions/errors from hooks and logs them without affecting + * the evaluation operation. + * + * THREADING AND LIFETIME GUARANTEES: + * - All hook callbacks are executed SYNCHRONOUSLY in the same thread as the + * caller of the SDK method (e.g., BoolVariation, Track). + * - Hook callbacks complete before control returns to the caller. + * - Context objects (EvaluationSeriesContext, TrackSeriesContext) passed to + * hooks are stack-allocated and remain valid for the entire duration of + * the callback. + * - This synchronous execution model is CRITICAL for the C bindings, which + * return pointers to internal data that are only valid during the callback. + * Asynchronous execution would create dangling pointers. + * - Hook implementations MUST NOT store references, pointers, or string_views + * from context objects beyond the immediate callback execution. + * + * IMPLEMENTATION REQUIREMENTS: + * - Do NOT introduce async hook execution without redesigning the C bindings. + * - Do NOT move hook execution to background threads. + * - If async hooks are needed in the future, the C bindings must be changed + * to return heap-allocated copies instead of pointers to stack data. + */ +class EvaluationSeriesExecutor { + public: + /** + * Constructs an evaluation series executor. + * @param hooks The list of hooks to execute. + * @param logger Logger for reporting hook errors. + */ + EvaluationSeriesExecutor(std::vector> const& hooks, + Logger& logger); + + /** + * Executes beforeEvaluation stages for all hooks in order of registration. + * @param context The evaluation context. + */ + void BeforeEvaluation(EvaluationSeriesContext const& context); + + /** + * Executes afterEvaluation stages for all hooks in reverse order of + * registration. + * @param context The evaluation context. + * @param detail The evaluation result. + */ + void AfterEvaluation(EvaluationSeriesContext const& context, + EvaluationDetail const& detail); + + private: + std::vector> const& hooks_; + Logger& logger_; + + // Per-invocation series data for each hook. + // The outer vector index corresponds to hook index. + std::vector series_data_; + + void LogHookError(std::string const& stage, + std::string const& hook_name, + std::string const& flag_name, + std::string const& error_message) const; +}; + +/** + * Utility for executing track hooks. + * + * THREADING AND LIFETIME GUARANTEES: + * - Executes all hooks SYNCHRONOUSLY in the caller's thread. + * - The TrackSeriesContext parameter must remain valid for the entire + * execution (typically stack-allocated by the caller). + * - Returns only after all hook callbacks have completed. + * - This synchronous model is REQUIRED for C binding safety - the C bindings + * return pointers to data owned by the context parameter. + */ +void ExecuteAfterTrack(std::vector> const& hooks, + TrackSeriesContext const& context, + const Logger& logger); + +} // namespace launchdarkly::server_side::hooks diff --git a/libs/server-sdk/tests/hooks_test.cpp b/libs/server-sdk/tests/hooks_test.cpp new file mode 100644 index 000000000..beed215d1 --- /dev/null +++ b/libs/server-sdk/tests/hooks_test.cpp @@ -0,0 +1,1021 @@ +// Tests for hooks implementation based on: +// https://github.com/launchdarkly/sdk-specs/blob/main/specs/HOOK-hooks/README.md +// Spec commit: main branch as of implementation date +// +// This test file validates the hooks specification requirements for server-side SDKs. + +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace launchdarkly; +using namespace launchdarkly::server_side; +using namespace launchdarkly::server_side::hooks; + +// Test hook that records all method calls for verification +class RecordingHook : public Hook { + public: + struct EvaluationCall { + std::string stage; // "before" or "after" + std::string flag_key; + std::string method; + std::optional result_value; + }; + + struct TrackCall { + std::string key; + std::optional metric_value; + std::optional data; + }; + + explicit RecordingHook(std::string name) : metadata_(std::move(name)) {} + + HookMetadata const& Metadata() const override { return metadata_; } + + EvaluationSeriesData BeforeEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data) override { + EvaluationCall call; + call.stage = "before"; + call.flag_key = std::string(series_context.FlagKey()); + call.method = std::string(series_context.Method()); + evaluation_calls_.push_back(call); + + // Add some data to pass to after stage + EvaluationSeriesDataBuilder builder(data); + builder.Set("hook_name", Value(std::string(metadata_.Name()))); + builder.Set("call_count", Value(static_cast(evaluation_calls_.size()))); + return builder.Build(); + } + + EvaluationSeriesData AfterEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data, + EvaluationDetail const& detail) override { + EvaluationCall call; + call.stage = "after"; + call.flag_key = std::string(series_context.FlagKey()); + call.method = std::string(series_context.Method()); + call.result_value = detail.Value(); + evaluation_calls_.push_back(call); + + // Verify we can read the data from before stage + auto hook_name = data.Get("hook_name"); + if (hook_name) { + received_data_from_before_ = true; + } + + return data; + } + + void AfterTrack(TrackSeriesContext const& series_context) override { + TrackCall call; + call.key = std::string(series_context.Key()); + call.metric_value = series_context.MetricValue(); + call.data = series_context.Data(); + track_calls_.push_back(call); + } + + std::vector const& GetEvaluationCalls() const { + return evaluation_calls_; + } + + std::vector const& GetTrackCalls() const { + return track_calls_; + } + + bool ReceivedDataFromBefore() const { return received_data_from_before_; } + + void Reset() { + evaluation_calls_.clear(); + track_calls_.clear(); + received_data_from_before_ = false; + } + + private: + HookMetadata metadata_; + std::vector evaluation_calls_; + std::vector track_calls_; + bool received_data_from_before_ = false; +}; + +// Hook that throws exceptions to test error handling +class ErrorThrowingHook : public Hook { + public: + enum class ThrowStage { Before, After, Track, None }; + + ErrorThrowingHook(std::string name, ThrowStage throw_stage) + : metadata_(std::move(name)), throw_stage_(throw_stage) {} + + HookMetadata const& Metadata() const override { return metadata_; } + + EvaluationSeriesData BeforeEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data) override { + if (throw_stage_ == ThrowStage::Before) { + throw std::runtime_error("BeforeEvaluation error"); + } + return data; + } + + EvaluationSeriesData AfterEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data, + EvaluationDetail const& detail) override { + if (throw_stage_ == ThrowStage::After) { + throw std::runtime_error("AfterEvaluation error"); + } + return data; + } + + void AfterTrack(TrackSeriesContext const& series_context) override { + if (throw_stage_ == ThrowStage::Track) { + throw std::runtime_error("AfterTrack error"); + } + } + + private: + HookMetadata metadata_; + ThrowStage throw_stage_; +}; + +class HooksTest : public ::testing::Test { + protected: + HooksTest() + : context_(ContextBuilder().Kind("user", "test-user").Build()) {} + + Context const context_; +}; + +// Requirement 1.1.3: A hook MUST provide a getMetadata method +TEST_F(HooksTest, HookProvidesMetadata) { + auto hook = std::make_shared("TestHook"); + ASSERT_EQ(hook->Metadata().Name(), "TestHook"); +} + +// Requirement 1.2.1: Hooks MUST support a beforeEvaluation stage +TEST_F(HooksTest, BeforeEvaluationStageExecuted) { + auto hook = std::make_shared("TestHook"); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(hook) + .Build() + .value(); + + Client client(std::move(config)); + client.BoolVariation(context_, "test-flag", false); + + auto const& calls = hook->GetEvaluationCalls(); + ASSERT_EQ(calls.size(), 2); + EXPECT_EQ(calls[0].stage, "before"); + EXPECT_EQ(calls[0].flag_key, "test-flag"); +} + +// Requirement 1.2.2: Hooks MUST support an afterEvaluation stage +TEST_F(HooksTest, AfterEvaluationStageExecuted) { + auto hook = std::make_shared("TestHook"); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(hook) + .Build() + .value(); + + Client client(std::move(config)); + client.BoolVariation(context_, "test-flag", true); + + auto const& calls = hook->GetEvaluationCalls(); + ASSERT_EQ(calls.size(), 2); + EXPECT_EQ(calls[1].stage, "after"); + EXPECT_EQ(calls[1].flag_key, "test-flag"); + EXPECT_TRUE(calls[1].result_value.has_value()); +} + +// Requirement 1.2.2.3: The afterEvaluation stage MUST be executed with the +// EvaluationSeriesData returned by the previous stage +TEST_F(HooksTest, SeriesDataPropagatedBetweenStages) { + auto hook = std::make_shared("TestHook"); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(hook) + .Build() + .value(); + + Client client(std::move(config)); + client.BoolVariation(context_, "test-flag", false); + + EXPECT_TRUE(hook->ReceivedDataFromBefore()); +} + +// Requirement 1.3.1: The client MUST provide a mechanism of registering hooks +// during initialization +TEST_F(HooksTest, HooksRegisteredDuringInitialization) { + auto hook1 = std::make_shared("Hook1"); + auto hook2 = std::make_shared("Hook2"); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(hook1) + .Hooks(hook2) + .Build() + .value(); + + Client client(std::move(config)); + client.BoolVariation(context_, "test-flag", false); + + EXPECT_EQ(hook1->GetEvaluationCalls().size(), 2); + EXPECT_EQ(hook2->GetEvaluationCalls().size(), 2); +} + +// Requirement 1.3.4: The client MUST execute hooks in the following order: +// - beforeEvaluation: in order of registration +// - afterEvaluation: in reverse order of registration +TEST_F(HooksTest, HooksExecutedInCorrectOrder) { + auto hook1 = std::make_shared("Hook1"); + auto hook2 = std::make_shared("Hook2"); + auto hook3 = std::make_shared("Hook3"); + + // To verify order, we'll use a shared vector + auto execution_order = std::make_shared>(); + + class OrderTrackingHook : public Hook { + public: + OrderTrackingHook(std::string name, + std::shared_ptr> order) + : metadata_(std::move(name)), order_(std::move(order)) {} + + HookMetadata const& Metadata() const override { return metadata_; } + + EvaluationSeriesData BeforeEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data) override { + order_->push_back(std::string(metadata_.Name()) + "-before"); + return data; + } + + EvaluationSeriesData AfterEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data, + EvaluationDetail const& detail) override { + order_->push_back(std::string(metadata_.Name()) + "-after"); + return data; + } + + private: + HookMetadata metadata_; + std::shared_ptr> order_; + }; + + auto order_hook1 = + std::make_shared("Hook1", execution_order); + auto order_hook2 = + std::make_shared("Hook2", execution_order); + auto order_hook3 = + std::make_shared("Hook3", execution_order); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(order_hook1) + .Hooks(order_hook2) + .Hooks(order_hook3) + .Build() + .value(); + + Client client(std::move(config)); + client.BoolVariation(context_, "test-flag", false); + + // Expected order: Hook1-before, Hook2-before, Hook3-before, + // Hook3-after, Hook2-after, Hook1-after + ASSERT_EQ(execution_order->size(), 6); + EXPECT_EQ((*execution_order)[0], "Hook1-before"); + EXPECT_EQ((*execution_order)[1], "Hook2-before"); + EXPECT_EQ((*execution_order)[2], "Hook3-before"); + EXPECT_EQ((*execution_order)[3], "Hook3-after"); + EXPECT_EQ((*execution_order)[4], "Hook2-after"); + EXPECT_EQ((*execution_order)[5], "Hook1-after"); +} + +// Requirement 1.3.6: The client MUST support propagation of series data +// between stages in a series for a single invocation +TEST_F(HooksTest, SeriesDataIsolatedPerHook) { + class DataIsolationHook : public Hook { + public: + explicit DataIsolationHook(std::string name, std::string marker) + : metadata_(std::move(name)), marker_(std::move(marker)) {} + + HookMetadata const& Metadata() const override { return metadata_; } + + EvaluationSeriesData BeforeEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data) override { + EvaluationSeriesDataBuilder builder(data); + builder.Set("marker", Value(marker_)); + return builder.Build(); + } + + EvaluationSeriesData AfterEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data, + EvaluationDetail const& detail) override { + auto marker = data.Get("marker"); + EXPECT_TRUE(marker.has_value()); + if (marker) { + EXPECT_EQ(*marker, Value(marker_)); + received_correct_marker_ = true; + } + return data; + } + + bool ReceivedCorrectMarker() const { return received_correct_marker_; } + + private: + HookMetadata metadata_; + std::string marker_; + bool received_correct_marker_ = false; + }; + + auto hook1 = std::make_shared("Hook1", "marker1"); + auto hook2 = std::make_shared("Hook2", "marker2"); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(hook1) + .Hooks(hook2) + .Build() + .value(); + + Client client(std::move(config)); + client.BoolVariation(context_, "test-flag", false); + + // Each hook should have received its own marker, not the other's + EXPECT_TRUE(hook1->ReceivedCorrectMarker()); + EXPECT_TRUE(hook2->ReceivedCorrectMarker()); +} + +// Requirement 1.3.7: The client MUST handle exceptions which are thrown during +// the execution of a stage, allowing operations to complete unaffected +TEST_F(HooksTest, ExceptionsInBeforeEvaluationDoNotAffectEvaluation) { + auto error_hook = std::make_shared( + "ErrorHook", ErrorThrowingHook::ThrowStage::Before); + auto recording_hook = std::make_shared("RecordingHook"); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(error_hook) + .Hooks(recording_hook) + .Build() + .value(); + + Client client(std::move(config)); + + // Evaluation should still complete and return default value + bool result = client.BoolVariation(context_, "test-flag", true); + EXPECT_EQ(result, true); + + // Recording hook should still have been called + EXPECT_EQ(recording_hook->GetEvaluationCalls().size(), 2); +} + +// Requirement 1.3.7: Exceptions in afterEvaluation +TEST_F(HooksTest, ExceptionsInAfterEvaluationDoNotAffectEvaluation) { + auto error_hook = std::make_shared( + "ErrorHook", ErrorThrowingHook::ThrowStage::After); + auto recording_hook = std::make_shared("RecordingHook"); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(error_hook) + .Hooks(recording_hook) + .Build() + .value(); + + Client client(std::move(config)); + + // Evaluation should still complete and return default value + bool result = client.BoolVariation(context_, "test-flag", true); + EXPECT_EQ(result, true); + + // Recording hook should still have been called + EXPECT_EQ(recording_hook->GetEvaluationCalls().size(), 2); +} + +// Requirement 1.6.1: Hooks MUST support a afterTrack handler +TEST_F(HooksTest, AfterTrackHandlerExecuted) { + auto hook = std::make_shared("TestHook"); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(hook) + .Build() + .value(); + + Client client(std::move(config)); + client.Track(context_, "test-event"); + + auto const& calls = hook->GetTrackCalls(); + ASSERT_EQ(calls.size(), 1); + EXPECT_EQ(calls[0].key, "test-event"); +} + +// Requirement 1.6.1: afterTrack with metric value +TEST_F(HooksTest, AfterTrackWithMetricValue) { + auto hook = std::make_shared("TestHook"); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(hook) + .Build() + .value(); + + Client client(std::move(config)); + client.Track(context_, "test-event", Value::Null(), 42.5); + + auto const& calls = hook->GetTrackCalls(); + ASSERT_EQ(calls.size(), 1); + EXPECT_EQ(calls[0].key, "test-event"); + ASSERT_TRUE(calls[0].metric_value.has_value()); + EXPECT_EQ(*calls[0].metric_value, 42.5); +} + +// Test that data is properly passed to afterTrack hooks +// This test would catch use-after-move bugs with ASAN +TEST_F(HooksTest, AfterTrackReceivesData) { + auto hook = std::make_shared("TestHook"); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(hook) + .Build() + .value(); + + Client client(std::move(config)); + + // Test with string data + client.Track(context_, "test-event", Value("test-data")); + + auto const& calls = hook->GetTrackCalls(); + ASSERT_EQ(calls.size(), 1); + EXPECT_EQ(calls[0].key, "test-event"); + ASSERT_TRUE(calls[0].data.has_value()); + EXPECT_EQ(calls[0].data->AsString(), "test-data"); + + hook->Reset(); + + // Test with complex data + auto complex_data = Value::Object({{"field1", Value("value1")}, {"field2", Value(42)}}); + client.Track(context_, "test-event-2", complex_data, 99.5); + + auto const& calls2 = hook->GetTrackCalls(); + ASSERT_EQ(calls2.size(), 1); + EXPECT_EQ(calls2[0].key, "test-event-2"); + ASSERT_TRUE(calls2[0].data.has_value()); + EXPECT_EQ(calls2[0].data->Type(), Value::Type::kObject); + auto const& obj = calls2[0].data->AsObject(); + EXPECT_EQ(obj["field1"].AsString(), "value1"); + EXPECT_EQ(obj["field2"].AsInt(), 42); + ASSERT_TRUE(calls2[0].metric_value.has_value()); + EXPECT_EQ(*calls2[0].metric_value, 99.5); +} + +// Requirement 1.3.4: afterTrack handlers execute in order of registration +TEST_F(HooksTest, AfterTrackExecutesInOrder) { + auto execution_order = std::make_shared>(); + + class OrderTrackingTrackHook : public Hook { + public: + OrderTrackingTrackHook(std::string name, + std::shared_ptr> order) + : metadata_(std::move(name)), order_(std::move(order)) {} + + HookMetadata const& Metadata() const override { return metadata_; } + + void AfterTrack(TrackSeriesContext const& series_context) override { + order_->push_back(std::string(metadata_.Name())); + } + + private: + HookMetadata metadata_; + std::shared_ptr> order_; + }; + + auto hook1 = + std::make_shared("Hook1", execution_order); + auto hook2 = + std::make_shared("Hook2", execution_order); + auto hook3 = + std::make_shared("Hook3", execution_order); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(hook1) + .Hooks(hook2) + .Hooks(hook3) + .Build() + .value(); + + Client client(std::move(config)); + client.Track(context_, "test-event"); + + // Expected order: Hook1, Hook2, Hook3 + ASSERT_EQ(execution_order->size(), 3); + EXPECT_EQ((*execution_order)[0], "Hook1"); + EXPECT_EQ((*execution_order)[1], "Hook2"); + EXPECT_EQ((*execution_order)[2], "Hook3"); +} + +// Test that exceptions in afterTrack don't affect tracking +TEST_F(HooksTest, ExceptionsInAfterTrackDoNotAffectTracking) { + auto error_hook = std::make_shared( + "ErrorHook", ErrorThrowingHook::ThrowStage::Track); + auto recording_hook = std::make_shared("RecordingHook"); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(error_hook) + .Hooks(recording_hook) + .Build() + .value(); + + Client client(std::move(config)); + client.Track(context_, "test-event"); + + // Recording hook should still have been called despite error in first hook + EXPECT_EQ(recording_hook->GetTrackCalls().size(), 1); +} + +// Test evaluation context provides correct information +TEST_F(HooksTest, EvaluationContextProvidesCorrectInformation) { + class ContextVerifyingHook : public Hook { + public: + explicit ContextVerifyingHook(std::string name) + : metadata_(std::move(name)) {} + + HookMetadata const& Metadata() const override { return metadata_; } + + EvaluationSeriesData BeforeEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data) override { + flag_key_ = std::string(series_context.FlagKey()); + method_ = std::string(series_context.Method()); + default_value_ = series_context.DefaultValue(); + has_context_ = series_context.EvaluationContext().Valid(); + return data; + } + + std::string flag_key_; + std::string method_; + Value default_value_; + bool has_context_ = false; + + private: + HookMetadata metadata_; + }; + + auto hook = std::make_shared("TestHook"); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(hook) + .Build() + .value(); + + Client client(std::move(config)); + client.StringVariation(context_, "test-flag", "default-value"); + + EXPECT_EQ(hook->flag_key_, "test-flag"); + EXPECT_EQ(hook->method_, "StringVariation"); + EXPECT_EQ(hook->default_value_, Value("default-value")); + EXPECT_TRUE(hook->has_context_); +} + +// Test that Variation vs VariationDetail methods are correctly identified +TEST_F(HooksTest, MethodNameDistinguishesVariationTypes) { + class MethodVerifyingHook : public Hook { + public: + explicit MethodVerifyingHook(std::string name) + : metadata_(std::move(name)) {} + + HookMetadata const& Metadata() const override { return metadata_; } + + EvaluationSeriesData BeforeEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data) override { + methods_.push_back(std::string(series_context.Method())); + return data; + } + + std::vector methods_; + + private: + HookMetadata metadata_; + }; + + auto hook = std::make_shared("TestHook"); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(hook) + .Build() + .value(); + + Client client(std::move(config)); + + // Test all variation methods + client.BoolVariation(context_, "test-flag", false); + client.BoolVariationDetail(context_, "test-flag", false); + client.StringVariation(context_, "test-flag", "default"); + client.StringVariationDetail(context_, "test-flag", "default"); + client.DoubleVariation(context_, "test-flag", 0.0); + client.DoubleVariationDetail(context_, "test-flag", 0.0); + client.IntVariation(context_, "test-flag", 0); + client.IntVariationDetail(context_, "test-flag", 0); + client.JsonVariation(context_, "test-flag", Value::Null()); + client.JsonVariationDetail(context_, "test-flag", Value::Null()); + + ASSERT_EQ(hook->methods_.size(), 10); + EXPECT_EQ(hook->methods_[0], "BoolVariation"); + EXPECT_EQ(hook->methods_[1], "BoolVariationDetail"); + EXPECT_EQ(hook->methods_[2], "StringVariation"); + EXPECT_EQ(hook->methods_[3], "StringVariationDetail"); + EXPECT_EQ(hook->methods_[4], "DoubleVariation"); + EXPECT_EQ(hook->methods_[5], "DoubleVariationDetail"); + EXPECT_EQ(hook->methods_[6], "IntVariation"); + EXPECT_EQ(hook->methods_[7], "IntVariationDetail"); + EXPECT_EQ(hook->methods_[8], "JsonVariation"); + EXPECT_EQ(hook->methods_[9], "JsonVariationDetail"); +} + +// Test EvaluationSeriesData builder functionality +TEST_F(HooksTest, EvaluationSeriesDataBuilder) { + EvaluationSeriesDataBuilder builder; + builder.Set("key1", Value("value1")); + builder.Set("key2", Value(42)); + builder.Set("key3", Value(true)); + + auto data = builder.Build(); + + EXPECT_TRUE(data.Has("key1")); + EXPECT_TRUE(data.Has("key2")); + EXPECT_TRUE(data.Has("key3")); + EXPECT_FALSE(data.Has("nonexistent")); + + auto value1 = data.Get("key1"); + ASSERT_TRUE(value1.has_value()); + EXPECT_EQ(*value1, Value("value1")); + + auto value2 = data.Get("key2"); + ASSERT_TRUE(value2.has_value()); + EXPECT_EQ(*value2, Value(42)); + + auto keys = data.Keys(); + EXPECT_EQ(keys.size(), 3); +} + +// Test that series data can be built from existing data +TEST_F(HooksTest, EvaluationSeriesDataBuilderFromExisting) { + EvaluationSeriesDataBuilder initial_builder; + initial_builder.Set("existing", Value("data")); + auto initial_data = initial_builder.Build(); + + EvaluationSeriesDataBuilder builder(initial_data); + builder.Set("new", Value("value")); + + auto data = builder.Build(); + + EXPECT_TRUE(data.Has("existing")); + EXPECT_TRUE(data.Has("new")); + + auto existing = data.Get("existing"); + ASSERT_TRUE(existing.has_value()); + EXPECT_EQ(*existing, Value("data")); + + auto new_val = data.Get("new"); + ASSERT_TRUE(new_val.has_value()); + EXPECT_EQ(*new_val, Value("value")); +} + +// Test storing and retrieving shared_ptr for arbitrary objects like spans +TEST_F(HooksTest, EvaluationSeriesDataStoresSharedPtr) { + // Simulate a span object + struct MockSpan { + std::string trace_id = "trace-123"; + bool closed = false; + }; + + EvaluationSeriesDataBuilder builder; + auto span = std::make_shared(MockSpan{}); + builder.SetShared("span", span); + builder.Set("string_value", Value("test")); + + auto data = builder.Build(); + + EXPECT_TRUE(data.Has("span")); + EXPECT_TRUE(data.Has("string_value")); + + // Retrieve the span + auto retrieved_span = data.GetShared("span"); + ASSERT_TRUE(retrieved_span.has_value()); + + // Cast back to the original type + auto typed_span = std::any_cast(*(*retrieved_span)); + EXPECT_EQ(typed_span.trace_id, "trace-123"); + EXPECT_FALSE(typed_span.closed); + + // Verify we can still get regular values + auto string_val = data.Get("string_value"); + ASSERT_TRUE(string_val.has_value()); + EXPECT_EQ(*string_val, Value("test")); +} + +// Test that Get returns nullopt for shared_ptr keys +TEST_F(HooksTest, EvaluationSeriesDataGetReturnsNulloptForSharedKeys) { + struct MockSpan {}; + + EvaluationSeriesDataBuilder builder; + auto span = std::make_shared(MockSpan{}); + builder.SetShared("span", span); + + auto data = builder.Build(); + + // Get should return nullopt for shared_ptr keys + auto value = data.Get("span"); + EXPECT_FALSE(value.has_value()); + + // GetShared should work + auto shared = data.GetShared("span"); + EXPECT_TRUE(shared.has_value()); +} + +// Test that GetShared returns nullopt for Value keys +TEST_F(HooksTest, EvaluationSeriesDataGetSharedReturnsNulloptForValueKeys) { + EvaluationSeriesDataBuilder builder; + builder.Set("value", Value(42)); + + auto data = builder.Build(); + + // GetShared should return nullopt for Value keys + auto shared = data.GetShared("value"); + EXPECT_FALSE(shared.has_value()); + + // Get should work + auto value = data.Get("value"); + ASSERT_TRUE(value.has_value()); + EXPECT_EQ(*value, Value(42)); +} + +// Test span use case: create in beforeEvaluation, close in afterEvaluation +TEST_F(HooksTest, SpanLifecycleAcrossEvaluationStages) { + struct MockSpan { + std::string trace_id; + bool closed = false; + + void Close() { closed = true; } + }; + + class SpanHook : public Hook { + public: + explicit SpanHook(std::string name) : metadata_(std::move(name)) {} + + HookMetadata const& Metadata() const override { return metadata_; } + + EvaluationSeriesData BeforeEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data) override { + auto span = std::make_shared( + MockSpan{"trace-" + std::string(series_context.FlagKey())}); + + EvaluationSeriesDataBuilder builder(data); + builder.SetShared("span", span); + return builder.Build(); + } + + EvaluationSeriesData AfterEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data, + EvaluationDetail const& detail) override { + auto span_any = data.GetShared("span"); + if (span_any) { + auto& span = std::any_cast(*(*span_any)); + span.Close(); + span_closed_ = span.closed; + } + return data; + } + + bool span_closed_ = false; + + private: + HookMetadata metadata_; + }; + + auto hook = std::make_shared("SpanHook"); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(hook) + .Build() + .value(); + + Client client(std::move(config)); + client.BoolVariation(context_, "test-flag", false); + + EXPECT_TRUE(hook->span_closed_); +} + +// Test that shared_ptr allows mutation of the stored object +TEST_F(HooksTest, SharedPtrAllowsMutationAcrossStages) { + struct Counter { + int count = 0; + }; + + class CounterHook : public Hook { + public: + explicit CounterHook(std::string name) : metadata_(std::move(name)) {} + + HookMetadata const& Metadata() const override { return metadata_; } + + EvaluationSeriesData BeforeEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data) override { + auto counter = std::make_shared(Counter{}); + + // Mutate the counter + auto& c = std::any_cast(*counter); + c.count = 10; + + EvaluationSeriesDataBuilder builder(data); + builder.SetShared("counter", counter); + return builder.Build(); + } + + EvaluationSeriesData AfterEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data, + EvaluationDetail const& detail) override { + auto counter_any = data.GetShared("counter"); + if (counter_any) { + // Verify we can still mutate it + auto& counter = std::any_cast(*(*counter_any)); + counter.count += 5; + final_count_ = counter.count; + } + return data; + } + + int final_count_ = 0; + + private: + HookMetadata metadata_; + }; + + auto hook = std::make_shared("CounterHook"); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(hook) + .Build() + .value(); + + Client client(std::move(config)); + client.BoolVariation(context_, "test-flag", false); + + EXPECT_EQ(hook->final_count_, 15); +} + +// Test HookContext for passing caller data to hooks (e.g., OpenTelemetry span parents) +TEST_F(HooksTest, HookContextBasicFunctionality) { + HookContext ctx; + + // Set values + auto span_parent = std::make_shared(std::string("span-parent-123")); + auto trace_id = std::make_shared(42); + + ctx.Set("span_parent", span_parent); + ctx.Set("trace_id", trace_id); + + // Check existence + EXPECT_TRUE(ctx.Has("span_parent")); + EXPECT_TRUE(ctx.Has("trace_id")); + EXPECT_FALSE(ctx.Has("nonexistent")); + + // Retrieve values + auto retrieved_span = ctx.Get("span_parent"); + ASSERT_TRUE(retrieved_span.has_value()); + auto span_str = std::any_cast(*(*retrieved_span)); + EXPECT_EQ(span_str, "span-parent-123"); + + auto retrieved_trace = ctx.Get("trace_id"); + ASSERT_TRUE(retrieved_trace.has_value()); + auto trace_val = std::any_cast(*(*retrieved_trace)); + EXPECT_EQ(trace_val, 42); +} + +// Test that hooks can access HookContext from EvaluationSeriesContext +// This simulates the OpenTelemetry span parent use case +TEST_F(HooksTest, HookAccessesCallerProvidedContext) { + struct SpanContext { + std::string trace_id; + std::string parent_span_id; + }; + + class OTelHook : public Hook { + public: + explicit OTelHook(std::string name) : metadata_(std::move(name)) {} + + HookMetadata const& Metadata() const override { return metadata_; } + + EvaluationSeriesData BeforeEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data) override { + // Access span parent from caller's context + auto span_parent_any = series_context.HookCtx().Get("span_parent"); + if (span_parent_any) { + auto span_parent = + std::any_cast(*(*span_parent_any)); + received_trace_id_ = span_parent.trace_id; + received_parent_span_ = span_parent.parent_span_id; + + // Create new span as child + auto new_span = std::make_shared(SpanContext{ + span_parent.trace_id, "child-span-" + received_parent_span_}); + + EvaluationSeriesDataBuilder builder(data); + builder.SetShared("span", new_span); + return builder.Build(); + } + return data; + } + + EvaluationSeriesData AfterEvaluation( + EvaluationSeriesContext const& series_context, + EvaluationSeriesData data, + EvaluationDetail const& detail) override { + // Close the span + auto span_any = data.GetShared("span"); + if (span_any) { + auto span = std::any_cast(*(*span_any)); + closed_span_trace_ = span.trace_id; + } + return data; + } + + std::string received_trace_id_; + std::string received_parent_span_; + std::string closed_span_trace_; + + private: + HookMetadata metadata_; + }; + + auto hook = std::make_shared("OTelHook"); + + auto config = ConfigBuilder("sdk-key") + .Offline(true) + .Hooks(hook) + .Build() + .value(); + + Client client(std::move(config)); + + // Create a HookContext with span parent information (simulating OpenTelemetry) + HookContext hook_context; + SpanContext span_parent{"trace-123", "parent-span-456"}; + hook_context.Set("span_parent", + std::make_shared(span_parent)); + + // Call variation with the HookContext + client.BoolVariation(context_, "test-flag", false, hook_context); + + // Verify hook received the span parent information + EXPECT_EQ(hook->received_trace_id_, "trace-123"); + EXPECT_EQ(hook->received_parent_span_, "parent-span-456"); + EXPECT_EQ(hook->closed_span_trace_, "trace-123"); +} + +// Test HookContext chaining +TEST_F(HooksTest, HookContextChaining) { + HookContext ctx; + auto val1 = std::make_shared(1); + auto val2 = std::make_shared(2); + + ctx.Set("key1", val1).Set("key2", val2); + + EXPECT_TRUE(ctx.Has("key1")); + EXPECT_TRUE(ctx.Has("key2")); + + auto retrieved1 = ctx.Get("key1"); + ASSERT_TRUE(retrieved1.has_value()); + EXPECT_EQ(std::any_cast(*(*retrieved1)), 1); + + auto retrieved2 = ctx.Get("key2"); + ASSERT_TRUE(retrieved2.has_value()); + EXPECT_EQ(std::any_cast(*(*retrieved2)), 2); +} diff --git a/libs/server-sdk/tests/server_c_bindings_hooks_test.cpp b/libs/server-sdk/tests/server_c_bindings_hooks_test.cpp new file mode 100644 index 000000000..c53dc2ee5 --- /dev/null +++ b/libs/server-sdk/tests/server_c_bindings_hooks_test.cpp @@ -0,0 +1,625 @@ +#include "gtest/gtest.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include + +#include +#include + +// Test tracker to verify hook execution +struct HookCallTracker { + std::vector calls; + int before_eval_count = 0; + int after_eval_count = 0; + int after_track_count = 0; +}; + +// C callback functions for testing + +static LDServerSDKEvaluationSeriesData TestHook_BeforeEvaluation( + LDServerSDKEvaluationSeriesContext series_context, + LDServerSDKEvaluationSeriesData data, + void* user_data) { + auto* tracker = static_cast(user_data); + tracker->before_eval_count++; + tracker->calls.push_back("before_eval"); + + // Return the data unchanged for basic test + return data; +} + +static LDServerSDKEvaluationSeriesData TestHook_AfterEvaluation( + LDServerSDKEvaluationSeriesContext series_context, + LDServerSDKEvaluationSeriesData data, + LDEvalDetail detail, + void* user_data) { + auto* tracker = static_cast(user_data); + tracker->after_eval_count++; + tracker->calls.push_back("after_eval"); + + // Return the data unchanged for basic test + return data; +} + +static void TestHook_AfterTrack( + LDServerSDKTrackSeriesContext track_context, + void* user_data) { + auto* tracker = static_cast(user_data); + tracker->after_track_count++; + tracker->calls.push_back("after_track"); +} + +// Test basic hook registration and execution +TEST(ServerCBindingsHooksTest, BasicHookExecution) { + HookCallTracker tracker; + + struct LDServerSDKHook hook; + LDServerSDKHook_Init(&hook); + hook.Name = "TestHook"; + hook.BeforeEvaluation = TestHook_BeforeEvaluation; + hook.AfterEvaluation = TestHook_AfterEvaluation; + hook.AfterTrack = TestHook_AfterTrack; + hook.UserData = &tracker; + + auto config_builder = LDServerConfigBuilder_New("sdk-key"); + LDServerConfigBuilder_Hooks(config_builder, hook); + LDServerConfigBuilder_Events_Enabled(config_builder, false); + + LDServerConfig config = nullptr; + LDServerConfigBuilder_Build(config_builder, &config); + auto client = LDServerSDK_New(config); + + // Create a context for evaluation + auto context_builder = LDContextBuilder_New(); + LDContextBuilder_AddKind(context_builder, "user", "user-key"); + auto context = LDContextBuilder_Build(context_builder); + + // Perform an evaluation - should trigger hooks + LDServerSDK_BoolVariation(client, context, "test-flag", false); + + // Verify hooks were called + EXPECT_EQ(tracker.before_eval_count, 1); + EXPECT_EQ(tracker.after_eval_count, 1); + EXPECT_EQ(tracker.calls.size(), 2); + EXPECT_EQ(tracker.calls[0], "before_eval"); + EXPECT_EQ(tracker.calls[1], "after_eval"); + + LDContext_Free(context); + LDServerSDK_Free(client); +} + +// Test track hooks +TEST(ServerCBindingsHooksTest, TrackHookExecution) { + HookCallTracker tracker; + + struct LDServerSDKHook hook; + LDServerSDKHook_Init(&hook); + hook.Name = "TrackTestHook"; + hook.AfterTrack = TestHook_AfterTrack; + hook.UserData = &tracker; + + auto config_builder = LDServerConfigBuilder_New("sdk-key"); + LDServerConfigBuilder_Hooks(config_builder, hook); + LDServerConfigBuilder_Events_Enabled(config_builder, false); + + LDServerConfig config = nullptr; + LDServerConfigBuilder_Build(config_builder, &config); + auto client = LDServerSDK_New(config); + + // Create a context for tracking + auto context_builder = LDContextBuilder_New(); + LDContextBuilder_AddKind(context_builder, "user", "user-key"); + auto context = LDContextBuilder_Build(context_builder); + + // Track an event - should trigger after_track hook + LDServerSDK_TrackEvent(client, context, "test-event"); + + // Verify hook was called + EXPECT_EQ(tracker.after_track_count, 1); + EXPECT_EQ(tracker.calls.size(), 1); + EXPECT_EQ(tracker.calls[0], "after_track"); + + LDContext_Free(context); + LDServerSDK_Free(client); +} + +// Hook that accesses evaluation series context accessors +static LDServerSDKEvaluationSeriesData ContextAccessor_BeforeEvaluation( + LDServerSDKEvaluationSeriesContext series_context, + LDServerSDKEvaluationSeriesData data, + void* user_data) { + auto* tracker = static_cast(user_data); + + // Test all accessor functions + char const* flag_key = LDEvaluationSeriesContext_FlagKey(series_context); + tracker->calls.push_back(std::string("flag:") + flag_key); + + LDContext context = LDEvaluationSeriesContext_Context(series_context); + EXPECT_NE(context, nullptr); + + LDValue default_value = LDEvaluationSeriesContext_DefaultValue(series_context); + EXPECT_NE(default_value, nullptr); + + char const* method = LDEvaluationSeriesContext_Method(series_context); + tracker->calls.push_back(std::string("method:") + method); + + LDHookContext hook_context = LDEvaluationSeriesContext_HookContext(series_context); + EXPECT_NE(hook_context, nullptr); + + char const* env_id = LDEvaluationSeriesContext_EnvironmentId(series_context); + if (env_id) { + tracker->calls.push_back(std::string("env:") + env_id); + } + + return data; +} + +TEST(ServerCBindingsHooksTest, EvaluationSeriesContextAccessors) { + HookCallTracker tracker; + + struct LDServerSDKHook hook; + LDServerSDKHook_Init(&hook); + hook.Name = "ContextAccessorHook"; + hook.BeforeEvaluation = ContextAccessor_BeforeEvaluation; + hook.UserData = &tracker; + + auto config_builder = LDServerConfigBuilder_New("sdk-key"); + LDServerConfigBuilder_Hooks(config_builder, hook); + LDServerConfigBuilder_Events_Enabled(config_builder, false); + + LDServerConfig config = nullptr; + LDServerConfigBuilder_Build(config_builder, &config); + auto client = LDServerSDK_New(config); + + auto context_builder = LDContextBuilder_New(); + LDContextBuilder_AddKind(context_builder, "user", "user-key"); + auto context = LDContextBuilder_Build(context_builder); + + // Perform evaluation + LDServerSDK_BoolVariation(client, context, "my-flag", false); + + // Verify accessor calls recorded expected values + EXPECT_GE(tracker.calls.size(), 2); + EXPECT_EQ(tracker.calls[0], "flag:my-flag"); + EXPECT_EQ(tracker.calls[1], "method:BoolVariation"); + + LDContext_Free(context); + LDServerSDK_Free(client); +} + +// Hook that accesses track series context accessors +static void TrackContextAccessor_AfterTrack( + LDServerSDKTrackSeriesContext track_context, + void* user_data) { + auto* tracker = static_cast(user_data); + + // Test all accessor functions + char const* key = LDTrackSeriesContext_Key(track_context); + tracker->calls.push_back(std::string("key:") + key); + + LDContext context = LDTrackSeriesContext_Context(track_context); + EXPECT_NE(context, nullptr); + + LDValue data_value = nullptr; + bool has_data = LDTrackSeriesContext_Data(track_context, &data_value); + if (has_data) { + tracker->calls.push_back("has_data"); + } + + double metric_value = 0.0; + bool has_metric = LDTrackSeriesContext_MetricValue(track_context, &metric_value); + if (has_metric) { + tracker->calls.push_back("has_metric"); + } + + LDHookContext hook_context = LDTrackSeriesContext_HookContext(track_context); + EXPECT_NE(hook_context, nullptr); + + char const* env_id = LDTrackSeriesContext_EnvironmentId(track_context); + if (env_id) { + tracker->calls.push_back(std::string("env:") + env_id); + } +} + +TEST(ServerCBindingsHooksTest, TrackSeriesContextAccessors) { + HookCallTracker tracker; + + struct LDServerSDKHook hook; + LDServerSDKHook_Init(&hook); + hook.Name = "TrackContextAccessorHook"; + hook.AfterTrack = TrackContextAccessor_AfterTrack; + hook.UserData = &tracker; + + auto config_builder = LDServerConfigBuilder_New("sdk-key"); + LDServerConfigBuilder_Hooks(config_builder, hook); + LDServerConfigBuilder_Events_Enabled(config_builder, false); + + LDServerConfig config = nullptr; + LDServerConfigBuilder_Build(config_builder, &config); + auto client = LDServerSDK_New(config); + + auto context_builder = LDContextBuilder_New(); + LDContextBuilder_AddKind(context_builder, "user", "user-key"); + auto context = LDContextBuilder_Build(context_builder); + + // Track event + LDServerSDK_TrackEvent(client, context, "test-event"); + + // Verify accessor calls + EXPECT_GE(tracker.calls.size(), 1); + EXPECT_EQ(tracker.calls[0], "key:test-event"); + + LDContext_Free(context); + LDServerSDK_Free(client); +} + +// Hook that passes data between stages using EvaluationSeriesData +static LDServerSDKEvaluationSeriesData DataPassing_BeforeEvaluation( + LDServerSDKEvaluationSeriesContext series_context, + LDServerSDKEvaluationSeriesData data, + void* user_data) { + // Create a builder from the incoming data + auto builder = LDEvaluationSeriesData_NewBuilder(data); + + // Add a string value + auto string_value = LDValue_NewString("test-string"); + LDEvaluationSeriesDataBuilder_SetValue(builder, "my-key", string_value); + + // Add a pointer value + int* my_int = new int(42); + LDEvaluationSeriesDataBuilder_SetPointer(builder, "my-pointer", my_int); + + // Store pointer in user_data so we can clean it up later + auto* tracker = static_cast(user_data); + tracker->calls.push_back("before:set_data"); + + LDValue_Free(string_value); + + return LDEvaluationSeriesDataBuilder_Build(builder); +} + +static LDServerSDKEvaluationSeriesData DataPassing_AfterEvaluation( + LDServerSDKEvaluationSeriesContext series_context, + LDServerSDKEvaluationSeriesData data, + LDEvalDetail detail, + void* user_data) { + auto* tracker = static_cast(user_data); + + // Retrieve the string value + LDValue retrieved_value = nullptr; + bool found = LDEvaluationSeriesData_GetValue(data, "my-key", &retrieved_value); + EXPECT_TRUE(found); + if (found) { + char const* str = LDValue_GetString(retrieved_value); + EXPECT_STREQ(str, "test-string"); + tracker->calls.push_back("after:got_value"); + } + + // Retrieve the pointer + void* retrieved_pointer = nullptr; + found = LDEvaluationSeriesData_GetPointer(data, "my-pointer", &retrieved_pointer); + EXPECT_TRUE(found); + if (found) { + int* my_int = static_cast(retrieved_pointer); + EXPECT_EQ(*my_int, 42); + tracker->calls.push_back("after:got_pointer"); + delete my_int; // Clean up + } + + return data; +} + +TEST(ServerCBindingsHooksTest, DataPassingBetweenStages) { + HookCallTracker tracker; + + struct LDServerSDKHook hook; + LDServerSDKHook_Init(&hook); + hook.Name = "DataPassingHook"; + hook.BeforeEvaluation = DataPassing_BeforeEvaluation; + hook.AfterEvaluation = DataPassing_AfterEvaluation; + hook.UserData = &tracker; + + auto config_builder = LDServerConfigBuilder_New("sdk-key"); + LDServerConfigBuilder_Hooks(config_builder, hook); + LDServerConfigBuilder_Events_Enabled(config_builder, false); + + LDServerConfig config = nullptr; + LDServerConfigBuilder_Build(config_builder, &config); + auto client = LDServerSDK_New(config); + + auto context_builder = LDContextBuilder_New(); + LDContextBuilder_AddKind(context_builder, "user", "user-key"); + auto context = LDContextBuilder_Build(context_builder); + + // Perform evaluation + LDServerSDK_BoolVariation(client, context, "test-flag", false); + + // Verify data was passed correctly + EXPECT_EQ(tracker.calls.size(), 3); + EXPECT_EQ(tracker.calls[0], "before:set_data"); + EXPECT_EQ(tracker.calls[1], "after:got_value"); + EXPECT_EQ(tracker.calls[2], "after:got_pointer"); + + LDContext_Free(context); + LDServerSDK_Free(client); +} + +// Hook that uses HookContext to pass caller data +static LDServerSDKEvaluationSeriesData HookContextTest_BeforeEvaluation( + LDServerSDKEvaluationSeriesContext series_context, + LDServerSDKEvaluationSeriesData data, + void* user_data) { + auto* tracker = static_cast(user_data); + + LDHookContext hook_context = LDEvaluationSeriesContext_HookContext(series_context); + + // Try to get a value that should be set by the caller + void const* retrieved_value = nullptr; + bool found = LDHookContext_Get(hook_context, "span", &retrieved_value); + if (found) { + // In a real scenario, this would be an OpenTelemetry span + int const* span_id = static_cast(retrieved_value); + tracker->calls.push_back(std::string("span:") + std::to_string(*span_id)); + } + + return data; +} + +TEST(ServerCBindingsHooksTest, HookContextOperations) { + HookCallTracker tracker; + + struct LDServerSDKHook hook; + LDServerSDKHook_Init(&hook); + hook.Name = "HookContextTestHook"; + hook.BeforeEvaluation = HookContextTest_BeforeEvaluation; + hook.UserData = &tracker; + + auto config_builder = LDServerConfigBuilder_New("sdk-key"); + LDServerConfigBuilder_Hooks(config_builder, hook); + LDServerConfigBuilder_Events_Enabled(config_builder, false); + + LDServerConfig config = nullptr; + LDServerConfigBuilder_Build(config_builder, &config); + auto client = LDServerSDK_New(config); + + auto context_builder = LDContextBuilder_New(); + LDContextBuilder_AddKind(context_builder, "user", "user-key"); + auto context = LDContextBuilder_Build(context_builder); + + // Create a HookContext and set a value (simulating caller setting context) + auto hook_context = LDHookContext_New(); + int span_id = 12345; + LDHookContext_Set(hook_context, "span", &span_id); + + // Perform evaluation with hook context + // Note: The C API doesn't currently expose a way to pass HookContext to evaluations + // In the real SDK, this would be passed through the evaluation call + // For now, we'll test the HookContext operations independently + + // Test Get operation + void const* retrieved_value = nullptr; + bool found = LDHookContext_Get(hook_context, "span", &retrieved_value); + EXPECT_TRUE(found); + if (found) { + int const* retrieved_span = static_cast(retrieved_value); + EXPECT_EQ(*retrieved_span, 12345); + } + + // Test missing key + found = LDHookContext_Get(hook_context, "missing-key", &retrieved_value); + EXPECT_FALSE(found); + + LDHookContext_Free(hook_context); + LDContext_Free(context); + LDServerSDK_Free(client); +} + +// Test multiple hooks executing in order +static LDServerSDKEvaluationSeriesData FirstHook_BeforeEvaluation( + LDServerSDKEvaluationSeriesContext series_context, + LDServerSDKEvaluationSeriesData data, + void* user_data) { + auto* tracker = static_cast(user_data); + tracker->calls.push_back("first_hook"); + return data; +} + +static LDServerSDKEvaluationSeriesData SecondHook_BeforeEvaluation( + LDServerSDKEvaluationSeriesContext series_context, + LDServerSDKEvaluationSeriesData data, + void* user_data) { + auto* tracker = static_cast(user_data); + tracker->calls.push_back("second_hook"); + return data; +} + +TEST(ServerCBindingsHooksTest, MultipleHooksInOrder) { + HookCallTracker tracker; + + struct LDServerSDKHook hook1; + LDServerSDKHook_Init(&hook1); + hook1.Name = "FirstHook"; + hook1.BeforeEvaluation = FirstHook_BeforeEvaluation; + hook1.UserData = &tracker; + + struct LDServerSDKHook hook2; + LDServerSDKHook_Init(&hook2); + hook2.Name = "SecondHook"; + hook2.BeforeEvaluation = SecondHook_BeforeEvaluation; + hook2.UserData = &tracker; + + auto config_builder = LDServerConfigBuilder_New("sdk-key"); + LDServerConfigBuilder_Hooks(config_builder, hook1); + LDServerConfigBuilder_Hooks(config_builder, hook2); + LDServerConfigBuilder_Events_Enabled(config_builder, false); + + LDServerConfig config = nullptr; + LDServerConfigBuilder_Build(config_builder, &config); + auto client = LDServerSDK_New(config); + + auto context_builder = LDContextBuilder_New(); + LDContextBuilder_AddKind(context_builder, "user", "user-key"); + auto context = LDContextBuilder_Build(context_builder); + + // Perform evaluation + LDServerSDK_BoolVariation(client, context, "test-flag", false); + + // Verify hooks executed in order + EXPECT_GE(tracker.calls.size(), 2); + EXPECT_EQ(tracker.calls[0], "first_hook"); + EXPECT_EQ(tracker.calls[1], "second_hook"); + + LDContext_Free(context); + LDServerSDK_Free(client); +} + +// Test that NULL data is handled correctly +static LDServerSDKEvaluationSeriesData NullDataTest_BeforeEvaluation( + LDServerSDKEvaluationSeriesContext series_context, + LDServerSDKEvaluationSeriesData data, + void* user_data) { + auto* tracker = static_cast(user_data); + + // Return NULL to indicate no data + tracker->calls.push_back("returned_null"); + return nullptr; +} + +static LDServerSDKEvaluationSeriesData NullDataTest_AfterEvaluation( + LDServerSDKEvaluationSeriesContext series_context, + LDServerSDKEvaluationSeriesData data, + LDEvalDetail detail, + void* user_data) { + auto* tracker = static_cast(user_data); + + // Should receive empty data from previous stage + LDValue test_value = nullptr; + bool found = LDEvaluationSeriesData_GetValue(data, "any-key", &test_value); + EXPECT_FALSE(found); + + tracker->calls.push_back("received_empty"); + return data; +} + +TEST(ServerCBindingsHooksTest, NullDataHandling) { + HookCallTracker tracker; + + struct LDServerSDKHook hook; + LDServerSDKHook_Init(&hook); + hook.Name = "NullDataHook"; + hook.BeforeEvaluation = NullDataTest_BeforeEvaluation; + hook.AfterEvaluation = NullDataTest_AfterEvaluation; + hook.UserData = &tracker; + + auto config_builder = LDServerConfigBuilder_New("sdk-key"); + LDServerConfigBuilder_Hooks(config_builder, hook); + LDServerConfigBuilder_Events_Enabled(config_builder, false); + + LDServerConfig config = nullptr; + LDServerConfigBuilder_Build(config_builder, &config); + auto client = LDServerSDK_New(config); + + auto context_builder = LDContextBuilder_New(); + LDContextBuilder_AddKind(context_builder, "user", "user-key"); + auto context = LDContextBuilder_Build(context_builder); + + // Perform evaluation + LDServerSDK_BoolVariation(client, context, "test-flag", false); + + // Verify NULL was handled correctly + EXPECT_EQ(tracker.calls.size(), 2); + EXPECT_EQ(tracker.calls[0], "returned_null"); + EXPECT_EQ(tracker.calls[1], "received_empty"); + + LDContext_Free(context); + LDServerSDK_Free(client); +} + +// Test EvaluationSeriesDataBuilder operations +TEST(ServerCBindingsHooksTest, EvaluationSeriesDataBuilder) { + // Create new empty data + auto data = LDEvaluationSeriesData_New(); + EXPECT_NE(data, nullptr); + + // Create builder from empty data + auto builder = LDEvaluationSeriesData_NewBuilder(data); + EXPECT_NE(builder, nullptr); + + // Add various values + auto string_value = LDValue_NewString("test"); + LDEvaluationSeriesDataBuilder_SetValue(builder, "key1", string_value); + + auto number_value = LDValue_NewNumber(42.5); + LDEvaluationSeriesDataBuilder_SetValue(builder, "key2", number_value); + + int my_int = 100; + LDEvaluationSeriesDataBuilder_SetPointer(builder, "ptr1", &my_int); + + // Build new data + auto new_data = LDEvaluationSeriesDataBuilder_Build(builder); + EXPECT_NE(new_data, nullptr); + + // Verify values + LDValue retrieved = nullptr; + bool found = LDEvaluationSeriesData_GetValue(new_data, "key1", &retrieved); + EXPECT_TRUE(found); + if (found) { + EXPECT_STREQ(LDValue_GetString(retrieved), "test"); + } + + found = LDEvaluationSeriesData_GetValue(new_data, "key2", &retrieved); + EXPECT_TRUE(found); + if (found) { + EXPECT_DOUBLE_EQ(LDValue_GetNumber(retrieved), 42.5); + } + + void* retrieved_ptr = nullptr; + found = LDEvaluationSeriesData_GetPointer(new_data, "ptr1", &retrieved_ptr); + EXPECT_TRUE(found); + if (found) { + int* ptr = static_cast(retrieved_ptr); + EXPECT_EQ(*ptr, 100); + } + + // Test missing key + found = LDEvaluationSeriesData_GetValue(new_data, "missing", &retrieved); + EXPECT_FALSE(found); + + LDValue_Free(string_value); + LDValue_Free(number_value); + LDEvaluationSeriesData_Free(new_data); + LDEvaluationSeriesData_Free(data); +} + +// Test that builder can be freed without building (error case) +TEST(ServerCBindingsHooksTest, EvaluationSeriesDataBuilderFreeWithoutBuild) { + // Create a builder + LDServerSDKEvaluationSeriesDataBuilder builder = + LDEvaluationSeriesData_NewBuilder(nullptr); + + // Add some data to it + LDValue test_value = LDValue_NewNumber(42.0); + LDEvaluationSeriesDataBuilder_SetValue(builder, "test", test_value); + + int test_int = 100; + LDEvaluationSeriesDataBuilder_SetPointer(builder, "ptr", &test_int); + + // Free the builder without building + // This tests the error case where user decides not to proceed + LDEvaluationSeriesDataBuilder_Free(builder); + + // No assertions needed - this test passes if no memory leaks occur + // ASAN will detect any issues + + LDValue_Free(test_value); +}