From 3f840b4ab2f8527701f730874456907a27123cba Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:36:03 -0400 Subject: [PATCH 01/15] Buffer with global weakmap and save on spanEnd --- .../test/integration/transactions.test.ts | 76 +++++++++---------- .../featureFlags/featureFlagsIntegration.ts | 16 +++- .../featureFlags/launchdarkly/integration.ts | 16 +++- .../featureFlags/openfeature/integration.ts | 17 ++++- .../featureFlags/statsig/integration.ts | 22 ++++-- .../featureFlags/unleash/integration.ts | 20 ++++- packages/browser/src/utils/featureFlags.ts | 51 ++++++++++++- packages/core/src/utils-hoist/worldwide.ts | 6 ++ 8 files changed, 168 insertions(+), 56 deletions(-) diff --git a/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts b/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts index 3bdf6c113555..0bbb77296a58 100644 --- a/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts +++ b/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts @@ -548,57 +548,57 @@ describe('Integration | Transactions', () => { expect(finishedSpans.length).toBe(0); }); -it('collects child spans that are finished within 5 minutes their parent span has been sent', async () => { - const timeout = 5 * 60 * 1000; - const now = Date.now(); - vi.useFakeTimers(); - vi.setSystemTime(now); + it('collects child spans that are finished within 5 minutes their parent span has been sent', async () => { + const timeout = 5 * 60 * 1000; + const now = Date.now(); + vi.useFakeTimers(); + vi.setSystemTime(now); - const logs: unknown[] = []; - vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + const logs: unknown[] = []; + vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); - const transactions: Event[] = []; + const transactions: Event[] = []; - mockSdkInit({ - tracesSampleRate: 1, - beforeSendTransaction: event => { - transactions.push(event); - return null; - }, - }); + mockSdkInit({ + tracesSampleRate: 1, + beforeSendTransaction: event => { + transactions.push(event); + return null; + }, + }); - const provider = getProvider(); - const spanProcessor = getSpanProcessor(); + const provider = getProvider(); + const spanProcessor = getSpanProcessor(); - const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined; + const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined; - if (!exporter) { - throw new Error('No exporter found, aborting test...'); - } + if (!exporter) { + throw new Error('No exporter found, aborting test...'); + } - startSpanManual({ name: 'test name' }, async span => { - const subSpan = startInactiveSpan({ name: 'inner span 1' }); - subSpan.end(); + startSpanManual({ name: 'test name' }, async span => { + const subSpan = startInactiveSpan({ name: 'inner span 1' }); + subSpan.end(); - const subSpan2 = startInactiveSpan({ name: 'inner span 2' }); + const subSpan2 = startInactiveSpan({ name: 'inner span 2' }); - span.end(); + span.end(); - setTimeout(() => { - subSpan2.end(); - }, timeout - 2); - }); + setTimeout(() => { + subSpan2.end(); + }, timeout - 2); + }); - vi.advanceTimersByTime(timeout - 1); + vi.advanceTimersByTime(timeout - 1); - expect(transactions).toHaveLength(2); - expect(transactions[0]?.spans).toHaveLength(1); + expect(transactions).toHaveLength(2); + expect(transactions[0]?.spans).toHaveLength(1); - const finishedSpans: any = exporter['_finishedSpanBuckets'].flatMap(bucket => - bucket ? Array.from(bucket.spans) : [], - ); - expect(finishedSpans.length).toBe(0); -}); + const finishedSpans: any = exporter['_finishedSpanBuckets'].flatMap(bucket => + bucket ? Array.from(bucket.spans) : [], + ); + expect(finishedSpans.length).toBe(0); + }); it('discards child spans that are finished after 5 minutes their parent span has been sent', async () => { const timeout = 5 * 60 * 1000; diff --git a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts b/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts index 54b5680cccd1..aa569380d79f 100644 --- a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts +++ b/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts @@ -1,6 +1,11 @@ -import type { Client, Event, EventHint, Integration, IntegrationFn } from '@sentry/core'; +import type { Client, Event, EventHint, Integration, IntegrationFn, Span } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../utils/featureFlags'; +import { + bufferSpanFeatureFlag, + copyFlagsFromScopeToEvent, + freezeSpanFeatureFlags, + insertFlagToScope, +} from '../../utils/featureFlags'; export interface FeatureFlagsIntegration extends Integration { addFeatureFlag: (name: string, value: unknown) => void; @@ -35,12 +40,19 @@ export const featureFlagsIntegration = defineIntegration(() => { return { name: 'FeatureFlags', + setup(client: Client) { + client.on('spanEnd', (span: Span) => { + freezeSpanFeatureFlags(span); + }); + }, + processEvent(event: Event, _hint: EventHint, _client: Client): Event { return copyFlagsFromScopeToEvent(event); }, addFeatureFlag(name: string, value: unknown): void { insertFlagToScope(name, value); + bufferSpanFeatureFlag(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..eef12f76d634 100644 --- a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts +++ b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts @@ -1,6 +1,11 @@ -import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; +import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { + bufferSpanFeatureFlag, + copyFlagsFromScopeToEvent, + freezeSpanFeatureFlags, + insertFlagToScope, +} from '../../../utils/featureFlags'; import type { LDContext, LDEvaluationDetail, LDInspectionFlagUsedHandler } from './types'; /** @@ -22,6 +27,12 @@ export const launchDarklyIntegration = defineIntegration(() => { return { name: 'LaunchDarkly', + setup(client: Client) { + client.on('spanEnd', (span: Span) => { + freezeSpanFeatureFlags(span); + }); + }, + processEvent(event: Event, _hint: EventHint, _client: Client): Event { return copyFlagsFromScopeToEvent(event); }, @@ -46,6 +57,7 @@ export function buildLaunchDarklyFlagUsedHandler(): LDInspectionFlagUsedHandler */ method: (flagKey: string, flagDetail: LDEvaluationDetail, _context: LDContext) => { insertFlagToScope(flagKey, flagDetail.value); + bufferSpanFeatureFlag(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..3c5264dcc019 100644 --- a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts +++ b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts @@ -5,15 +5,26 @@ * Add the integration hook to your OpenFeature object. * - OpenFeature.getClient().addHooks(new OpenFeatureIntegrationHook()); */ -import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; +import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { + bufferSpanFeatureFlag, + copyFlagsFromScopeToEvent, + freezeSpanFeatureFlags, + insertFlagToScope, +} from '../../../utils/featureFlags'; import type { EvaluationDetails, HookContext, HookHints, JsonValue, OpenFeatureHook } from './types'; export const openFeatureIntegration = defineIntegration(() => { return { name: 'OpenFeature', + setup(client: Client) { + client.on('spanEnd', (span: Span) => { + freezeSpanFeatureFlags(span); + }); + }, + processEvent(event: Event, _hint: EventHint, _client: Client): Event { return copyFlagsFromScopeToEvent(event); }, @@ -29,6 +40,7 @@ export class OpenFeatureIntegrationHook implements OpenFeatureHook { */ public after(_hookContext: Readonly>, evaluationDetails: EvaluationDetails): void { insertFlagToScope(evaluationDetails.flagKey, evaluationDetails.value); + bufferSpanFeatureFlag(evaluationDetails.flagKey, evaluationDetails.value); } /** @@ -36,5 +48,6 @@ export class OpenFeatureIntegrationHook implements OpenFeatureHook { */ public error(hookContext: Readonly>, _error: unknown, _hookHints?: HookHints): void { insertFlagToScope(hookContext.flagKey, hookContext.defaultValue); + bufferSpanFeatureFlag(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..641e7412e6a8 100644 --- a/packages/browser/src/integrations/featureFlags/statsig/integration.ts +++ b/packages/browser/src/integrations/featureFlags/statsig/integration.ts @@ -1,6 +1,11 @@ -import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; +import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { + bufferSpanFeatureFlag, + copyFlagsFromScopeToEvent, + freezeSpanFeatureFlags, + insertFlagToScope, +} from '../../../utils/featureFlags'; import type { FeatureGate, StatsigClient } from './types'; /** @@ -31,15 +36,20 @@ export const statsigIntegration = defineIntegration( return { name: 'Statsig', - processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); - }, + setup(client: Client) { + client.on('spanEnd', (span: Span) => { + freezeSpanFeatureFlags(span); + }); - setup() { statsigClient.on('gate_evaluation', (event: { gate: FeatureGate }) => { insertFlagToScope(event.gate.name, event.gate.value); + bufferSpanFeatureFlag(event.gate.name, event.gate.value); }); }, + + processEvent(event: Event, _hint: EventHint, _client: Client): Event { + return copyFlagsFromScopeToEvent(event); + }, }; }, ) satisfies IntegrationFn; diff --git a/packages/browser/src/integrations/featureFlags/unleash/integration.ts b/packages/browser/src/integrations/featureFlags/unleash/integration.ts index 21d945dfcaae..75559dd3841b 100644 --- a/packages/browser/src/integrations/featureFlags/unleash/integration.ts +++ b/packages/browser/src/integrations/featureFlags/unleash/integration.ts @@ -1,7 +1,12 @@ -import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; +import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; import { defineIntegration, fill, logger } from '@sentry/core'; import { DEBUG_BUILD } from '../../../debug-build'; -import { copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; +import { + bufferSpanFeatureFlag, + copyFlagsFromScopeToEvent, + freezeSpanFeatureFlags, + insertFlagToScope, +} from '../../../utils/featureFlags'; import type { UnleashClient, UnleashClientClass } from './types'; type UnleashIntegrationOptions = { @@ -35,14 +40,20 @@ export const unleashIntegration = defineIntegration( return { name: 'Unleash', - processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); + setup(client: Client) { + client.on('spanEnd', (span: Span) => { + freezeSpanFeatureFlags(span); + }); }, setupOnce() { const unleashClientPrototype = unleashClientClass.prototype as UnleashClient; fill(unleashClientPrototype, 'isEnabled', _wrappedIsEnabled); }, + + processEvent(event: Event, _hint: EventHint, _client: Client): Event { + return copyFlagsFromScopeToEvent(event); + }, }; }, ) satisfies IntegrationFn; @@ -65,6 +76,7 @@ function _wrappedIsEnabled( if (typeof toggleName === 'string' && typeof result === 'boolean') { insertFlagToScope(toggleName, result); + bufferSpanFeatureFlag(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..ff85e160c8b0 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 type { Event, FeatureFlag, Span } from '@sentry/core'; +import { getActiveSpan, getCurrentScope, GLOBAL_OBJ, logger } from '@sentry/core'; import { DEBUG_BUILD } from '../debug-build'; /** @@ -13,6 +13,13 @@ import { DEBUG_BUILD } from '../debug-build'; */ export const FLAG_BUFFER_SIZE = 100; +/** + * Max number of flag evaluations to record per span. + */ +export const MAX_FLAGS_PER_SPAN = 10; + +GLOBAL_OBJ._spanToFlagBufferMap = new WeakMap(); + /** * Copies feature flags that are in current scope context to the event context */ @@ -87,3 +94,43 @@ export function insertToFlagBuffer(flags: FeatureFlag[], name: string, value: un result: value, }); } + +/** + * Records a feature flag evaluation for the active span, adding it to a weak map of flag buffers. This is a no-op for non-boolean values. + * The keys in each buffer are unique. Once the buffer for a span reaches maxFlagsPerSpan, subsequent flags are dropped. + * + * @param name Name of the feature flag. + * @param value Value of the feature flag. Non-boolean values are ignored. + * @param maxFlagsPerSpan Max number of flags a buffer should store. Default value should always be used in production. + */ +export function bufferSpanFeatureFlag( + name: string, + value: unknown, + maxFlagsPerSpan: number = MAX_FLAGS_PER_SPAN, +): void { + const spanFlagMap = GLOBAL_OBJ._spanToFlagBufferMap; + if (!spanFlagMap || typeof value !== 'boolean') { + return; + } + + const span = getActiveSpan(); + if (span) { + const flags = spanFlagMap.get(span) || []; + if (!flags.find(flag => flag.flag === name) && flags.length < maxFlagsPerSpan) { + flags.push({ flag: name, result: value }); + } + spanFlagMap.set(span, flags); + } +} + +/** + * Add the buffered feature flags for a span to the span attributes. Call this on span end. + * + * @param span Span to add flags to. + */ +export function freezeSpanFeatureFlags(span: Span): void { + const flags = GLOBAL_OBJ._spanToFlagBufferMap?.get(span); + if (flags) { + span.setAttributes(Object.fromEntries(flags.map(flag => [`flag.evaluation.${flag.flag}`, flag.result]))); + } +} diff --git a/packages/core/src/utils-hoist/worldwide.ts b/packages/core/src/utils-hoist/worldwide.ts index 3a396d96f809..69e64c7ac98d 100644 --- a/packages/core/src/utils-hoist/worldwide.ts +++ b/packages/core/src/utils-hoist/worldwide.ts @@ -14,7 +14,9 @@ import type { Carrier } from '../carrier'; import type { Client } from '../client'; +import type { FeatureFlag } from '../featureFlags'; import type { SerializedLog } from '../types-hoist/log'; +import type { Span } from '../types-hoist/span'; import type { SdkSource } from './env'; /** Internal global with common properties and Sentry extensions */ @@ -56,6 +58,10 @@ export type InternalGlobal = { */ _sentryModuleMetadata?: Record; _sentryEsmLoaderHookRegistered?: boolean; + /** + * A map of spans to feature flag buffers. Populated by feature flag integrations. + */ + _spanToFlagBufferMap?: WeakMap; } & Carrier; /** Get's the global object for the current JavaScript runtime */ From af6fa724288775080192d87482798f42e4ba8be6 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:45:06 -0400 Subject: [PATCH 02/15] Update docstrs --- .../featureFlags/featureFlagsIntegration.ts | 5 ++--- .../featureFlags/launchdarkly/integration.ts | 10 +++++----- .../featureFlags/openfeature/integration.ts | 16 ++++++++++++---- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts b/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts index aa569380d79f..549c99af2c13 100644 --- a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts +++ b/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts @@ -12,9 +12,8 @@ export interface FeatureFlagsIntegration extends Integration { } /** - * Sentry integration for buffering feature flags manually with an API, and - * capturing them on error events. We recommend you do this on each flag - * evaluation. Flags are buffered per Sentry scope and limited to 100 per event. + * Sentry integration for buffering feature flag evaluations manually with an API, and + * capturing them on error events and spans. * * See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information. * diff --git a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts index eef12f76d634..caa860574ce4 100644 --- a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts +++ b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts @@ -9,7 +9,7 @@ import { import type { LDContext, LDEvaluationDetail, LDInspectionFlagUsedHandler } from './types'; /** - * Sentry integration for capturing feature flags from LaunchDarkly. + * Sentry integration for capturing feature flag evaluations from LaunchDarkly. * * See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information. * @@ -40,10 +40,10 @@ export const launchDarklyIntegration = defineIntegration(() => { }) satisfies IntegrationFn; /** - * LaunchDarkly hook that listens for flag evaluations and updates the `flags` - * context in our Sentry scope. This needs to be registered as an - * 'inspector' in LaunchDarkly initialize() options, separately from - * `launchDarklyIntegration`. Both are needed to collect feature flags on error. + * LaunchDarkly hook to listen for and buffer flag evaluations. This needs to + * be registered as an 'inspector' in LaunchDarkly initialize() options, + * separately from `launchDarklyIntegration`. Both the hook and the integration + * are needed to capture LaunchDarkly flags. */ export function buildLaunchDarklyFlagUsedHandler(): LDInspectionFlagUsedHandler { return { diff --git a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts index 3c5264dcc019..bd5efd1e282f 100644 --- a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts +++ b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts @@ -1,9 +1,17 @@ /** - * OpenFeature integration. + * Sentry integration for capturing OpenFeature feature flag evaluations. * - * Add the openFeatureIntegration() function call to your integration lists. - * Add the integration hook to your OpenFeature object. - * - OpenFeature.getClient().addHooks(new OpenFeatureIntegrationHook()); + * See the [feature flag documentation](https://develop.sentry.dev/sdk/expected-features/#feature-flags) for more information. + * + * @example + * ``` + * import * as Sentry from "@sentry/browser"; + * import { OpenFeature } from "@openfeature/web-sdk"; + * + * Sentry.init(..., integrations: [Sentry.openFeatureIntegration()]); + * OpenFeature.setProvider(new MyProviderOfChoice()); + * OpenFeature.addHooks(new Sentry.OpenFeatureIntegrationHook()); + * ``` */ import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; From 5453c1f6cfe06359ae02ac34516cd9a104aba0a8 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:57:10 -0400 Subject: [PATCH 03/15] Attribute prefix const --- packages/browser/src/utils/featureFlags.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/browser/src/utils/featureFlags.ts b/packages/browser/src/utils/featureFlags.ts index ff85e160c8b0..9ee5fd793a91 100644 --- a/packages/browser/src/utils/featureFlags.ts +++ b/packages/browser/src/utils/featureFlags.ts @@ -18,8 +18,11 @@ export const FLAG_BUFFER_SIZE = 100; */ export const MAX_FLAGS_PER_SPAN = 10; +// Global map of spans to feature flag buffers. Populated by feature flag integrations. GLOBAL_OBJ._spanToFlagBufferMap = new WeakMap(); +const SPAN_FLAG_ATTRIBUTE_PREFIX = 'flag.evaluation.'; + /** * Copies feature flags that are in current scope context to the event context */ @@ -131,6 +134,6 @@ export function bufferSpanFeatureFlag( export function freezeSpanFeatureFlags(span: Span): void { const flags = GLOBAL_OBJ._spanToFlagBufferMap?.get(span); if (flags) { - span.setAttributes(Object.fromEntries(flags.map(flag => [`flag.evaluation.${flag.flag}`, flag.result]))); + span.setAttributes(Object.fromEntries(flags.map(flag => [`${SPAN_FLAG_ATTRIBUTE_PREFIX}${flag.flag}`, flag.result]))); } } From bc183e69920252e1154a4e1442ea6c10a5faff05 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 4 Jun 2025 15:01:47 -0400 Subject: [PATCH 04/15] Handle dup evals of same flag --- packages/browser/src/utils/featureFlags.ts | 17 +++++++++-------- packages/core/src/utils-hoist/worldwide.ts | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/browser/src/utils/featureFlags.ts b/packages/browser/src/utils/featureFlags.ts index 9ee5fd793a91..6baf05f92b72 100644 --- a/packages/browser/src/utils/featureFlags.ts +++ b/packages/browser/src/utils/featureFlags.ts @@ -53,8 +53,7 @@ export function copyFlagsFromScopeToEvent(event: Event): Event { * * @param name Name of the feature flag to insert. * @param value Value of the feature flag. - * @param maxSize Max number of flags the buffer should store. It's recommended - * to keep this consistent across insertions. Default is FLAG_BUFFER_SIZE + * @param maxSize Max number of flags the buffer should store. Default value should always be used in production. */ export function insertFlagToScope(name: string, value: unknown, maxSize: number = FLAG_BUFFER_SIZE): void { const scopeContexts = getCurrentScope().getScopeData().contexts; @@ -68,7 +67,7 @@ export function insertFlagToScope(name: string, value: unknown, maxSize: number /** * Exported for tests. Currently only accepts boolean values (otherwise no-op). */ -export function insertToFlagBuffer(flags: FeatureFlag[], name: string, value: unknown, maxSize: number): void { +export function insertToFlagBuffer(flags: FeatureFlag[], name: string, value: unknown, maxSize: number, allowEviction: boolean = true): void { if (typeof value !== 'boolean') { return; } @@ -87,8 +86,12 @@ export function insertToFlagBuffer(flags: FeatureFlag[], name: string, value: un } if (flags.length === maxSize) { - // If at capacity, pop the earliest flag - O(n) - flags.shift(); + if (allowEviction) { + // If at capacity, pop the earliest flag - O(n) + flags.shift(); + } else { + return; + } } // Push the flag to the end - O(1) @@ -119,9 +122,7 @@ export function bufferSpanFeatureFlag( const span = getActiveSpan(); if (span) { const flags = spanFlagMap.get(span) || []; - if (!flags.find(flag => flag.flag === name) && flags.length < maxFlagsPerSpan) { - flags.push({ flag: name, result: value }); - } + insertToFlagBuffer(flags, name, value, maxFlagsPerSpan, false); spanFlagMap.set(span, flags); } } diff --git a/packages/core/src/utils-hoist/worldwide.ts b/packages/core/src/utils-hoist/worldwide.ts index 69e64c7ac98d..2b0624bbd303 100644 --- a/packages/core/src/utils-hoist/worldwide.ts +++ b/packages/core/src/utils-hoist/worldwide.ts @@ -61,7 +61,7 @@ export type InternalGlobal = { /** * A map of spans to feature flag buffers. Populated by feature flag integrations. */ - _spanToFlagBufferMap?: WeakMap; + _spanToFlagBufferMap?: WeakMap>; } & Carrier; /** Get's the global object for the current JavaScript runtime */ From bc439c86d5a1b4fa5bde0b381d4d3f54fcfc60fd Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Wed, 4 Jun 2025 15:52:10 -0400 Subject: [PATCH 05/15] Update docstrs. Todo: update util unit tests --- packages/browser/src/utils/featureFlags.ts | 26 ++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/browser/src/utils/featureFlags.ts b/packages/browser/src/utils/featureFlags.ts index 6baf05f92b72..d4b591849599 100644 --- a/packages/browser/src/utils/featureFlags.ts +++ b/packages/browser/src/utils/featureFlags.ts @@ -43,18 +43,16 @@ export function copyFlagsFromScopeToEvent(event: Event): Event { } /** - * Creates a feature flags values array in current context if it does not exist - * and inserts the flag into a FeatureFlag array while maintaining ordered LRU - * properties. Not thread-safe. After inserting: - * - `flags` is sorted in order of recency, with the newest flag at the end. - * - No other flags with the same name exist in `flags`. - * - The length of `flags` does not exceed `maxSize`. The oldest flag is evicted - * as needed. + * Inserts a flag into the current scope's context while maintaining ordered LRU properties. + * Not thread-safe. After inserting: + * - The flag buffer is sorted in order of recency, with the newest evaluation at the end. + * - The names in the buffer are always unique. + * - The length of the buffer never exceeds `maxSize`. * * @param name Name of the feature flag to insert. * @param value Value of the feature flag. * @param maxSize Max number of flags the buffer should store. Default value should always be used in production. - */ +*/ export function insertFlagToScope(name: string, value: unknown, maxSize: number = FLAG_BUFFER_SIZE): void { const scopeContexts = getCurrentScope().getScopeData().contexts; if (!scopeContexts.flags) { @@ -65,7 +63,17 @@ export function insertFlagToScope(name: string, value: unknown, maxSize: number } /** - * Exported for tests. Currently only accepts boolean values (otherwise no-op). + * Exported for tests only. Currently only accepts boolean values (otherwise no-op). + * Inserts a flag into a FeatureFlag array while maintaining the following properties: + * - Flags are sorted in order of recency, with the newest evaluation at the end. + * - The flag names are always unique. + * - The length of the array never exceeds `maxSize`. + * + * @param flags The buffer to insert the flag into. + * @param name Name of the feature flag to insert. + * @param value Value of the feature flag. + * @param maxSize Max number of flags the buffer should store. Default value should always be used in production. + * @param allowEviction If true, the oldest flag is evicted when the buffer is full. Otherwise the new flag is dropped. */ export function insertToFlagBuffer(flags: FeatureFlag[], name: string, value: unknown, maxSize: number, allowEviction: boolean = true): void { if (typeof value !== 'boolean') { From 403a02bb5043bc862c95558a074e28db6c6fb154 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Thu, 5 Jun 2025 13:32:08 -0400 Subject: [PATCH 06/15] Nest existing tests under onError folders --- .../featureFlags/featureFlags/{ => onError}/basic/test.ts | 6 +++--- .../featureFlags/featureFlags/{ => onError}/init.js | 0 .../featureFlags/featureFlags/{ => onError}/subject.js | 0 .../featureFlags/featureFlags/{ => onError}/template.html | 0 .../featureFlags/{ => onError}/withScope/test.ts | 4 ++-- .../featureFlags/launchdarkly/{ => onError}/basic/test.ts | 6 +++--- .../featureFlags/launchdarkly/{ => onError}/init.js | 0 .../featureFlags/launchdarkly/{ => onError}/subject.js | 0 .../featureFlags/launchdarkly/{ => onError}/template.html | 0 .../launchdarkly/{ => onError}/withScope/test.ts | 4 ++-- .../featureFlags/openfeature/{ => onError}/basic/test.ts | 6 +++--- .../openfeature/{ => onError}/errorHook/init.js | 0 .../openfeature/{ => onError}/errorHook/test.ts | 6 +++--- .../featureFlags/openfeature/{ => onError}/init.js | 0 .../featureFlags/openfeature/{ => onError}/subject.js | 0 .../featureFlags/openfeature/{ => onError}/template.html | 0 .../openfeature/{ => onError}/withScope/test.ts | 4 ++-- .../featureFlags/statsig/{ => onError}/basic/test.ts | 0 .../integrations/featureFlags/statsig/{ => onError}/init.js | 0 .../featureFlags/statsig/{ => onError}/subject.js | 0 .../featureFlags/statsig/{ => onError}/template.html | 0 .../featureFlags/statsig/{ => onError}/withScope/test.ts | 0 .../featureFlags/unleash/{ => onError}/basic/test.ts | 0 .../integrations/featureFlags/unleash/{ => onError}/init.js | 0 .../featureFlags/unleash/{ => onError}/subject.js | 0 .../featureFlags/unleash/{ => onError}/template.html | 0 .../featureFlags/unleash/{ => onError}/withScope/test.ts | 0 27 files changed, 18 insertions(+), 18 deletions(-) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/{ => onError}/basic/test.ts (90%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/{ => onError}/init.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/{ => onError}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/{ => onError}/template.html (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/{ => onError}/withScope/test.ts (94%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/{ => onError}/basic/test.ts (89%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/{ => onError}/init.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/{ => onError}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/{ => onError}/template.html (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/{ => onError}/withScope/test.ts (94%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/{ => onError}/basic/test.ts (89%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/{ => onError}/errorHook/init.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/{ => onError}/errorHook/test.ts (89%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/{ => onError}/init.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/{ => onError}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/{ => onError}/template.html (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/{ => onError}/withScope/test.ts (94%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/{ => onError}/basic/test.ts (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/{ => onError}/init.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/{ => onError}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/{ => onError}/template.html (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/{ => onError}/withScope/test.ts (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/{ => onError}/basic/test.ts (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/{ => onError}/init.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/{ => onError}/subject.js (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/{ => onError}/template.html (100%) rename dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/{ => onError}/withScope/test.ts (100%) diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts similarity index 90% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/basic/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts index b63583906cc4..cd9e18606ff8 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; +import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/subject.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/subject.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/template.html rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/template.html diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/withScope/test.ts similarity index 94% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/withScope/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/withScope/test.ts index 41418122b526..c7d1e714731e 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/withScope/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts similarity index 89% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/basic/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts index 5d7f58bdb27b..a02cff1b1f17 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; +import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/subject.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/subject.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/template.html rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/template.html diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/withScope/test.ts similarity index 94% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/withScope/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/withScope/test.ts index 78703e4e5389..e26c74e67f28 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/withScope/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts similarity index 89% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/basic/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts index 77112ee82658..5858c6a44c0c 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; +import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts similarity index 89% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts index d8f1e1311dfa..cfba65eb371a 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/errorHook/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; +import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Flag evaluation error hook', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/subject.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/subject.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/template.html rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/template.html diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/withScope/test.ts similarity index 94% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/withScope/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/withScope/test.ts index 67e68becb104..b6540eb7e901 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/withScope/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/basic/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/subject.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/subject.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/template.html rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/template.html diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/withScope/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/basic/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/init.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/init.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/init.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/subject.js similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/subject.js rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/subject.js diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/template.html similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/template.html rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/template.html diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts similarity index 100% rename from dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/withScope/test.ts rename to dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts From ffe9bcdba07c68a393a02cdb001e488679d56338 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Thu, 5 Jun 2025 13:36:52 -0400 Subject: [PATCH 07/15] Fix imports --- .../integrations/featureFlags/statsig/onError/basic/test.ts | 6 +++--- .../featureFlags/statsig/onError/withScope/test.ts | 4 ++-- .../integrations/featureFlags/unleash/onError/basic/test.ts | 6 +++--- .../featureFlags/unleash/onError/withScope/test.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts index cb434e49e86e..5ecf56c98ef8 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; +import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts index 42ee35e4604d..b63a21db894a 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts index b2e522fc78f4..8c2e698ed79f 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; -import { FLAG_BUFFER_SIZE } from '../../constants'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; +import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts index a512882b568a..73496719cf90 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts @@ -1,7 +1,7 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; -import { sentryTest } from '../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../utils/helpers'; +import { sentryTest } from '../../../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { From 7c2c1616bc78fd24078c3edc6d8b6358b36a22d0 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 6 Jun 2025 12:49:30 -0400 Subject: [PATCH 08/15] Fix global type, add unit tests, add generic ffs test --- .../integrations/featureFlags/constants.ts | 4 +- .../featureFlags/onError/basic/test.ts | 6 +- .../featureFlags/onError/withScope/test.ts | 6 +- .../featureFlags/featureFlags/onSpan/init.js | 13 ++++ .../featureFlags/onSpan/subject.js | 28 ++++++++ .../featureFlags/onSpan/template.html | 12 ++++ .../featureFlags/featureFlags/onSpan/test.ts | 68 +++++++++++++++++++ .../launchdarkly/onError/basic/test.ts | 6 +- .../launchdarkly/onError/withScope/test.ts | 6 +- .../openfeature/onError/basic/test.ts | 6 +- .../openfeature/onError/errorHook/test.ts | 6 +- .../openfeature/onError/withScope/test.ts | 6 +- .../statsig/onError/basic/test.ts | 6 +- .../statsig/onError/withScope/test.ts | 6 +- .../unleash/onError/basic/test.ts | 6 +- .../unleash/onError/withScope/test.ts | 6 +- packages/browser/src/utils/featureFlags.ts | 15 +++- .../browser/test/utils/featureFlags.test.ts | 19 ++++++ packages/core/src/utils-hoist/worldwide.ts | 2 +- 19 files changed, 211 insertions(+), 16 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/template.html create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts index 680105d242e5..ba3c35a08241 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts @@ -1 +1,3 @@ -export const FLAG_BUFFER_SIZE = 100; // Corresponds to constant in featureFlags.ts, in browser utils. +// Corresponds to constants in featureFlags.ts, in browser utils. +export const FLAG_BUFFER_SIZE = 100; +export const MAX_FLAGS_PER_SPAN = 10; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts index cd9e18606ff8..742cdd42109b 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/basic/test.ts @@ -1,6 +1,10 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/withScope/test.ts index c7d1e714731e..fecc762d4c99 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onError/withScope/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; import { sentryTest } from '../../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js new file mode 100644 index 000000000000..fa6a67ec3711 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/init.js @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + integrations: [ + Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }), + Sentry.featureFlagsIntegration(), + ], +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/subject.js new file mode 100644 index 000000000000..4c04fd07f314 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/subject.js @@ -0,0 +1,28 @@ +const btnStartSpan = document.getElementById('btnStartSpan'); +const btnEndSpan = document.getElementById('btnEndSpan'); +const btnStartNestedSpan = document.getElementById('btnStartNestedSpan'); +const btnEndNestedSpan = document.getElementById('btnEndNestedSpan'); + +window.withNestedSpans = callback => { + window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => { + window.traceId = rootSpan.spanContext().traceId; + + window.Sentry.startSpan({ name: 'test-span' }, _span => { + window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => { + callback(); + }); + }); + }); +}; + +// btnStartNestedSpan.addEventListener('click', () => { +// Sentry.startSpanManual( +// { name: 'test-nested-span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }, +// async span => { +// await new Promise(resolve => { +// btnEndNestedSpan.addEventListener('click', resolve); +// }); +// span.end(); +// }, +// ); +// }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/template.html new file mode 100644 index 000000000000..59340f55667c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts new file mode 100644 index 000000000000..560570a6fed4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts @@ -0,0 +1,68 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { + type EventAndTraceHeader, + eventAndTraceHeaderRequestParser, + getMultipleSentryEnvelopeRequests, + shouldSkipFeatureFlagsTest, + shouldSkipTracingTest, +} from '../../../../../utils/helpers'; +import { MAX_FLAGS_PER_SPAN } from '../../constants'; + +sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( + page, + 1, + {}, // envelopeType: 'transaction' }, + eventAndTraceHeaderRequestParser, // properFullEnvelopeRequestParser + ); + + // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. + await page.evaluate(maxFlags => { + (window as any).withNestedSpans(() => { + const flagsIntegration = (window as any).Sentry.getClient().getIntegrationByName('FeatureFlags'); + for (let i = 1; i <= maxFlags; i++) { + flagsIntegration.addFeatureFlag(`feat${i}`, false); + } + flagsIntegration.addFeatureFlag(`feat${maxFlags + 1}`, true); // dropped flag + flagsIntegration.addFeatureFlag('feat3', true); // update + }); + return true; + }, MAX_FLAGS_PER_SPAN); + const event = (await envelopeRequestPromise)[0][0]; + const innerSpan = event.spans?.[0]; + const outerSpan = event.spans?.[1]; + const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + + expect(innerSpanFlags).toEqual([]); + + const expectedOuterSpanFlags = []; + for (let i = 1; i <= 2; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); + } + for (let i = 4; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); + } + expectedOuterSpanFlags.push(['flag.evaluation.feat3', true]); + expect(outerSpanFlags).toEqual(expectedOuterSpanFlags); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts index a02cff1b1f17..1c1a04595187 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/basic/test.ts @@ -1,6 +1,10 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/withScope/test.ts index e26c74e67f28..2efb3fdc9ad0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onError/withScope/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; import { sentryTest } from '../../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts index 5858c6a44c0c..84deca47415d 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/basic/test.ts @@ -1,6 +1,10 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts index cfba65eb371a..c2de7f54abd7 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts @@ -1,6 +1,10 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Flag evaluation error hook', async ({ getLocalTestUrl, page }) => { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/withScope/test.ts index b6540eb7e901..14cc072af30d 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/withScope/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; import { sentryTest } from '../../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts index 5ecf56c98ef8..331dbb8ad433 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/basic/test.ts @@ -1,6 +1,10 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts index b63a21db894a..e80c6dbfc5fa 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onError/withScope/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; import { sentryTest } from '../../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts index 8c2e698ed79f..341bbbd03e96 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/basic/test.ts @@ -1,6 +1,10 @@ import { expect } from '@playwright/test'; import { sentryTest } from '../../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; import { FLAG_BUFFER_SIZE } from '../../../constants'; sentryTest('Basic test with eviction, update, and no async tasks', async ({ getLocalTestUrl, page }) => { diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts index 73496719cf90..fe3aec3ff188 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onError/withScope/test.ts @@ -1,7 +1,11 @@ import { expect } from '@playwright/test'; import type { Scope } from '@sentry/browser'; import { sentryTest } from '../../../../../../utils/fixtures'; -import { envelopeRequestParser, shouldSkipFeatureFlagsTest, waitForErrorRequest } from '../../../../../../utils/helpers'; +import { + envelopeRequestParser, + shouldSkipFeatureFlagsTest, + waitForErrorRequest, +} from '../../../../../../utils/helpers'; sentryTest('Flag evaluations in forked scopes are stored separately.', async ({ getLocalTestUrl, page }) => { if (shouldSkipFeatureFlagsTest()) { diff --git a/packages/browser/src/utils/featureFlags.ts b/packages/browser/src/utils/featureFlags.ts index d4b591849599..ffaadeb15779 100644 --- a/packages/browser/src/utils/featureFlags.ts +++ b/packages/browser/src/utils/featureFlags.ts @@ -52,7 +52,7 @@ export function copyFlagsFromScopeToEvent(event: Event): Event { * @param name Name of the feature flag to insert. * @param value Value of the feature flag. * @param maxSize Max number of flags the buffer should store. Default value should always be used in production. -*/ + */ export function insertFlagToScope(name: string, value: unknown, maxSize: number = FLAG_BUFFER_SIZE): void { const scopeContexts = getCurrentScope().getScopeData().contexts; if (!scopeContexts.flags) { @@ -75,7 +75,13 @@ export function insertFlagToScope(name: string, value: unknown, maxSize: number * @param maxSize Max number of flags the buffer should store. Default value should always be used in production. * @param allowEviction If true, the oldest flag is evicted when the buffer is full. Otherwise the new flag is dropped. */ -export function insertToFlagBuffer(flags: FeatureFlag[], name: string, value: unknown, maxSize: number, allowEviction: boolean = true): void { +export function insertToFlagBuffer( + flags: FeatureFlag[], + name: string, + value: unknown, + maxSize: number, + allowEviction: boolean = true, +): void { if (typeof value !== 'boolean') { return; } @@ -98,6 +104,7 @@ export function insertToFlagBuffer(flags: FeatureFlag[], name: string, value: un // If at capacity, pop the earliest flag - O(n) flags.shift(); } else { + return; } } @@ -143,6 +150,8 @@ export function bufferSpanFeatureFlag( export function freezeSpanFeatureFlags(span: Span): void { const flags = GLOBAL_OBJ._spanToFlagBufferMap?.get(span); if (flags) { - span.setAttributes(Object.fromEntries(flags.map(flag => [`${SPAN_FLAG_ATTRIBUTE_PREFIX}${flag.flag}`, flag.result]))); + span.setAttributes( + Object.fromEntries(flags.map(flag => [`${SPAN_FLAG_ATTRIBUTE_PREFIX}${flag.flag}`, flag.result])), + ); } } diff --git a/packages/browser/test/utils/featureFlags.test.ts b/packages/browser/test/utils/featureFlags.test.ts index 1c0bed312590..e60871832261 100644 --- a/packages/browser/test/utils/featureFlags.test.ts +++ b/packages/browser/test/utils/featureFlags.test.ts @@ -59,6 +59,25 @@ describe('flags', () => { ]); }); + it('drops new entries when allowEviction is false and buffer is full', () => { + const buffer: FeatureFlag[] = []; + const maxSize = 0; + insertToFlagBuffer(buffer, 'feat1', true, maxSize, false); + insertToFlagBuffer(buffer, 'feat2', true, maxSize, false); + insertToFlagBuffer(buffer, 'feat3', true, maxSize, false); + + expect(buffer).toEqual([]); + }); + + it('still updates order and values when allowEviction is false and buffer is full', () => { + const buffer: FeatureFlag[] = []; + const maxSize = 1; + insertToFlagBuffer(buffer, 'feat1', false, maxSize, false); + insertToFlagBuffer(buffer, 'feat1', true, maxSize, false); + + expect(buffer).toEqual([{ flag: 'feat1', result: true }]); + }); + it('does not allocate unnecessary space', () => { const buffer: FeatureFlag[] = []; const maxSize = 1000; diff --git a/packages/core/src/utils-hoist/worldwide.ts b/packages/core/src/utils-hoist/worldwide.ts index 2b0624bbd303..69e64c7ac98d 100644 --- a/packages/core/src/utils-hoist/worldwide.ts +++ b/packages/core/src/utils-hoist/worldwide.ts @@ -61,7 +61,7 @@ export type InternalGlobal = { /** * A map of spans to feature flag buffers. Populated by feature flag integrations. */ - _spanToFlagBufferMap?: WeakMap>; + _spanToFlagBufferMap?: WeakMap; } & Carrier; /** Get's the global object for the current JavaScript runtime */ From e6da588f34821495536de981e9b18f8946df25ff Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:24:42 -0400 Subject: [PATCH 09/15] Add ld test --- .../featureFlags/onSpan/subject.js | 12 ---- .../featureFlags/featureFlags/onSpan/test.ts | 5 +- .../featureFlags/launchdarkly/onSpan/init.js | 39 +++++++++++ .../launchdarkly/onSpan/subject.js | 16 +++++ .../launchdarkly/onSpan/template.html | 12 ++++ .../featureFlags/launchdarkly/onSpan/test.ts | 69 +++++++++++++++++++ 6 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/template.html create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/subject.js index 4c04fd07f314..ad874b2bd697 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/subject.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/subject.js @@ -14,15 +14,3 @@ window.withNestedSpans = callback => { }); }); }; - -// btnStartNestedSpan.addEventListener('click', () => { -// Sentry.startSpanManual( -// { name: 'test-nested-span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }, -// async span => { -// await new Promise(resolve => { -// btnEndNestedSpan.addEventListener('click', resolve); -// }); -// span.end(); -// }, -// ); -// }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts index 560570a6fed4..e1ed9e96b1e5 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts @@ -28,8 +28,8 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( page, 1, - {}, // envelopeType: 'transaction' }, - eventAndTraceHeaderRequestParser, // properFullEnvelopeRequestParser + {}, + eventAndTraceHeaderRequestParser, ); // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. @@ -44,6 +44,7 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a }); return true; }, MAX_FLAGS_PER_SPAN); + const event = (await envelopeRequestPromise)[0][0]; const innerSpan = event.spans?.[0]; const outerSpan = event.spans?.[1]; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js new file mode 100644 index 000000000000..9e4b802f28f3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/init.js @@ -0,0 +1,39 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.sentryLDIntegration = Sentry.launchDarklyIntegration(); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + integrations: [ + Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }), + window.sentryLDIntegration, + ], +}); + +// Manually mocking this because LD only has mock test utils for the React SDK. +// Also, no SDK has mock utils for FlagUsedHandler's. +const MockLaunchDarkly = { + initialize(_clientId, context, options) { + const flagUsedHandler = options.inspectors ? options.inspectors[0].method : undefined; + + return { + variation(key, defaultValue) { + if (flagUsedHandler) { + flagUsedHandler(key, { value: defaultValue }, context); + } + return defaultValue; + }, + }; + }, +}; + +window.initializeLD = () => { + return MockLaunchDarkly.initialize( + 'example-client-id', + { kind: 'user', key: 'example-context-key' }, + { inspectors: [Sentry.buildLaunchDarklyFlagUsedHandler()] }, + ); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/subject.js new file mode 100644 index 000000000000..ad874b2bd697 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/subject.js @@ -0,0 +1,16 @@ +const btnStartSpan = document.getElementById('btnStartSpan'); +const btnEndSpan = document.getElementById('btnEndSpan'); +const btnStartNestedSpan = document.getElementById('btnStartNestedSpan'); +const btnEndNestedSpan = document.getElementById('btnEndNestedSpan'); + +window.withNestedSpans = callback => { + window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => { + window.traceId = rootSpan.spanContext().traceId; + + window.Sentry.startSpan({ name: 'test-span' }, _span => { + window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => { + callback(); + }); + }); + }); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/template.html new file mode 100644 index 000000000000..59340f55667c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts new file mode 100644 index 000000000000..a49191f4d4a3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts @@ -0,0 +1,69 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { + type EventAndTraceHeader, + eventAndTraceHeaderRequestParser, + getMultipleSentryEnvelopeRequests, + shouldSkipFeatureFlagsTest, + shouldSkipTracingTest, +} from '../../../../../utils/helpers'; +import { MAX_FLAGS_PER_SPAN } from '../../constants'; + +sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( + page, + 1, + {}, + eventAndTraceHeaderRequestParser, + ); + + // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. + await page.evaluate(maxFlags => { + (window as any).withNestedSpans(() => { + const ldClient = (window as any).initializeLD(); + for (let i = 1; i <= maxFlags; i++) { + ldClient.variation(`feat${i}`, false); + } + ldClient.variation(`feat${maxFlags + 1}`, true); // dropped + ldClient.variation('feat3', true); // update + }); + return true; + }, MAX_FLAGS_PER_SPAN); + + const event = (await envelopeRequestPromise)[0][0]; + const innerSpan = event.spans?.[0]; + const outerSpan = event.spans?.[1]; + const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + + expect(innerSpanFlags).toEqual([]); + + const expectedOuterSpanFlags = []; + for (let i = 1; i <= 2; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); + } + for (let i = 4; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); + } + expectedOuterSpanFlags.push(['flag.evaluation.feat3', true]); + expect(outerSpanFlags).toEqual(expectedOuterSpanFlags); +}); From 2459a11182f12f7076db3b96056b6243f92388ad Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 6 Jun 2025 15:42:17 -0400 Subject: [PATCH 10/15] Add of, stat, unleash tests --- .../featureFlags/openfeature/onSpan/init.js | 25 +++++++ .../openfeature/onSpan/subject.js | 16 ++++ .../openfeature/onSpan/template.html | 12 +++ .../featureFlags/openfeature/onSpan/test.ts | 69 ++++++++++++++++++ .../featureFlags/statsig/onSpan/init.js | 39 ++++++++++ .../featureFlags/statsig/onSpan/subject.js | 16 ++++ .../featureFlags/statsig/onSpan/template.html | 12 +++ .../featureFlags/statsig/onSpan/test.ts | 73 +++++++++++++++++++ .../featureFlags/unleash/onSpan/init.js | 60 +++++++++++++++ .../featureFlags/unleash/onSpan/subject.js | 16 ++++ .../featureFlags/unleash/onSpan/template.html | 12 +++ .../featureFlags/unleash/onSpan/test.ts | 71 ++++++++++++++++++ 12 files changed, 421 insertions(+) create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/template.html create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/template.html create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/template.html create mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js new file mode 100644 index 000000000000..9de421d19e63 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js @@ -0,0 +1,25 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.sentryOpenFeatureIntegration = Sentry.openFeatureIntegration(); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + integrations: [ + window.sentryOpenFeatureIntegration, + Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }), + ], +}); + +window.initialize = () => { + return { + getBooleanValue(flag, value) { + let hook = new Sentry.OpenFeatureIntegrationHook(); + hook.after(null, { flagKey: flag, value: value }); + return value; + }, + }; +}; + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/subject.js new file mode 100644 index 000000000000..ad874b2bd697 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/subject.js @@ -0,0 +1,16 @@ +const btnStartSpan = document.getElementById('btnStartSpan'); +const btnEndSpan = document.getElementById('btnEndSpan'); +const btnStartNestedSpan = document.getElementById('btnStartNestedSpan'); +const btnEndNestedSpan = document.getElementById('btnEndNestedSpan'); + +window.withNestedSpans = callback => { + window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => { + window.traceId = rootSpan.spanContext().traceId; + + window.Sentry.startSpan({ name: 'test-span' }, _span => { + window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => { + callback(); + }); + }); + }); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/template.html new file mode 100644 index 000000000000..59340f55667c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts new file mode 100644 index 000000000000..793b2ed6fffd --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts @@ -0,0 +1,69 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { + type EventAndTraceHeader, + eventAndTraceHeaderRequestParser, + getMultipleSentryEnvelopeRequests, + shouldSkipFeatureFlagsTest, + shouldSkipTracingTest, +} from '../../../../../utils/helpers'; +import { MAX_FLAGS_PER_SPAN } from '../../constants'; + +sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( + page, + 1, + {}, + eventAndTraceHeaderRequestParser, + ); + + // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. + await page.evaluate(maxFlags => { + (window as any).withNestedSpans(() => { + const client = (window as any).initialize(); + for (let i = 1; i <= maxFlags; i++) { + client.getBooleanValue(`feat${i}`, false); + } + client.getBooleanValue(`feat${maxFlags + 1}`, true); // drop + client.getBooleanValue('feat3', true); // update + }); + return true; + }, MAX_FLAGS_PER_SPAN); + + const event = (await envelopeRequestPromise)[0][0]; + const innerSpan = event.spans?.[0]; + const outerSpan = event.spans?.[1]; + const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + + expect(innerSpanFlags).toEqual([]); + + const expectedOuterSpanFlags = []; + for (let i = 1; i <= 2; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); + } + for (let i = 4; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); + } + expectedOuterSpanFlags.push(['flag.evaluation.feat3', true]); + expect(outerSpanFlags).toEqual(expectedOuterSpanFlags); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js new file mode 100644 index 000000000000..22f74d2ebd7c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/init.js @@ -0,0 +1,39 @@ +import * as Sentry from '@sentry/browser'; + +class MockStatsigClient { + constructor() { + this._gateEvaluationListeners = []; + this._mockGateValues = {}; + } + + on(event, listener) { + this._gateEvaluationListeners.push(listener); + } + + checkGate(name) { + const value = this._mockGateValues[name] || false; // unknown features default to false. + this._gateEvaluationListeners.forEach(listener => { + listener({ gate: { name, value } }); + }); + return value; + } + + setMockGateValue(name, value) { + this._mockGateValues[name] = value; + } +} + +window.statsigClient = new MockStatsigClient(); + +window.Sentry = Sentry; +window.sentryStatsigIntegration = Sentry.statsigIntegration({ featureFlagClient: window.statsigClient }); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + integrations: [ + window.sentryStatsigIntegration, + Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }), + ], +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/subject.js new file mode 100644 index 000000000000..ad874b2bd697 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/subject.js @@ -0,0 +1,16 @@ +const btnStartSpan = document.getElementById('btnStartSpan'); +const btnEndSpan = document.getElementById('btnEndSpan'); +const btnStartNestedSpan = document.getElementById('btnStartNestedSpan'); +const btnEndNestedSpan = document.getElementById('btnEndNestedSpan'); + +window.withNestedSpans = callback => { + window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => { + window.traceId = rootSpan.spanContext().traceId; + + window.Sentry.startSpan({ name: 'test-span' }, _span => { + window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => { + callback(); + }); + }); + }); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/template.html new file mode 100644 index 000000000000..59340f55667c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts new file mode 100644 index 000000000000..384f59620cdf --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts @@ -0,0 +1,73 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { + type EventAndTraceHeader, + eventAndTraceHeaderRequestParser, + getMultipleSentryEnvelopeRequests, + shouldSkipFeatureFlagsTest, + shouldSkipTracingTest, +} from '../../../../../utils/helpers'; +import { MAX_FLAGS_PER_SPAN } from '../../constants'; + +sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( + page, + 1, + {}, + eventAndTraceHeaderRequestParser, + ); + + // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. + await page.evaluate(maxFlags => { + (window as any).withNestedSpans(() => { + const client = (window as any).statsigClient; + for (let i = 1; i <= maxFlags; i++) { + client.checkGate(`feat${i}`); // values default to false + } + + client.setMockGateValue(`feat${maxFlags + 1}`, true); + client.checkGate(`feat${maxFlags + 1}`); // dropped + + client.setMockGateValue('feat3', true); + client.checkGate('feat3'); // update + }); + return true; + }, MAX_FLAGS_PER_SPAN); + + const event = (await envelopeRequestPromise)[0][0]; + const innerSpan = event.spans?.[0]; + const outerSpan = event.spans?.[1]; + const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + + expect(innerSpanFlags).toEqual([]); + + const expectedOuterSpanFlags = []; + for (let i = 1; i <= 2; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); + } + for (let i = 4; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); + } + expectedOuterSpanFlags.push(['flag.evaluation.feat3', true]); + expect(outerSpanFlags).toEqual(expectedOuterSpanFlags); +}); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js new file mode 100644 index 000000000000..399ef2fc830a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js @@ -0,0 +1,60 @@ +import * as Sentry from '@sentry/browser'; + +window.UnleashClient = class { + constructor() { + this._featureToVariant = { + strFeat: { name: 'variant1', enabled: true, feature_enabled: true, payload: { type: 'string', value: 'test' } }, + noPayloadFeat: { name: 'eu-west', enabled: true, feature_enabled: true }, + jsonFeat: { + name: 'paid-orgs', + enabled: true, + feature_enabled: true, + payload: { + type: 'json', + value: '{"foo": {"bar": "baz"}, "hello": [1, 2, 3]}', + }, + }, + + // Enabled feature with no configured variants. + noVariantFeat: { name: 'disabled', enabled: false, feature_enabled: true }, + + // Disabled feature. + disabledFeat: { name: 'disabled', enabled: false, feature_enabled: false }, + }; + + // Variant returned for features that don't exist. + // `feature_enabled` may be defined in prod, but we want to test the undefined case. + this._fallbackVariant = { + name: 'disabled', + enabled: false, + }; + } + + isEnabled(toggleName) { + const variant = this._featureToVariant[toggleName] || this._fallbackVariant; + return variant.feature_enabled || false; + } + + getVariant(toggleName) { + return this._featureToVariant[toggleName] || this._fallbackVariant; + } +}; + +// Not a mock UnleashClient class method since it needs to match the signature of the actual UnleashClient. +window.setVariant = (client, featureName, variantName, isEnabled) => { + client._featureToVariant[featureName] = { name: variantName, enabled: isEnabled, feature_enabled: isEnabled }; +} + +window.Sentry = Sentry; +window.sentryUnleashIntegration = Sentry.unleashIntegration({ featureFlagClientClass: window.UnleashClient }); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + integrations: [ + window.sentryUnleashIntegration, + Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }), + ], +}); + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/subject.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/subject.js new file mode 100644 index 000000000000..ad874b2bd697 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/subject.js @@ -0,0 +1,16 @@ +const btnStartSpan = document.getElementById('btnStartSpan'); +const btnEndSpan = document.getElementById('btnEndSpan'); +const btnStartNestedSpan = document.getElementById('btnStartNestedSpan'); +const btnEndNestedSpan = document.getElementById('btnEndNestedSpan'); + +window.withNestedSpans = callback => { + window.Sentry.startSpan({ name: 'test-root-span' }, rootSpan => { + window.traceId = rootSpan.spanContext().traceId; + + window.Sentry.startSpan({ name: 'test-span' }, _span => { + window.Sentry.startSpan({ name: 'test-nested-span' }, _nestedSpan => { + callback(); + }); + }); + }); +}; diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/template.html b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/template.html new file mode 100644 index 000000000000..59340f55667c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts new file mode 100644 index 000000000000..f3cf4c624369 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts @@ -0,0 +1,71 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../../utils/fixtures'; +import { + type EventAndTraceHeader, + eventAndTraceHeaderRequestParser, + getMultipleSentryEnvelopeRequests, + shouldSkipFeatureFlagsTest, + shouldSkipTracingTest, +} from '../../../../../utils/helpers'; +import { MAX_FLAGS_PER_SPAN } from '../../constants'; + +sentryTest("Feature flags are added to active span's attributes on span end.", async ({ getLocalTestUrl, page }) => { + if (shouldSkipFeatureFlagsTest() || shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true }); + await page.goto(url); + + const envelopeRequestPromise = getMultipleSentryEnvelopeRequests( + page, + 1, + {}, + eventAndTraceHeaderRequestParser, + ); + + // withNestedSpans is a util used to start 3 nested spans: root-span (not recorded in transaction_event.spans), span, and nested-span. + await page.evaluate(maxFlags => { + (window as any).withNestedSpans(() => { + const client = new (window as any).UnleashClient(); + for (let i = 1; i <= maxFlags; i++) { + client.isEnabled(`feat${i}`); + } + client.isEnabled(`feat${maxFlags + 1}`); // dropped + + (window as any).setVariant(client, 'feat3', 'var1', true); + client.isEnabled('feat3'); // update + }); + return true; + }, MAX_FLAGS_PER_SPAN); + + const event = (await envelopeRequestPromise)[0][0]; + const innerSpan = event.spans?.[0]; + const outerSpan = event.spans?.[1]; + const outerSpanFlags = Object.entries(outerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + const innerSpanFlags = Object.entries(innerSpan?.data ?? {}).filter(([key, _val]) => + key.startsWith('flag.evaluation'), + ); + + expect(innerSpanFlags).toEqual([]); + + const expectedOuterSpanFlags = []; + for (let i = 1; i <= 2; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); + } + for (let i = 4; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); + } + expectedOuterSpanFlags.push(['flag.evaluation.feat3', true]); + expect(outerSpanFlags).toEqual(expectedOuterSpanFlags); +}); From 3c13997bbb8a76129f00bfe2c2944c948274cf4f Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Fri, 6 Jun 2025 17:15:00 -0500 Subject: [PATCH 11/15] fmt --- .../integrations/featureFlags/openfeature/onSpan/init.js | 1 - .../suites/integrations/featureFlags/unleash/onSpan/init.js | 3 +-- packages/browser/src/utils/featureFlags.ts | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js index 9de421d19e63..4fc1cace150c 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/init.js @@ -22,4 +22,3 @@ window.initialize = () => { }, }; }; - diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js index 399ef2fc830a..93993d8f6188 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/init.js @@ -43,7 +43,7 @@ window.UnleashClient = class { // Not a mock UnleashClient class method since it needs to match the signature of the actual UnleashClient. window.setVariant = (client, featureName, variantName, isEnabled) => { client._featureToVariant[featureName] = { name: variantName, enabled: isEnabled, feature_enabled: isEnabled }; -} +}; window.Sentry = Sentry; window.sentryUnleashIntegration = Sentry.unleashIntegration({ featureFlagClientClass: window.UnleashClient }); @@ -57,4 +57,3 @@ Sentry.init({ Sentry.browserTracingIntegration({ instrumentNavigation: false, instrumentPageLoad: false }), ], }); - diff --git a/packages/browser/src/utils/featureFlags.ts b/packages/browser/src/utils/featureFlags.ts index ffaadeb15779..49ee8ee14a48 100644 --- a/packages/browser/src/utils/featureFlags.ts +++ b/packages/browser/src/utils/featureFlags.ts @@ -104,7 +104,6 @@ export function insertToFlagBuffer( // If at capacity, pop the earliest flag - O(n) flags.shift(); } else { - return; } } From 8085ee8b73e13d0a18227cf4741ae1f6b6ab1cd0 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 10 Jun 2025 10:43:07 -0700 Subject: [PATCH 12/15] Reset unrelated otel test --- .../test/integration/transactions.test.ts | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts b/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts index 0bbb77296a58..3bdf6c113555 100644 --- a/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts +++ b/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts @@ -548,57 +548,57 @@ describe('Integration | Transactions', () => { expect(finishedSpans.length).toBe(0); }); - it('collects child spans that are finished within 5 minutes their parent span has been sent', async () => { - const timeout = 5 * 60 * 1000; - const now = Date.now(); - vi.useFakeTimers(); - vi.setSystemTime(now); +it('collects child spans that are finished within 5 minutes their parent span has been sent', async () => { + const timeout = 5 * 60 * 1000; + const now = Date.now(); + vi.useFakeTimers(); + vi.setSystemTime(now); - const logs: unknown[] = []; - vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + const logs: unknown[] = []; + vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); - const transactions: Event[] = []; + const transactions: Event[] = []; - mockSdkInit({ - tracesSampleRate: 1, - beforeSendTransaction: event => { - transactions.push(event); - return null; - }, - }); + mockSdkInit({ + tracesSampleRate: 1, + beforeSendTransaction: event => { + transactions.push(event); + return null; + }, + }); - const provider = getProvider(); - const spanProcessor = getSpanProcessor(); + const provider = getProvider(); + const spanProcessor = getSpanProcessor(); - const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined; + const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined; - if (!exporter) { - throw new Error('No exporter found, aborting test...'); - } + if (!exporter) { + throw new Error('No exporter found, aborting test...'); + } - startSpanManual({ name: 'test name' }, async span => { - const subSpan = startInactiveSpan({ name: 'inner span 1' }); - subSpan.end(); + startSpanManual({ name: 'test name' }, async span => { + const subSpan = startInactiveSpan({ name: 'inner span 1' }); + subSpan.end(); - const subSpan2 = startInactiveSpan({ name: 'inner span 2' }); + const subSpan2 = startInactiveSpan({ name: 'inner span 2' }); - span.end(); + span.end(); - setTimeout(() => { - subSpan2.end(); - }, timeout - 2); - }); + setTimeout(() => { + subSpan2.end(); + }, timeout - 2); + }); - vi.advanceTimersByTime(timeout - 1); + vi.advanceTimersByTime(timeout - 1); - expect(transactions).toHaveLength(2); - expect(transactions[0]?.spans).toHaveLength(1); + expect(transactions).toHaveLength(2); + expect(transactions[0]?.spans).toHaveLength(1); - const finishedSpans: any = exporter['_finishedSpanBuckets'].flatMap(bucket => - bucket ? Array.from(bucket.spans) : [], - ); - expect(finishedSpans.length).toBe(0); - }); + const finishedSpans: any = exporter['_finishedSpanBuckets'].flatMap(bucket => + bucket ? Array.from(bucket.spans) : [], + ); + expect(finishedSpans.length).toBe(0); +}); it('discards child spans that are finished after 5 minutes their parent span has been sent', async () => { const timeout = 5 * 60 * 1000; From 2b385a47c8f7897a8938f285c100213b10b736a6 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Sat, 14 Jun 2025 17:46:29 -0700 Subject: [PATCH 13/15] Set attr directly on eval instead of in hook --- .../featureFlags/featureFlags/onSpan/test.ts | 11 +++---- .../featureFlags/launchdarkly/onSpan/test.ts | 11 +++---- .../featureFlags/openfeature/onSpan/test.ts | 11 +++---- .../featureFlags/statsig/onSpan/test.ts | 11 +++---- .../featureFlags/unleash/onSpan/test.ts | 11 +++---- .../featureFlags/featureFlagsIntegration.ts | 15 ++------- .../featureFlags/launchdarkly/integration.ts | 11 ++----- .../featureFlags/openfeature/integration.ts | 13 ++------ .../featureFlags/statsig/integration.ts | 15 ++------- .../featureFlags/unleash/integration.ts | 11 ++----- packages/browser/src/utils/featureFlags.ts | 32 +++++++------------ packages/core/src/utils-hoist/worldwide.ts | 4 +-- 12 files changed, 46 insertions(+), 110 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts index e1ed9e96b1e5..476b76d03475 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts @@ -58,12 +58,9 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a expect(innerSpanFlags).toEqual([]); const expectedOuterSpanFlags = []; - for (let i = 1; i <= 2; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); } - for (let i = 4; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); - } - expectedOuterSpanFlags.push(['flag.evaluation.feat3', true]); - expect(outerSpanFlags).toEqual(expectedOuterSpanFlags); + // Order agnostic (attribute dict is unordered). + expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts index a49191f4d4a3..965f00f91fa0 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts @@ -58,12 +58,9 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a expect(innerSpanFlags).toEqual([]); const expectedOuterSpanFlags = []; - for (let i = 1; i <= 2; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); } - for (let i = 4; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); - } - expectedOuterSpanFlags.push(['flag.evaluation.feat3', true]); - expect(outerSpanFlags).toEqual(expectedOuterSpanFlags); + // Order agnostic (attribute dict is unordered). + expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts index 793b2ed6fffd..f3b43425477f 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts @@ -58,12 +58,9 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a expect(innerSpanFlags).toEqual([]); const expectedOuterSpanFlags = []; - for (let i = 1; i <= 2; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); } - for (let i = 4; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); - } - expectedOuterSpanFlags.push(['flag.evaluation.feat3', true]); - expect(outerSpanFlags).toEqual(expectedOuterSpanFlags); + // Order agnostic (attribute dict is unordered). + expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts index 384f59620cdf..dec534f9ffab 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts @@ -62,12 +62,9 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a expect(innerSpanFlags).toEqual([]); const expectedOuterSpanFlags = []; - for (let i = 1; i <= 2; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); } - for (let i = 4; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); - } - expectedOuterSpanFlags.push(['flag.evaluation.feat3', true]); - expect(outerSpanFlags).toEqual(expectedOuterSpanFlags); + // Order agnostic (attribute dict is unordered). + expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); }); diff --git a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts index f3cf4c624369..b2607ffa4c07 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts @@ -60,12 +60,9 @@ sentryTest("Feature flags are added to active span's attributes on span end.", a expect(innerSpanFlags).toEqual([]); const expectedOuterSpanFlags = []; - for (let i = 1; i <= 2; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, i === 3]); } - for (let i = 4; i <= MAX_FLAGS_PER_SPAN; i++) { - expectedOuterSpanFlags.push([`flag.evaluation.feat${i}`, false]); - } - expectedOuterSpanFlags.push(['flag.evaluation.feat3', true]); - expect(outerSpanFlags).toEqual(expectedOuterSpanFlags); + // Order agnostic (attribute dict is unordered). + expect(outerSpanFlags.sort()).toEqual(expectedOuterSpanFlags.sort()); }); diff --git a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts b/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts index 549c99af2c13..f7a1e0bfd1c3 100644 --- a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts +++ b/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts @@ -1,11 +1,6 @@ import type { Client, Event, EventHint, Integration, IntegrationFn, Span } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { - bufferSpanFeatureFlag, - copyFlagsFromScopeToEvent, - freezeSpanFeatureFlags, - insertFlagToScope, -} from '../../utils/featureFlags'; +import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../utils/featureFlags'; export interface FeatureFlagsIntegration extends Integration { addFeatureFlag: (name: string, value: unknown) => void; @@ -39,19 +34,13 @@ export const featureFlagsIntegration = defineIntegration(() => { return { name: 'FeatureFlags', - setup(client: Client) { - client.on('spanEnd', (span: Span) => { - freezeSpanFeatureFlags(span); - }); - }, - processEvent(event: Event, _hint: EventHint, _client: Client): Event { return copyFlagsFromScopeToEvent(event); }, addFeatureFlag(name: string, value: unknown): void { insertFlagToScope(name, value); - bufferSpanFeatureFlag(name, value); + addFeatureFlagToActiveSpan(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 caa860574ce4..91e06e77d18f 100644 --- a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts +++ b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts @@ -1,9 +1,8 @@ import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; import { - bufferSpanFeatureFlag, + addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, - freezeSpanFeatureFlags, insertFlagToScope, } from '../../../utils/featureFlags'; import type { LDContext, LDEvaluationDetail, LDInspectionFlagUsedHandler } from './types'; @@ -27,12 +26,6 @@ export const launchDarklyIntegration = defineIntegration(() => { return { name: 'LaunchDarkly', - setup(client: Client) { - client.on('spanEnd', (span: Span) => { - freezeSpanFeatureFlags(span); - }); - }, - processEvent(event: Event, _hint: EventHint, _client: Client): Event { return copyFlagsFromScopeToEvent(event); }, @@ -57,7 +50,7 @@ export function buildLaunchDarklyFlagUsedHandler(): LDInspectionFlagUsedHandler */ method: (flagKey: string, flagDetail: LDEvaluationDetail, _context: LDContext) => { insertFlagToScope(flagKey, flagDetail.value); - bufferSpanFeatureFlag(flagKey, flagDetail.value); + addFeatureFlagToActiveSpan(flagKey, flagDetail.value); }, }; } diff --git a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts index bd5efd1e282f..108fadfe7146 100644 --- a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts +++ b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts @@ -16,9 +16,8 @@ import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; import { - bufferSpanFeatureFlag, + addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, - freezeSpanFeatureFlags, insertFlagToScope, } from '../../../utils/featureFlags'; import type { EvaluationDetails, HookContext, HookHints, JsonValue, OpenFeatureHook } from './types'; @@ -27,12 +26,6 @@ export const openFeatureIntegration = defineIntegration(() => { return { name: 'OpenFeature', - setup(client: Client) { - client.on('spanEnd', (span: Span) => { - freezeSpanFeatureFlags(span); - }); - }, - processEvent(event: Event, _hint: EventHint, _client: Client): Event { return copyFlagsFromScopeToEvent(event); }, @@ -48,7 +41,7 @@ export class OpenFeatureIntegrationHook implements OpenFeatureHook { */ public after(_hookContext: Readonly>, evaluationDetails: EvaluationDetails): void { insertFlagToScope(evaluationDetails.flagKey, evaluationDetails.value); - bufferSpanFeatureFlag(evaluationDetails.flagKey, evaluationDetails.value); + addFeatureFlagToActiveSpan(evaluationDetails.flagKey, evaluationDetails.value); } /** @@ -56,6 +49,6 @@ export class OpenFeatureIntegrationHook implements OpenFeatureHook { */ public error(hookContext: Readonly>, _error: unknown, _hookHints?: HookHints): void { insertFlagToScope(hookContext.flagKey, hookContext.defaultValue); - bufferSpanFeatureFlag(hookContext.flagKey, hookContext.defaultValue); + addFeatureFlagToActiveSpan(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 641e7412e6a8..082b028f92cb 100644 --- a/packages/browser/src/integrations/featureFlags/statsig/integration.ts +++ b/packages/browser/src/integrations/featureFlags/statsig/integration.ts @@ -1,11 +1,6 @@ import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { - bufferSpanFeatureFlag, - copyFlagsFromScopeToEvent, - freezeSpanFeatureFlags, - insertFlagToScope, -} from '../../../utils/featureFlags'; +import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; import type { FeatureGate, StatsigClient } from './types'; /** @@ -36,14 +31,10 @@ export const statsigIntegration = defineIntegration( return { name: 'Statsig', - setup(client: Client) { - client.on('spanEnd', (span: Span) => { - freezeSpanFeatureFlags(span); - }); - + setup(_client: Client) { statsigClient.on('gate_evaluation', (event: { gate: FeatureGate }) => { insertFlagToScope(event.gate.name, event.gate.value); - bufferSpanFeatureFlag(event.gate.name, event.gate.value); + addFeatureFlagToActiveSpan(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 75559dd3841b..b24a27e8c1bc 100644 --- a/packages/browser/src/integrations/featureFlags/unleash/integration.ts +++ b/packages/browser/src/integrations/featureFlags/unleash/integration.ts @@ -2,9 +2,8 @@ import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core import { defineIntegration, fill, logger } from '@sentry/core'; import { DEBUG_BUILD } from '../../../debug-build'; import { - bufferSpanFeatureFlag, + addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, - freezeSpanFeatureFlags, insertFlagToScope, } from '../../../utils/featureFlags'; import type { UnleashClient, UnleashClientClass } from './types'; @@ -40,12 +39,6 @@ export const unleashIntegration = defineIntegration( return { name: 'Unleash', - setup(client: Client) { - client.on('spanEnd', (span: Span) => { - freezeSpanFeatureFlags(span); - }); - }, - setupOnce() { const unleashClientPrototype = unleashClientClass.prototype as UnleashClient; fill(unleashClientPrototype, 'isEnabled', _wrappedIsEnabled); @@ -76,7 +69,7 @@ function _wrappedIsEnabled( if (typeof toggleName === 'string' && typeof result === 'boolean') { insertFlagToScope(toggleName, result); - bufferSpanFeatureFlag(toggleName, result); + addFeatureFlagToActiveSpan(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 49ee8ee14a48..83120daef930 100644 --- a/packages/browser/src/utils/featureFlags.ts +++ b/packages/browser/src/utils/featureFlags.ts @@ -19,7 +19,7 @@ export const FLAG_BUFFER_SIZE = 100; export const MAX_FLAGS_PER_SPAN = 10; // Global map of spans to feature flag buffers. Populated by feature flag integrations. -GLOBAL_OBJ._spanToFlagBufferMap = new WeakMap(); +GLOBAL_OBJ._spanToFlagBufferMap = new WeakMap>(); const SPAN_FLAG_ATTRIBUTE_PREFIX = 'flag.evaluation.'; @@ -116,14 +116,15 @@ export function insertToFlagBuffer( } /** - * Records a feature flag evaluation for the active span, adding it to a weak map of flag buffers. This is a no-op for non-boolean values. - * The keys in each buffer are unique. Once the buffer for a span reaches maxFlagsPerSpan, subsequent flags are dropped. + * Records a feature flag evaluation for the active span. This is a no-op for non-boolean values. + * The flag and its value is stored in span attributes with the `flag.evaluation` prefix. Once the + * unique flags for a span reaches maxFlagsPerSpan, subsequent flags are dropped. * * @param name Name of the feature flag. * @param value Value of the feature flag. Non-boolean values are ignored. * @param maxFlagsPerSpan Max number of flags a buffer should store. Default value should always be used in production. */ -export function bufferSpanFeatureFlag( +export function addFeatureFlagToActiveSpan( name: string, value: unknown, maxFlagsPerSpan: number = MAX_FLAGS_PER_SPAN, @@ -135,22 +136,13 @@ export function bufferSpanFeatureFlag( const span = getActiveSpan(); if (span) { - const flags = spanFlagMap.get(span) || []; - insertToFlagBuffer(flags, name, value, maxFlagsPerSpan, false); + const flags = spanFlagMap.get(span) || new Set(); + if (flags.has(name)) { + span.setAttribute(`${SPAN_FLAG_ATTRIBUTE_PREFIX}${name}`, value); + } else if (flags.size < maxFlagsPerSpan) { + flags.add(name); + span.setAttribute(`${SPAN_FLAG_ATTRIBUTE_PREFIX}${name}`, value); + } spanFlagMap.set(span, flags); } } - -/** - * Add the buffered feature flags for a span to the span attributes. Call this on span end. - * - * @param span Span to add flags to. - */ -export function freezeSpanFeatureFlags(span: Span): void { - const flags = GLOBAL_OBJ._spanToFlagBufferMap?.get(span); - if (flags) { - span.setAttributes( - Object.fromEntries(flags.map(flag => [`${SPAN_FLAG_ATTRIBUTE_PREFIX}${flag.flag}`, flag.result])), - ); - } -} diff --git a/packages/core/src/utils-hoist/worldwide.ts b/packages/core/src/utils-hoist/worldwide.ts index 69e64c7ac98d..7b3ca9d49707 100644 --- a/packages/core/src/utils-hoist/worldwide.ts +++ b/packages/core/src/utils-hoist/worldwide.ts @@ -59,9 +59,9 @@ export type InternalGlobal = { _sentryModuleMetadata?: Record; _sentryEsmLoaderHookRegistered?: boolean; /** - * A map of spans to feature flag buffers. Populated by feature flag integrations. + * A map of spans to evaluated feature flags. Populated by feature flag integrations. */ - _spanToFlagBufferMap?: WeakMap; + _spanToFlagBufferMap?: WeakMap>; } & Carrier; /** Get's the global object for the current JavaScript runtime */ From 44324cc56458a078e42a4fed914812cd49ec8c7f Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Sat, 14 Jun 2025 17:48:02 -0700 Subject: [PATCH 14/15] Remove allowEviction --- packages/browser/src/utils/featureFlags.ts | 10 ++-------- .../browser/test/utils/featureFlags.test.ts | 19 ------------------- 2 files changed, 2 insertions(+), 27 deletions(-) diff --git a/packages/browser/src/utils/featureFlags.ts b/packages/browser/src/utils/featureFlags.ts index 83120daef930..236cb73a2b32 100644 --- a/packages/browser/src/utils/featureFlags.ts +++ b/packages/browser/src/utils/featureFlags.ts @@ -73,14 +73,12 @@ export function insertFlagToScope(name: string, value: unknown, maxSize: number * @param name Name of the feature flag to insert. * @param value Value of the feature flag. * @param maxSize Max number of flags the buffer should store. Default value should always be used in production. - * @param allowEviction If true, the oldest flag is evicted when the buffer is full. Otherwise the new flag is dropped. */ export function insertToFlagBuffer( flags: FeatureFlag[], name: string, value: unknown, maxSize: number, - allowEviction: boolean = true, ): void { if (typeof value !== 'boolean') { return; @@ -100,12 +98,8 @@ export function insertToFlagBuffer( } if (flags.length === maxSize) { - if (allowEviction) { - // If at capacity, pop the earliest flag - O(n) - flags.shift(); - } else { - return; - } + // If at capacity, pop the earliest flag - O(n) + flags.shift(); } // Push the flag to the end - O(1) diff --git a/packages/browser/test/utils/featureFlags.test.ts b/packages/browser/test/utils/featureFlags.test.ts index e60871832261..1c0bed312590 100644 --- a/packages/browser/test/utils/featureFlags.test.ts +++ b/packages/browser/test/utils/featureFlags.test.ts @@ -59,25 +59,6 @@ describe('flags', () => { ]); }); - it('drops new entries when allowEviction is false and buffer is full', () => { - const buffer: FeatureFlag[] = []; - const maxSize = 0; - insertToFlagBuffer(buffer, 'feat1', true, maxSize, false); - insertToFlagBuffer(buffer, 'feat2', true, maxSize, false); - insertToFlagBuffer(buffer, 'feat3', true, maxSize, false); - - expect(buffer).toEqual([]); - }); - - it('still updates order and values when allowEviction is false and buffer is full', () => { - const buffer: FeatureFlag[] = []; - const maxSize = 1; - insertToFlagBuffer(buffer, 'feat1', false, maxSize, false); - insertToFlagBuffer(buffer, 'feat1', true, maxSize, false); - - expect(buffer).toEqual([{ flag: 'feat1', result: true }]); - }); - it('does not allocate unnecessary space', () => { const buffer: FeatureFlag[] = []; const maxSize = 1000; From ea6872a3ce6d44a74a8b29e25717d238306adc02 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Sat, 14 Jun 2025 17:52:49 -0700 Subject: [PATCH 15/15] Fix --- .../test/integration/transactions.test.ts | 76 +++++++++---------- .../featureFlags/featureFlagsIntegration.ts | 2 +- .../featureFlags/launchdarkly/integration.ts | 8 +- .../featureFlags/openfeature/integration.ts | 8 +- .../featureFlags/statsig/integration.ts | 2 +- .../featureFlags/unleash/integration.ts | 8 +- packages/browser/src/utils/featureFlags.ts | 7 +- packages/core/src/utils-hoist/worldwide.ts | 1 - 8 files changed, 47 insertions(+), 65 deletions(-) diff --git a/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts b/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts index 3bdf6c113555..0bbb77296a58 100644 --- a/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts +++ b/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts @@ -548,57 +548,57 @@ describe('Integration | Transactions', () => { expect(finishedSpans.length).toBe(0); }); -it('collects child spans that are finished within 5 minutes their parent span has been sent', async () => { - const timeout = 5 * 60 * 1000; - const now = Date.now(); - vi.useFakeTimers(); - vi.setSystemTime(now); + it('collects child spans that are finished within 5 minutes their parent span has been sent', async () => { + const timeout = 5 * 60 * 1000; + const now = Date.now(); + vi.useFakeTimers(); + vi.setSystemTime(now); - const logs: unknown[] = []; - vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + const logs: unknown[] = []; + vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); - const transactions: Event[] = []; + const transactions: Event[] = []; - mockSdkInit({ - tracesSampleRate: 1, - beforeSendTransaction: event => { - transactions.push(event); - return null; - }, - }); + mockSdkInit({ + tracesSampleRate: 1, + beforeSendTransaction: event => { + transactions.push(event); + return null; + }, + }); - const provider = getProvider(); - const spanProcessor = getSpanProcessor(); + const provider = getProvider(); + const spanProcessor = getSpanProcessor(); - const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined; + const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined; - if (!exporter) { - throw new Error('No exporter found, aborting test...'); - } + if (!exporter) { + throw new Error('No exporter found, aborting test...'); + } - startSpanManual({ name: 'test name' }, async span => { - const subSpan = startInactiveSpan({ name: 'inner span 1' }); - subSpan.end(); + startSpanManual({ name: 'test name' }, async span => { + const subSpan = startInactiveSpan({ name: 'inner span 1' }); + subSpan.end(); - const subSpan2 = startInactiveSpan({ name: 'inner span 2' }); + const subSpan2 = startInactiveSpan({ name: 'inner span 2' }); - span.end(); + span.end(); - setTimeout(() => { - subSpan2.end(); - }, timeout - 2); - }); + setTimeout(() => { + subSpan2.end(); + }, timeout - 2); + }); - vi.advanceTimersByTime(timeout - 1); + vi.advanceTimersByTime(timeout - 1); - expect(transactions).toHaveLength(2); - expect(transactions[0]?.spans).toHaveLength(1); + expect(transactions).toHaveLength(2); + expect(transactions[0]?.spans).toHaveLength(1); - const finishedSpans: any = exporter['_finishedSpanBuckets'].flatMap(bucket => - bucket ? Array.from(bucket.spans) : [], - ); - expect(finishedSpans.length).toBe(0); -}); + const finishedSpans: any = exporter['_finishedSpanBuckets'].flatMap(bucket => + bucket ? Array.from(bucket.spans) : [], + ); + expect(finishedSpans.length).toBe(0); + }); it('discards child spans that are finished after 5 minutes their parent span has been sent', async () => { const timeout = 5 * 60 * 1000; diff --git a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts b/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts index f7a1e0bfd1c3..e11084c84c2d 100644 --- a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts +++ b/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts @@ -1,4 +1,4 @@ -import type { Client, Event, EventHint, Integration, IntegrationFn, Span } from '@sentry/core'; +import type { Client, Event, EventHint, Integration, IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../utils/featureFlags'; diff --git a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts index 91e06e77d18f..eeb20dc07cf9 100644 --- a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts +++ b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts @@ -1,10 +1,6 @@ -import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; +import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { - addFeatureFlagToActiveSpan, - copyFlagsFromScopeToEvent, - insertFlagToScope, -} from '../../../utils/featureFlags'; +import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; import type { LDContext, LDEvaluationDetail, LDInspectionFlagUsedHandler } from './types'; /** diff --git a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts index 108fadfe7146..79dc97394cce 100644 --- a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts +++ b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts @@ -13,13 +13,9 @@ * OpenFeature.addHooks(new Sentry.OpenFeatureIntegrationHook()); * ``` */ -import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; +import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { - addFeatureFlagToActiveSpan, - copyFlagsFromScopeToEvent, - insertFlagToScope, -} from '../../../utils/featureFlags'; +import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; import type { EvaluationDetails, HookContext, HookHints, JsonValue, OpenFeatureHook } from './types'; export const openFeatureIntegration = defineIntegration(() => { diff --git a/packages/browser/src/integrations/featureFlags/statsig/integration.ts b/packages/browser/src/integrations/featureFlags/statsig/integration.ts index 082b028f92cb..ee472d77669a 100644 --- a/packages/browser/src/integrations/featureFlags/statsig/integration.ts +++ b/packages/browser/src/integrations/featureFlags/statsig/integration.ts @@ -1,4 +1,4 @@ -import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; +import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; import type { FeatureGate, StatsigClient } from './types'; diff --git a/packages/browser/src/integrations/featureFlags/unleash/integration.ts b/packages/browser/src/integrations/featureFlags/unleash/integration.ts index b24a27e8c1bc..ee7e2a3a0d4d 100644 --- a/packages/browser/src/integrations/featureFlags/unleash/integration.ts +++ b/packages/browser/src/integrations/featureFlags/unleash/integration.ts @@ -1,11 +1,7 @@ -import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; +import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { defineIntegration, fill, logger } from '@sentry/core'; import { DEBUG_BUILD } from '../../../debug-build'; -import { - addFeatureFlagToActiveSpan, - copyFlagsFromScopeToEvent, - insertFlagToScope, -} from '../../../utils/featureFlags'; +import { addFeatureFlagToActiveSpan, copyFlagsFromScopeToEvent, insertFlagToScope } from '../../../utils/featureFlags'; import type { UnleashClient, UnleashClientClass } from './types'; type UnleashIntegrationOptions = { diff --git a/packages/browser/src/utils/featureFlags.ts b/packages/browser/src/utils/featureFlags.ts index 236cb73a2b32..9ae389773bcd 100644 --- a/packages/browser/src/utils/featureFlags.ts +++ b/packages/browser/src/utils/featureFlags.ts @@ -74,12 +74,7 @@ export function insertFlagToScope(name: string, value: unknown, maxSize: number * @param value Value of the feature flag. * @param maxSize Max number of flags the buffer should store. Default value should always be used in production. */ -export function insertToFlagBuffer( - flags: FeatureFlag[], - name: string, - value: unknown, - maxSize: number, -): void { +export function insertToFlagBuffer(flags: FeatureFlag[], name: string, value: unknown, maxSize: number): void { if (typeof value !== 'boolean') { return; } diff --git a/packages/core/src/utils-hoist/worldwide.ts b/packages/core/src/utils-hoist/worldwide.ts index 7b3ca9d49707..70196e4b0c8b 100644 --- a/packages/core/src/utils-hoist/worldwide.ts +++ b/packages/core/src/utils-hoist/worldwide.ts @@ -14,7 +14,6 @@ import type { Carrier } from '../carrier'; import type { Client } from '../client'; -import type { FeatureFlag } from '../featureFlags'; import type { SerializedLog } from '../types-hoist/log'; import type { Span } from '../types-hoist/span'; import type { SdkSource } from './env';