diff --git a/packages/shared/src/telemetry/attributes.ts b/packages/shared/src/telemetry/attributes.ts index c61808456..4ec6b214b 100644 --- a/packages/shared/src/telemetry/attributes.ts +++ b/packages/shared/src/telemetry/attributes.ts @@ -42,7 +42,6 @@ export const TelemetryAttribute = { * * - type: `undefined` * - requirement level: `conditionally required` - * - condition: variant is not defined on the evaluation details * - example: `#ff0000`; `1`; `true` */ VALUE: 'feature_flag.result.value', diff --git a/packages/shared/src/telemetry/evaluation-event.ts b/packages/shared/src/telemetry/evaluation-event.ts index 81a85aad2..d23e024fc 100644 --- a/packages/shared/src/telemetry/evaluation-event.ts +++ b/packages/shared/src/telemetry/evaluation-event.ts @@ -1,8 +1,20 @@ -import { ErrorCode, StandardResolutionReasons, type EvaluationDetails, type FlagValue } from '../evaluation/evaluation'; -import type { HookContext } from '../hooks/hooks'; +import { ErrorCode, StandardResolutionReasons, type EvaluationDetails, type FlagValue } from '../evaluation'; +import type { HookContext } from '../hooks'; import { TelemetryAttribute } from './attributes'; import { TelemetryFlagMetadata } from './flag-metadata'; +/** + * Attribute types for OpenTelemetry. + * @see https://github.com/open-telemetry/opentelemetry-js/blob/fbbce6e1c0de86e4c504b5788d876fae4d3bc254/api/src/common/Attributes.ts#L35 + */ +export declare type AttributeValue = + | string + | number + | boolean + | string[] + | number[] + | boolean[]; + type EvaluationEvent = { /** * The name of the feature flag evaluation event. @@ -13,7 +25,7 @@ type EvaluationEvent = { * @experimental The attributes are subject to change. * @see https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/ */ - attributes: Record; + attributes: Record; }; const FLAG_EVALUATION_EVENT_NAME = 'feature_flag.evaluation'; @@ -36,12 +48,23 @@ export function createEvaluationEvent( if (evaluationDetails.variant) { attributes[TelemetryAttribute.VARIANT] = evaluationDetails.variant; - } else { - attributes[TelemetryAttribute.VALUE] = evaluationDetails.value; + } + + if (evaluationDetails.value !== null) { + if (typeof evaluationDetails.value !== 'object') { + attributes[TelemetryAttribute.VALUE] = evaluationDetails.value; + } else { + try { + // Objects are not valid attribute values, so we convert them to a JSON string + attributes[TelemetryAttribute.VALUE] = JSON.stringify(evaluationDetails.value); + } catch { + // We ignore non serializable values + } + } } const contextId = - evaluationDetails.flagMetadata[TelemetryFlagMetadata.CONTEXT_ID] || hookContext.context.targetingKey; + evaluationDetails.flagMetadata[TelemetryFlagMetadata.CONTEXT_ID] ?? hookContext.context.targetingKey; if (contextId) { attributes[TelemetryAttribute.CONTEXT_ID] = contextId; } diff --git a/packages/shared/test/telemetry.spec.ts b/packages/shared/test/telemetry.spec.ts index c8ca5f48d..5d15e0a4f 100644 --- a/packages/shared/test/telemetry.spec.ts +++ b/packages/shared/test/telemetry.spec.ts @@ -49,7 +49,7 @@ describe('evaluationEvent', () => { }); }); - it('should include variant when provided', () => { + it('should include variant and value when provided', () => { const details: EvaluationDetails = { flagKey, value: true, @@ -61,7 +61,24 @@ describe('evaluationEvent', () => { const result = createEvaluationEvent(mockHookContext, details); expect(result.attributes[TelemetryAttribute.VARIANT]).toBe('test-variant'); - expect(result.attributes[TelemetryAttribute.VALUE]).toBeUndefined(); + expect(result.attributes[TelemetryAttribute.VALUE]).toEqual(true); + }); + + it('should include object values as strings', () => { + const flagValue = { key: 'value' }; + + const details: EvaluationDetails = { + flagKey, + value: flagValue, + variant: 'test-variant', + reason: StandardResolutionReasons.STATIC, + flagMetadata: {}, + }; + + const result = createEvaluationEvent(mockHookContext, details); + + expect(result.attributes[TelemetryAttribute.VARIANT]).toBe('test-variant'); + expect(result.attributes[TelemetryAttribute.VALUE]).toEqual(JSON.stringify(flagValue)); }); it('should include flag metadata when provided', () => {