From 3a547a33ca6763dd1dc3a4b4aed69f3f41963f22 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 3 Jun 2025 11:33:36 -0400 Subject: [PATCH 1/3] Span class updates --- .../core/src/tracing/sentryNonRecordingSpan.ts | 5 +++++ packages/core/src/tracing/sentrySpan.ts | 15 +++++++++++++++ packages/core/src/types-hoist/span.ts | 7 +++++++ 3 files changed, 27 insertions(+) diff --git a/packages/core/src/tracing/sentryNonRecordingSpan.ts b/packages/core/src/tracing/sentryNonRecordingSpan.ts index 9ace30c4e70a..10951503a733 100644 --- a/packages/core/src/tracing/sentryNonRecordingSpan.ts +++ b/packages/core/src/tracing/sentryNonRecordingSpan.ts @@ -79,6 +79,11 @@ export class SentryNonRecordingSpan implements Span { return this; } + /** @inheritDoc */ + public addFeatureFlag(_name: string, _value: boolean): this { + return this; + } + /** * This should generally not be used, * but we need it for being compliant with the OTEL Span interface. diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index f0e2a6bb374a..45b9d265d529 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -45,6 +45,8 @@ import { timedEventsToMeasurements } from './measurement'; import { getCapturedScopesOnSpan } from './utils'; const MAX_SPAN_COUNT = 1000; +const MAX_FEATURE_FLAGS_PER_SPAN = 10; // The maximum number of feature flag evaluations that can be recorded in span attributes. +const FEATURE_FLAG_ATTRIBUTE_PREFIX = 'sentry.flag.evaluation.'; /** * Span contains all data about a span @@ -57,6 +59,7 @@ export class SentrySpan implements Span { protected _name?: string | undefined; protected _attributes: SpanAttributes; protected _links?: SpanLink[]; + protected _numFeatureFlags: number; /** Epoch timestamp in seconds when the span started. */ protected _startTime: number; /** Epoch timestamp in seconds when the span ended. */ @@ -81,6 +84,7 @@ export class SentrySpan implements Span { this._spanId = spanContext.spanId || generateSpanId(); this._startTime = spanContext.startTimestamp || timestampInSeconds(); this._links = spanContext.links; + this._numFeatureFlags = 0; this._attributes = {}; this.setAttributes({ @@ -132,6 +136,17 @@ export class SentrySpan implements Span { return this; } + /** @inheritDoc */ + public addFeatureFlag(name: string, value: boolean): this { + if (!(name in this._attributes)) { + if (this._numFeatureFlags >= MAX_FEATURE_FLAGS_PER_SPAN) { + return this; + } + this._numFeatureFlags++; + } + return this.setAttribute(`${FEATURE_FLAG_ATTRIBUTE_PREFIX}${name}`, value); + } + /** * This should generally not be used, * but it is needed for being compliant with the OTEL Span interface. diff --git a/packages/core/src/types-hoist/span.ts b/packages/core/src/types-hoist/span.ts index d82463768b7f..0a7cb7c3836f 100644 --- a/packages/core/src/types-hoist/span.ts +++ b/packages/core/src/types-hoist/span.ts @@ -273,6 +273,13 @@ export interface Span { */ addLinks(links: SpanLink[]): this; + /** + * Adds a feature flag evaluation to the span. + * @param name - The name of the feature flag. + * @param value - The value of the feature flag. + */ + addFeatureFlag(name: string, value: boolean): this; + /** * NOT USED IN SENTRY, only added for compliance with OTEL Span interface */ From dff89ff7e26bd743179a8b85c645f21e1d9e19a3 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 3 Jun 2025 11:43:56 -0400 Subject: [PATCH 2/3] Use in browser util and integrations --- .../featureFlags/featureFlagsIntegration.ts | 3 ++- .../featureFlags/launchdarkly/integration.ts | 3 ++- .../featureFlags/openfeature/integration.ts | 4 +++- .../featureFlags/statsig/integration.ts | 3 ++- .../featureFlags/unleash/integration.ts | 3 ++- packages/browser/src/utils/featureFlags.ts | 14 +++++++++++++- packages/core/src/tracing/sentrySpan.ts | 2 +- 7 files changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts b/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts index 54b5680cccd1..a0b91e2aafe6 100644 --- a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts +++ b/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts @@ -1,6 +1,6 @@ import type { Client, Event, EventHint, Integration, IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../utils/featureFlags'; +import { addFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../utils/featureFlags'; export interface FeatureFlagsIntegration extends Integration { addFeatureFlag: (name: string, value: unknown) => void; @@ -41,6 +41,7 @@ export const featureFlagsIntegration = defineIntegration(() => { addFeatureFlag(name: string, value: unknown): void { insertFlagToScope(name, value); + addFlagToActiveSpan(name, value); }, }; }) as IntegrationFn; diff --git a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts index f96b8deb8fa0..2cca41afa1d8 100644 --- a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts +++ b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts @@ -1,6 +1,6 @@ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { addFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; import type { LDContext, LDEvaluationDetail, LDInspectionFlagUsedHandler } from './types'; /** @@ -46,6 +46,7 @@ export function buildLaunchDarklyFlagUsedHandler(): LDInspectionFlagUsedHandler */ method: (flagKey: string, flagDetail: LDEvaluationDetail, _context: LDContext) => { insertFlagToScope(flagKey, flagDetail.value); + addFlagToActiveSpan(flagKey, flagDetail.value); }, }; } diff --git a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts index b1963e9964e6..b29941282056 100644 --- a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts +++ b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts @@ -7,7 +7,7 @@ */ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { addFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; import type { EvaluationDetails, HookContext, HookHints, JsonValue, OpenFeatureHook } from './types'; export const openFeatureIntegration = defineIntegration(() => { @@ -29,6 +29,7 @@ export class OpenFeatureIntegrationHook implements OpenFeatureHook { */ public after(_hookContext: Readonly>, evaluationDetails: EvaluationDetails): void { insertFlagToScope(evaluationDetails.flagKey, evaluationDetails.value); + addFlagToActiveSpan(evaluationDetails.flagKey, evaluationDetails.value); } /** @@ -36,5 +37,6 @@ export class OpenFeatureIntegrationHook implements OpenFeatureHook { */ public error(hookContext: Readonly>, _error: unknown, _hookHints?: HookHints): void { insertFlagToScope(hookContext.flagKey, hookContext.defaultValue); + addFlagToActiveSpan(hookContext.flagKey, hookContext.defaultValue); } } diff --git a/packages/browser/src/integrations/featureFlags/statsig/integration.ts b/packages/browser/src/integrations/featureFlags/statsig/integration.ts index 54600458cfb9..6048e147db61 100644 --- a/packages/browser/src/integrations/featureFlags/statsig/integration.ts +++ b/packages/browser/src/integrations/featureFlags/statsig/integration.ts @@ -1,6 +1,6 @@ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { addFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; import type { FeatureGate, StatsigClient } from './types'; /** @@ -38,6 +38,7 @@ export const statsigIntegration = defineIntegration( setup() { statsigClient.on('gate_evaluation', (event: { gate: FeatureGate }) => { insertFlagToScope(event.gate.name, event.gate.value); + addFlagToActiveSpan(event.gate.name, event.gate.value); }); }, }; diff --git a/packages/browser/src/integrations/featureFlags/unleash/integration.ts b/packages/browser/src/integrations/featureFlags/unleash/integration.ts index 21d945dfcaae..6595dd982600 100644 --- a/packages/browser/src/integrations/featureFlags/unleash/integration.ts +++ b/packages/browser/src/integrations/featureFlags/unleash/integration.ts @@ -1,7 +1,7 @@ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { defineIntegration, fill, logger } from '@sentry/core'; import { DEBUG_BUILD } from '../../../debug-build'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { addFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; import type { UnleashClient, UnleashClientClass } from './types'; type UnleashIntegrationOptions = { @@ -65,6 +65,7 @@ function _wrappedIsEnabled( if (typeof toggleName === 'string' && typeof result === 'boolean') { insertFlagToScope(toggleName, result); + addFlagToActiveSpan(toggleName, result); } else if (DEBUG_BUILD) { logger.error( `[Feature Flags] UnleashClient.isEnabled does not match expected signature. arg0: ${toggleName} (${typeof toggleName}), result: ${result} (${typeof result})`, diff --git a/packages/browser/src/utils/featureFlags.ts b/packages/browser/src/utils/featureFlags.ts index a71e7233fe75..586df5034518 100644 --- a/packages/browser/src/utils/featureFlags.ts +++ b/packages/browser/src/utils/featureFlags.ts @@ -1,5 +1,5 @@ import type { Event, FeatureFlag } from '@sentry/core'; -import { getCurrentScope, logger } from '@sentry/core'; +import { getActiveSpan, getCurrentScope, logger } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; /** @@ -87,3 +87,15 @@ export function insertToFlagBuffer(flags: FeatureFlag[], name: string, value: un result: value, }); } + +/** + * Add a feature flag evaluation to the active span. Currently a no-op for non-boolean values. + * @param name + * @param value + */ +export function addFlagToActiveSpan(name: string, value: unknown): void { + if (typeof value !== 'boolean') { + return; + } + getActiveSpan()?.addFeatureFlag(name, value); +} diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 45b9d265d529..856b457835ee 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -45,7 +45,7 @@ import { timedEventsToMeasurements } from './measurement'; import { getCapturedScopesOnSpan } from './utils'; const MAX_SPAN_COUNT = 1000; -const MAX_FEATURE_FLAGS_PER_SPAN = 10; // The maximum number of feature flag evaluations that can be recorded in span attributes. +const MAX_FEATURE_FLAGS_PER_SPAN = 10; // The maximum number of feature flag evaluations that can be recorded in span attributes. const FEATURE_FLAG_ATTRIBUTE_PREFIX = 'sentry.flag.evaluation.'; /** From 710945cc0cf9f89ede3f62bf912b730e8a5a1e75 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 3 Jun 2025 11:50:19 -0400 Subject: [PATCH 3/3] Disable max lines in sentrySpan --- packages/core/src/tracing/sentrySpan.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 856b457835ee..31134f65e8b6 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -1,3 +1,5 @@ +/* eslint-disable max-lines */ + import { getClient, getCurrentScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; import { createSpanEnvelope } from '../envelope';