From 70ccac1034c1b23cd86d8d3fcdbc9889db7fd8fe Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Mon, 9 Jun 2025 21:13:16 -0500 Subject: [PATCH 01/32] Dup integration and utils to core, todos in index and integration --- packages/browser/src/index.ts | 4 +- packages/core/src/index.ts | 3 + .../featureFlags/featureFlagsIntegration.ts | 60 +++++++ .../src/integrations/featureFlags/index.ts | 1 + packages/core/src/utils/featureFlags.ts | 161 ++++++++++++++++++ .../core/test/lib/utils/featureFlags.test.ts | 128 ++++++++++++++ 6 files changed, 355 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts create mode 100644 packages/core/src/integrations/featureFlags/index.ts create mode 100644 packages/core/src/utils/featureFlags.ts create mode 100644 packages/core/test/lib/utils/featureFlags.test.ts diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index 2b2279c099b3..963d8ab38546 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -61,13 +61,13 @@ export { instrumentSupabaseClient, zodErrorsIntegration, thirdPartyErrorFilterIntegration, + featureFlagsIntegration, } from '@sentry/core'; -export type { Span } from '@sentry/core'; +export type { Span, FeatureFlagsIntegration } from '@sentry/core'; export { makeBrowserOfflineTransport } from './transports/offline'; export { browserProfilingIntegration } from './profiling/integration'; export { spotlightBrowserIntegration } from './integrations/spotlight'; export { browserSessionIntegration } from './integrations/browsersession'; -export { featureFlagsIntegration, type FeatureFlagsIntegration } from './integrations/featureFlags'; export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler } from './integrations/featureFlags/launchdarkly'; export { openFeatureIntegration, OpenFeatureIntegrationHook } from './integrations/featureFlags/openfeature'; export { unleashIntegration } from './integrations/featureFlags/unleash'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 986d18a972d2..07dba32ee0b0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -112,6 +112,7 @@ export { supabaseIntegration, instrumentSupabaseClient } from './integrations/su export { zodErrorsIntegration } from './integrations/zoderrors'; export { thirdPartyErrorFilterIntegration } from './integrations/third-party-errors-filter'; export { consoleIntegration } from './integrations/console'; +export { featureFlagsIntegration, type FeatureFlagsIntegration } from './integrations/featureFlags'; export { profiler } from './profiling'; export { instrumentFetchRequest } from './fetch'; @@ -123,6 +124,8 @@ export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_captureSeria export { consoleLoggingIntegration } from './logs/console-integration'; export type { FeatureFlag } from './featureFlags'; +// TODO: ^export from utils? +// TODO: how to export utils without making public? export { applyAggregateErrorsToEvent } from './utils-hoist/aggregate-errors'; export { getBreadcrumbLogLevelFromHttpStatusCode } from './utils-hoist/breadcrumb-log-level'; diff --git a/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts b/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts new file mode 100644 index 000000000000..7d5f3470d4ac --- /dev/null +++ b/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts @@ -0,0 +1,60 @@ +import { type Client } from '../../client'; +import { defineIntegration } from '../../integration'; +import { type Event, type EventHint } from '../../types-hoist/event'; +import { type Integration, type IntegrationFn } from '../../types-hoist/integration'; +import { type Span } from '../../types-hoist/span'; +import { + bufferSpanFeatureFlag, + copyFlagsFromScopeToEvent, + freezeSpanFeatureFlags, + insertFlagToScope, +} from '../../utils/featureFlags'; + +export interface FeatureFlagsIntegration extends Integration { + addFeatureFlag: (name: string, value: unknown) => void; +} + +/** + * 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. + * + * @example + * ``` + * import * as Sentry from '@sentry/*'; //TODO: + * import { type FeatureFlagsIntegration } from '@sentry/*'; + * + * // Setup + * Sentry.init(..., integrations: [Sentry.featureFlagsIntegration()]) + * + * // Verify + * const flagsIntegration = Sentry.getClient()?.getIntegrationByName('FeatureFlags'); + * if (flagsIntegration) { + * flagsIntegration.addFeatureFlag('my-flag', true); + * } else { + * // check your setup + * } + * Sentry.captureException(Exception('broke')); // 'my-flag' should be captured to this Sentry event. + * ``` + */ +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/core/src/integrations/featureFlags/index.ts b/packages/core/src/integrations/featureFlags/index.ts new file mode 100644 index 000000000000..2106ee7accf0 --- /dev/null +++ b/packages/core/src/integrations/featureFlags/index.ts @@ -0,0 +1 @@ +export { featureFlagsIntegration, type FeatureFlagsIntegration } from './featureFlagsIntegration'; diff --git a/packages/core/src/utils/featureFlags.ts b/packages/core/src/utils/featureFlags.ts new file mode 100644 index 000000000000..04afb2fc95fd --- /dev/null +++ b/packages/core/src/utils/featureFlags.ts @@ -0,0 +1,161 @@ +import { getCurrentScope } from '../currentScopes'; +import { DEBUG_BUILD } from '../debug-build'; +import { type FeatureFlag } from '../featureFlags'; +import { type Event } from '../types-hoist/event'; +import { type Span } from '../types-hoist/span'; +import { logger } from '../utils-hoist/logger'; +import { GLOBAL_OBJ } from '../utils-hoist/worldwide'; +import { getActiveSpan } from './spanUtils'; + +/** + * Ordered LRU cache for storing feature flags in the scope context. The name + * of each flag in the buffer is unique, and the output of getAll() is ordered + * from oldest to newest. + */ + +/** + * Max size of the LRU flag buffer stored in Sentry scope and event contexts. + */ +export const FLAG_BUFFER_SIZE = 100; + +/** + * Max number of flag evaluations to record per span. + */ +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 + */ +export function copyFlagsFromScopeToEvent(event: Event): Event { + const scope = getCurrentScope(); + const flagContext = scope.getScopeData().contexts.flags; + const flagBuffer = flagContext ? flagContext.values : []; + + if (!flagBuffer.length) { + return event; + } + + if (event.contexts === undefined) { + event.contexts = {}; + } + event.contexts.flags = { values: [...flagBuffer] }; + return event; +} + +/** + * 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) { + scopeContexts.flags = { values: [] }; + } + const flags = scopeContexts.flags.values as FeatureFlag[]; + insertToFlagBuffer(flags, name, value, maxSize); +} + +/** + * 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') { + return; + } + + if (flags.length > maxSize) { + DEBUG_BUILD && logger.error(`[Feature Flags] insertToFlagBuffer called on a buffer larger than maxSize=${maxSize}`); + return; + } + + // Check if the flag is already in the buffer - O(n) + const index = flags.findIndex(f => f.flag === name); + + if (index !== -1) { + // The flag was found, remove it from its current position - O(n) + flags.splice(index, 1); + } + + if (flags.length === maxSize) { + if (allowEviction) { + // If at capacity, pop the earliest flag - O(n) + flags.shift(); + } else { + return; + } + } + + // Push the flag to the end - O(1) + flags.push({ + flag: name, + 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) || []; + insertToFlagBuffer(flags, name, value, maxFlagsPerSpan, false); + 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/test/lib/utils/featureFlags.test.ts b/packages/core/test/lib/utils/featureFlags.test.ts new file mode 100644 index 000000000000..ded0f5066427 --- /dev/null +++ b/packages/core/test/lib/utils/featureFlags.test.ts @@ -0,0 +1,128 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { getCurrentScope } from '../../../src/currentScopes'; +import { type FeatureFlag } from '../../../src/featureFlags'; +import { insertFlagToScope, insertToFlagBuffer } from '../../../src/utils/featureFlags'; +import { logger } from '../../../src/utils-hoist/logger'; + +describe('flags', () => { + describe('insertFlagToScope()', () => { + it('adds flags to the current scope context', () => { + const maxSize = 3; + insertFlagToScope('feat1', true, maxSize); + insertFlagToScope('feat2', true, maxSize); + insertFlagToScope('feat3', true, maxSize); + insertFlagToScope('feat4', true, maxSize); + + const scope = getCurrentScope(); + expect(scope.getScopeData().contexts.flags?.values).toEqual([ + { flag: 'feat2', result: true }, + { flag: 'feat3', result: true }, + { flag: 'feat4', result: true }, + ]); + }); + }); + + describe('insertToFlagBuffer()', () => { + const loggerSpy = vi.spyOn(logger, 'error'); + + afterEach(() => { + loggerSpy.mockClear(); + }); + + it('maintains ordering and evicts the oldest entry', () => { + const buffer: FeatureFlag[] = []; + const maxSize = 3; + insertToFlagBuffer(buffer, 'feat1', true, maxSize); + insertToFlagBuffer(buffer, 'feat2', true, maxSize); + insertToFlagBuffer(buffer, 'feat3', true, maxSize); + insertToFlagBuffer(buffer, 'feat4', true, maxSize); + + expect(buffer).toEqual([ + { flag: 'feat2', result: true }, + { flag: 'feat3', result: true }, + { flag: 'feat4', result: true }, + ]); + }); + + it('does not duplicate same-name flags and updates order and values', () => { + const buffer: FeatureFlag[] = []; + const maxSize = 3; + insertToFlagBuffer(buffer, 'feat1', true, maxSize); + insertToFlagBuffer(buffer, 'feat2', true, maxSize); + insertToFlagBuffer(buffer, 'feat3', true, maxSize); + insertToFlagBuffer(buffer, 'feat3', false, maxSize); + insertToFlagBuffer(buffer, 'feat1', false, maxSize); + + expect(buffer).toEqual([ + { flag: 'feat2', result: true }, + { flag: 'feat3', result: false }, + { flag: 'feat1', result: false }, + ]); + }); + + 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; + insertToFlagBuffer(buffer, 'feat1', true, maxSize); + insertToFlagBuffer(buffer, 'feat2', true, maxSize); + + expect(buffer).toEqual([ + { flag: 'feat1', result: true }, + { flag: 'feat2', result: true }, + ]); + }); + + it('does not accept non-boolean values', () => { + const buffer: FeatureFlag[] = []; + const maxSize = 1000; + insertToFlagBuffer(buffer, 'feat1', 1, maxSize); + insertToFlagBuffer(buffer, 'feat2', 'string', maxSize); + + expect(buffer).toEqual([]); + }); + + it('logs error and is a no-op when buffer is larger than maxSize', () => { + const buffer: FeatureFlag[] = [ + { flag: 'feat1', result: true }, + { flag: 'feat2', result: true }, + ]; + + insertToFlagBuffer(buffer, 'feat1', true, 1); + expect(loggerSpy).toHaveBeenCalledWith( + expect.stringContaining('[Feature Flags] insertToFlagBuffer called on a buffer larger than maxSize'), + ); + expect(buffer).toEqual([ + { flag: 'feat1', result: true }, + { flag: 'feat2', result: true }, + ]); + + insertToFlagBuffer(buffer, 'feat1', true, -2); + expect(loggerSpy).toHaveBeenCalledWith( + expect.stringContaining('[Feature Flags] insertToFlagBuffer called on a buffer larger than maxSize'), + ); + expect(buffer).toEqual([ + { flag: 'feat1', result: true }, + { flag: 'feat2', result: true }, + ]); + }); + }); +}); From 6381f91c743cc647a4d68d61fcd8fecdd8a9d129 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:46:24 -0700 Subject: [PATCH 02/32] Import utils from core with _INTERNAL_ prefix --- .../featureFlags/launchdarkly/integration.ts | 20 ++++---- .../featureFlags/openfeature/integration.ts | 24 ++++----- .../featureFlags/statsig/integration.ts | 20 ++++---- .../featureFlags/unleash/integration.ts | 22 ++++---- packages/core/src/index.ts | 2 +- .../featureFlags/featureFlagsIntegration.ts | 16 +++--- packages/core/src/utils/featureFlags.ts | 18 +++---- .../core/test/lib/utils/featureFlags.test.ts | 50 +++++++++---------- 8 files changed, 87 insertions(+), 85 deletions(-) diff --git a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts index caa860574ce4..5890c92baf00 100644 --- a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts +++ b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts @@ -1,11 +1,11 @@ import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; import { - bufferSpanFeatureFlag, - copyFlagsFromScopeToEvent, - freezeSpanFeatureFlags, - insertFlagToScope, -} from '../../../utils/featureFlags'; + defineIntegration, + _INTERNAL_bufferSpanFeatureFlag, + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_freezeSpanFeatureFlags, + _INTERNAL_insertFlagToScope, +} from '@sentry/core'; import type { LDContext, LDEvaluationDetail, LDInspectionFlagUsedHandler } from './types'; /** @@ -29,12 +29,12 @@ export const launchDarklyIntegration = defineIntegration(() => { setup(client: Client) { client.on('spanEnd', (span: Span) => { - freezeSpanFeatureFlags(span); + _INTERNAL_freezeSpanFeatureFlags(span); }); }, processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); + return _INTERNAL_copyFlagsFromScopeToEvent(event); }, }; }) satisfies IntegrationFn; @@ -56,8 +56,8 @@ export function buildLaunchDarklyFlagUsedHandler(): LDInspectionFlagUsedHandler * Handle a flag evaluation by storing its name and value on the current scope. */ method: (flagKey: string, flagDetail: LDEvaluationDetail, _context: LDContext) => { - insertFlagToScope(flagKey, flagDetail.value); - bufferSpanFeatureFlag(flagKey, flagDetail.value); + _INTERNAL_insertFlagToScope(flagKey, flagDetail.value); + _INTERNAL_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 bd5efd1e282f..2f80202d6613 100644 --- a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts +++ b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts @@ -14,13 +14,13 @@ * ``` */ import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; import { - bufferSpanFeatureFlag, - copyFlagsFromScopeToEvent, - freezeSpanFeatureFlags, - insertFlagToScope, -} from '../../../utils/featureFlags'; + defineIntegration, + _INTERNAL_bufferSpanFeatureFlag, + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_freezeSpanFeatureFlags, + _INTERNAL_insertFlagToScope, +} from '@sentry/core'; import type { EvaluationDetails, HookContext, HookHints, JsonValue, OpenFeatureHook } from './types'; export const openFeatureIntegration = defineIntegration(() => { @@ -29,12 +29,12 @@ export const openFeatureIntegration = defineIntegration(() => { setup(client: Client) { client.on('spanEnd', (span: Span) => { - freezeSpanFeatureFlags(span); + _INTERNAL_freezeSpanFeatureFlags(span); }); }, processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); + return _INTERNAL_copyFlagsFromScopeToEvent(event); }, }; }) satisfies IntegrationFn; @@ -47,15 +47,15 @@ export class OpenFeatureIntegrationHook implements OpenFeatureHook { * Successful evaluation result. */ public after(_hookContext: Readonly>, evaluationDetails: EvaluationDetails): void { - insertFlagToScope(evaluationDetails.flagKey, evaluationDetails.value); - bufferSpanFeatureFlag(evaluationDetails.flagKey, evaluationDetails.value); + _INTERNAL_insertFlagToScope(evaluationDetails.flagKey, evaluationDetails.value); + _INTERNAL_bufferSpanFeatureFlag(evaluationDetails.flagKey, evaluationDetails.value); } /** * On error evaluation result. */ public error(hookContext: Readonly>, _error: unknown, _hookHints?: HookHints): void { - insertFlagToScope(hookContext.flagKey, hookContext.defaultValue); - bufferSpanFeatureFlag(hookContext.flagKey, hookContext.defaultValue); + _INTERNAL_insertFlagToScope(hookContext.flagKey, hookContext.defaultValue); + _INTERNAL_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 641e7412e6a8..e7ce439049db 100644 --- a/packages/browser/src/integrations/featureFlags/statsig/integration.ts +++ b/packages/browser/src/integrations/featureFlags/statsig/integration.ts @@ -1,11 +1,11 @@ import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; import { - bufferSpanFeatureFlag, - copyFlagsFromScopeToEvent, - freezeSpanFeatureFlags, - insertFlagToScope, -} from '../../../utils/featureFlags'; + defineIntegration, + _INTERNAL_bufferSpanFeatureFlag, + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_freezeSpanFeatureFlags, + _INTERNAL_insertFlagToScope, +} from '@sentry/core'; import type { FeatureGate, StatsigClient } from './types'; /** @@ -38,17 +38,17 @@ export const statsigIntegration = defineIntegration( setup(client: Client) { client.on('spanEnd', (span: Span) => { - freezeSpanFeatureFlags(span); + _INTERNAL_freezeSpanFeatureFlags(span); }); statsigClient.on('gate_evaluation', (event: { gate: FeatureGate }) => { - insertFlagToScope(event.gate.name, event.gate.value); - bufferSpanFeatureFlag(event.gate.name, event.gate.value); + _INTERNAL_insertFlagToScope(event.gate.name, event.gate.value); + _INTERNAL_bufferSpanFeatureFlag(event.gate.name, event.gate.value); }); }, processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); + return _INTERNAL_copyFlagsFromScopeToEvent(event); }, }; }, diff --git a/packages/browser/src/integrations/featureFlags/unleash/integration.ts b/packages/browser/src/integrations/featureFlags/unleash/integration.ts index 75559dd3841b..063569cf8310 100644 --- a/packages/browser/src/integrations/featureFlags/unleash/integration.ts +++ b/packages/browser/src/integrations/featureFlags/unleash/integration.ts @@ -1,12 +1,14 @@ 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, - copyFlagsFromScopeToEvent, - freezeSpanFeatureFlags, - insertFlagToScope, -} from '../../../utils/featureFlags'; + defineIntegration, + fill, + logger, + _INTERNAL_bufferSpanFeatureFlag, + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_freezeSpanFeatureFlags, + _INTERNAL_insertFlagToScope, +} from '@sentry/core'; import type { UnleashClient, UnleashClientClass } from './types'; type UnleashIntegrationOptions = { @@ -42,7 +44,7 @@ export const unleashIntegration = defineIntegration( setup(client: Client) { client.on('spanEnd', (span: Span) => { - freezeSpanFeatureFlags(span); + _INTERNAL_freezeSpanFeatureFlags(span); }); }, @@ -52,7 +54,7 @@ export const unleashIntegration = defineIntegration( }, processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); + return _INTERNAL_copyFlagsFromScopeToEvent(event); }, }; }, @@ -75,8 +77,8 @@ function _wrappedIsEnabled( const result = original.apply(this, args); if (typeof toggleName === 'string' && typeof result === 'boolean') { - insertFlagToScope(toggleName, result); - bufferSpanFeatureFlag(toggleName, result); + _INTERNAL_insertFlagToScope(toggleName, result); + _INTERNAL_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/core/src/index.ts b/packages/core/src/index.ts index 07dba32ee0b0..f2027370a235 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -125,7 +125,7 @@ export { consoleLoggingIntegration } from './logs/console-integration'; export type { FeatureFlag } from './featureFlags'; // TODO: ^export from utils? -// TODO: how to export utils without making public? +export { _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_insertFlagToScope, _INTERNAL_bufferSpanFeatureFlag, _INTERNAL_freezeSpanFeatureFlags } from './utils/featureFlags'; export { applyAggregateErrorsToEvent } from './utils-hoist/aggregate-errors'; export { getBreadcrumbLogLevelFromHttpStatusCode } from './utils-hoist/breadcrumb-log-level'; diff --git a/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts b/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts index 7d5f3470d4ac..ad806200adc5 100644 --- a/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts +++ b/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts @@ -4,10 +4,10 @@ import { type Event, type EventHint } from '../../types-hoist/event'; import { type Integration, type IntegrationFn } from '../../types-hoist/integration'; import { type Span } from '../../types-hoist/span'; import { - bufferSpanFeatureFlag, - copyFlagsFromScopeToEvent, - freezeSpanFeatureFlags, - insertFlagToScope, + _INTERNAL_bufferSpanFeatureFlag, + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_freezeSpanFeatureFlags, + _INTERNAL_insertFlagToScope, } from '../../utils/featureFlags'; export interface FeatureFlagsIntegration extends Integration { @@ -44,17 +44,17 @@ export const featureFlagsIntegration = defineIntegration(() => { setup(client: Client) { client.on('spanEnd', (span: Span) => { - freezeSpanFeatureFlags(span); + _INTERNAL_freezeSpanFeatureFlags(span); }); }, processEvent(event: Event, _hint: EventHint, _client: Client): Event { - return copyFlagsFromScopeToEvent(event); + return _INTERNAL_copyFlagsFromScopeToEvent(event); }, addFeatureFlag(name: string, value: unknown): void { - insertFlagToScope(name, value); - bufferSpanFeatureFlag(name, value); + _INTERNAL_insertFlagToScope(name, value); + _INTERNAL_bufferSpanFeatureFlag(name, value); }, }; }) as IntegrationFn; diff --git a/packages/core/src/utils/featureFlags.ts b/packages/core/src/utils/featureFlags.ts index 04afb2fc95fd..ced582feef43 100644 --- a/packages/core/src/utils/featureFlags.ts +++ b/packages/core/src/utils/featureFlags.ts @@ -16,12 +16,12 @@ import { getActiveSpan } from './spanUtils'; /** * Max size of the LRU flag buffer stored in Sentry scope and event contexts. */ -export const FLAG_BUFFER_SIZE = 100; +const FLAG_BUFFER_SIZE = 100; /** * Max number of flag evaluations to record per span. */ -export const MAX_FLAGS_PER_SPAN = 10; +const MAX_FLAGS_PER_SPAN = 10; // Global map of spans to feature flag buffers. Populated by feature flag integrations. GLOBAL_OBJ._spanToFlagBufferMap = new WeakMap(); @@ -31,7 +31,7 @@ const SPAN_FLAG_ATTRIBUTE_PREFIX = 'flag.evaluation.'; /** * Copies feature flags that are in current scope context to the event context */ -export function copyFlagsFromScopeToEvent(event: Event): Event { +export function _INTERNAL_copyFlagsFromScopeToEvent(event: Event): Event { const scope = getCurrentScope(); const flagContext = scope.getScopeData().contexts.flags; const flagBuffer = flagContext ? flagContext.values : []; @@ -58,13 +58,13 @@ export function copyFlagsFromScopeToEvent(event: Event): Event { * @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 { +export function _INTERNAL_insertFlagToScope(name: string, value: unknown, maxSize: number = FLAG_BUFFER_SIZE): void { const scopeContexts = getCurrentScope().getScopeData().contexts; if (!scopeContexts.flags) { scopeContexts.flags = { values: [] }; } const flags = scopeContexts.flags.values as FeatureFlag[]; - insertToFlagBuffer(flags, name, value, maxSize); + _INTERNAL_insertToFlagBuffer(flags, name, value, maxSize); } /** @@ -80,7 +80,7 @@ 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( +export function _INTERNAL_insertToFlagBuffer( flags: FeatureFlag[], name: string, value: unknown, @@ -128,7 +128,7 @@ export function insertToFlagBuffer( * @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 _INTERNAL_bufferSpanFeatureFlag( name: string, value: unknown, maxFlagsPerSpan: number = MAX_FLAGS_PER_SPAN, @@ -141,7 +141,7 @@ export function bufferSpanFeatureFlag( const span = getActiveSpan(); if (span) { const flags = spanFlagMap.get(span) || []; - insertToFlagBuffer(flags, name, value, maxFlagsPerSpan, false); + _INTERNAL_insertToFlagBuffer(flags, name, value, maxFlagsPerSpan, false); spanFlagMap.set(span, flags); } } @@ -151,7 +151,7 @@ export function bufferSpanFeatureFlag( * * @param span Span to add flags to. */ -export function freezeSpanFeatureFlags(span: Span): void { +export function _INTERNAL_freezeSpanFeatureFlags(span: Span): void { const flags = GLOBAL_OBJ._spanToFlagBufferMap?.get(span); if (flags) { span.setAttributes( diff --git a/packages/core/test/lib/utils/featureFlags.test.ts b/packages/core/test/lib/utils/featureFlags.test.ts index ded0f5066427..c14a9c4240c8 100644 --- a/packages/core/test/lib/utils/featureFlags.test.ts +++ b/packages/core/test/lib/utils/featureFlags.test.ts @@ -1,17 +1,17 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { getCurrentScope } from '../../../src/currentScopes'; import { type FeatureFlag } from '../../../src/featureFlags'; -import { insertFlagToScope, insertToFlagBuffer } from '../../../src/utils/featureFlags'; +import { _INTERNAL_insertFlagToScope, _INTERNAL_insertToFlagBuffer } from '../../../src/utils/featureFlags'; import { logger } from '../../../src/utils-hoist/logger'; describe('flags', () => { describe('insertFlagToScope()', () => { it('adds flags to the current scope context', () => { const maxSize = 3; - insertFlagToScope('feat1', true, maxSize); - insertFlagToScope('feat2', true, maxSize); - insertFlagToScope('feat3', true, maxSize); - insertFlagToScope('feat4', true, maxSize); + _INTERNAL_insertFlagToScope('feat1', true, maxSize); + _INTERNAL_insertFlagToScope('feat2', true, maxSize); + _INTERNAL_insertFlagToScope('feat3', true, maxSize); + _INTERNAL_insertFlagToScope('feat4', true, maxSize); const scope = getCurrentScope(); expect(scope.getScopeData().contexts.flags?.values).toEqual([ @@ -32,10 +32,10 @@ describe('flags', () => { it('maintains ordering and evicts the oldest entry', () => { const buffer: FeatureFlag[] = []; const maxSize = 3; - insertToFlagBuffer(buffer, 'feat1', true, maxSize); - insertToFlagBuffer(buffer, 'feat2', true, maxSize); - insertToFlagBuffer(buffer, 'feat3', true, maxSize); - insertToFlagBuffer(buffer, 'feat4', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat2', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat3', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat4', true, maxSize); expect(buffer).toEqual([ { flag: 'feat2', result: true }, @@ -47,11 +47,11 @@ describe('flags', () => { it('does not duplicate same-name flags and updates order and values', () => { const buffer: FeatureFlag[] = []; const maxSize = 3; - insertToFlagBuffer(buffer, 'feat1', true, maxSize); - insertToFlagBuffer(buffer, 'feat2', true, maxSize); - insertToFlagBuffer(buffer, 'feat3', true, maxSize); - insertToFlagBuffer(buffer, 'feat3', false, maxSize); - insertToFlagBuffer(buffer, 'feat1', false, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat2', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat3', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat3', false, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', false, maxSize); expect(buffer).toEqual([ { flag: 'feat2', result: true }, @@ -63,9 +63,9 @@ 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); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, maxSize, false); + _INTERNAL_insertToFlagBuffer(buffer, 'feat2', true, maxSize, false); + _INTERNAL_insertToFlagBuffer(buffer, 'feat3', true, maxSize, false); expect(buffer).toEqual([]); }); @@ -73,8 +73,8 @@ describe('flags', () => { 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); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', false, maxSize, false); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, maxSize, false); expect(buffer).toEqual([{ flag: 'feat1', result: true }]); }); @@ -82,8 +82,8 @@ describe('flags', () => { it('does not allocate unnecessary space', () => { const buffer: FeatureFlag[] = []; const maxSize = 1000; - insertToFlagBuffer(buffer, 'feat1', true, maxSize); - insertToFlagBuffer(buffer, 'feat2', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat2', true, maxSize); expect(buffer).toEqual([ { flag: 'feat1', result: true }, @@ -94,8 +94,8 @@ describe('flags', () => { it('does not accept non-boolean values', () => { const buffer: FeatureFlag[] = []; const maxSize = 1000; - insertToFlagBuffer(buffer, 'feat1', 1, maxSize); - insertToFlagBuffer(buffer, 'feat2', 'string', maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', 1, maxSize); + _INTERNAL_insertToFlagBuffer(buffer, 'feat2', 'string', maxSize); expect(buffer).toEqual([]); }); @@ -106,7 +106,7 @@ describe('flags', () => { { flag: 'feat2', result: true }, ]; - insertToFlagBuffer(buffer, 'feat1', true, 1); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, 1); expect(loggerSpy).toHaveBeenCalledWith( expect.stringContaining('[Feature Flags] insertToFlagBuffer called on a buffer larger than maxSize'), ); @@ -115,7 +115,7 @@ describe('flags', () => { { flag: 'feat2', result: true }, ]); - insertToFlagBuffer(buffer, 'feat1', true, -2); + _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, -2); expect(loggerSpy).toHaveBeenCalledWith( expect.stringContaining('[Feature Flags] insertToFlagBuffer called on a buffer larger than maxSize'), ); From a93b0e0237dc4a3758fa5d31e513ed79fba7b3e2 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:52:31 -0700 Subject: [PATCH 03/32] Delete old browser utils and integration and yarn fix --- .../featureFlags/featureFlagsIntegration.ts | 57 ------- .../src/integrations/featureFlags/index.ts | 1 - .../featureFlags/launchdarkly/integration.ts | 2 +- .../featureFlags/openfeature/integration.ts | 2 +- .../featureFlags/statsig/integration.ts | 2 +- .../featureFlags/unleash/integration.ts | 8 +- packages/browser/src/utils/featureFlags.ts | 156 ------------------ .../browser/test/utils/featureFlags.test.ts | 127 -------------- packages/core/src/index.ts | 7 +- 9 files changed, 13 insertions(+), 349 deletions(-) delete mode 100644 packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts delete mode 100644 packages/browser/src/integrations/featureFlags/index.ts delete mode 100644 packages/browser/src/utils/featureFlags.ts delete mode 100644 packages/browser/test/utils/featureFlags.test.ts diff --git a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts b/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts deleted file mode 100644 index 549c99af2c13..000000000000 --- a/packages/browser/src/integrations/featureFlags/featureFlagsIntegration.ts +++ /dev/null @@ -1,57 +0,0 @@ -import type { Client, Event, EventHint, Integration, IntegrationFn, Span } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; -import { - bufferSpanFeatureFlag, - copyFlagsFromScopeToEvent, - freezeSpanFeatureFlags, - insertFlagToScope, -} from '../../utils/featureFlags'; - -export interface FeatureFlagsIntegration extends Integration { - addFeatureFlag: (name: string, value: unknown) => void; -} - -/** - * 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. - * - * @example - * ``` - * import * as Sentry from '@sentry/browser'; - * import { type FeatureFlagsIntegration } from '@sentry/browser'; - * - * // Setup - * Sentry.init(..., integrations: [Sentry.featureFlagsIntegration()]) - * - * // Verify - * const flagsIntegration = Sentry.getClient()?.getIntegrationByName('FeatureFlags'); - * if (flagsIntegration) { - * flagsIntegration.addFeatureFlag('my-flag', true); - * } else { - * // check your setup - * } - * Sentry.captureException(Exception('broke')); // 'my-flag' should be captured to this Sentry event. - * ``` - */ -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/index.ts b/packages/browser/src/integrations/featureFlags/index.ts deleted file mode 100644 index 2106ee7accf0..000000000000 --- a/packages/browser/src/integrations/featureFlags/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { featureFlagsIntegration, type FeatureFlagsIntegration } from './featureFlagsIntegration'; diff --git a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts index 5890c92baf00..c68a8d889bda 100644 --- a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts +++ b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts @@ -1,10 +1,10 @@ import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; import { - defineIntegration, _INTERNAL_bufferSpanFeatureFlag, _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_freezeSpanFeatureFlags, _INTERNAL_insertFlagToScope, + defineIntegration, } from '@sentry/core'; 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 2f80202d6613..ffe5bbbbee72 100644 --- a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts +++ b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts @@ -15,11 +15,11 @@ */ import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; import { - defineIntegration, _INTERNAL_bufferSpanFeatureFlag, _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_freezeSpanFeatureFlags, _INTERNAL_insertFlagToScope, + defineIntegration, } from '@sentry/core'; import type { EvaluationDetails, HookContext, HookHints, JsonValue, OpenFeatureHook } from './types'; diff --git a/packages/browser/src/integrations/featureFlags/statsig/integration.ts b/packages/browser/src/integrations/featureFlags/statsig/integration.ts index e7ce439049db..b57d6befb186 100644 --- a/packages/browser/src/integrations/featureFlags/statsig/integration.ts +++ b/packages/browser/src/integrations/featureFlags/statsig/integration.ts @@ -1,10 +1,10 @@ import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; import { - defineIntegration, _INTERNAL_bufferSpanFeatureFlag, _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_freezeSpanFeatureFlags, _INTERNAL_insertFlagToScope, + defineIntegration, } from '@sentry/core'; 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 063569cf8310..e861d066ca1c 100644 --- a/packages/browser/src/integrations/featureFlags/unleash/integration.ts +++ b/packages/browser/src/integrations/featureFlags/unleash/integration.ts @@ -1,14 +1,14 @@ import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; -import { DEBUG_BUILD } from '../../../debug-build'; import { - defineIntegration, - fill, - logger, _INTERNAL_bufferSpanFeatureFlag, _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_freezeSpanFeatureFlags, _INTERNAL_insertFlagToScope, + defineIntegration, + fill, + logger, } from '@sentry/core'; +import { DEBUG_BUILD } from '../../../debug-build'; import type { UnleashClient, UnleashClientClass } from './types'; type UnleashIntegrationOptions = { diff --git a/packages/browser/src/utils/featureFlags.ts b/packages/browser/src/utils/featureFlags.ts deleted file mode 100644 index 49ee8ee14a48..000000000000 --- a/packages/browser/src/utils/featureFlags.ts +++ /dev/null @@ -1,156 +0,0 @@ -import type { Event, FeatureFlag, Span } from '@sentry/core'; -import { getActiveSpan, getCurrentScope, GLOBAL_OBJ, logger } from '@sentry/core'; -import { DEBUG_BUILD } from '../debug-build'; - -/** - * Ordered LRU cache for storing feature flags in the scope context. The name - * of each flag in the buffer is unique, and the output of getAll() is ordered - * from oldest to newest. - */ - -/** - * Max size of the LRU flag buffer stored in Sentry scope and event contexts. - */ -export const FLAG_BUFFER_SIZE = 100; - -/** - * Max number of flag evaluations to record per span. - */ -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 - */ -export function copyFlagsFromScopeToEvent(event: Event): Event { - const scope = getCurrentScope(); - const flagContext = scope.getScopeData().contexts.flags; - const flagBuffer = flagContext ? flagContext.values : []; - - if (!flagBuffer.length) { - return event; - } - - if (event.contexts === undefined) { - event.contexts = {}; - } - event.contexts.flags = { values: [...flagBuffer] }; - return event; -} - -/** - * 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) { - scopeContexts.flags = { values: [] }; - } - const flags = scopeContexts.flags.values as FeatureFlag[]; - insertToFlagBuffer(flags, name, value, maxSize); -} - -/** - * 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') { - return; - } - - if (flags.length > maxSize) { - DEBUG_BUILD && logger.error(`[Feature Flags] insertToFlagBuffer called on a buffer larger than maxSize=${maxSize}`); - return; - } - - // Check if the flag is already in the buffer - O(n) - const index = flags.findIndex(f => f.flag === name); - - if (index !== -1) { - // The flag was found, remove it from its current position - O(n) - flags.splice(index, 1); - } - - if (flags.length === maxSize) { - if (allowEviction) { - // If at capacity, pop the earliest flag - O(n) - flags.shift(); - } else { - return; - } - } - - // Push the flag to the end - O(1) - flags.push({ - flag: name, - 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) || []; - insertToFlagBuffer(flags, name, value, maxFlagsPerSpan, false); - 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/browser/test/utils/featureFlags.test.ts b/packages/browser/test/utils/featureFlags.test.ts deleted file mode 100644 index e60871832261..000000000000 --- a/packages/browser/test/utils/featureFlags.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import type { FeatureFlag } from '@sentry/core'; -import { getCurrentScope, logger } from '@sentry/core'; -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { insertFlagToScope, insertToFlagBuffer } from '../../src/utils/featureFlags'; - -describe('flags', () => { - describe('insertFlagToScope()', () => { - it('adds flags to the current scope context', () => { - const maxSize = 3; - insertFlagToScope('feat1', true, maxSize); - insertFlagToScope('feat2', true, maxSize); - insertFlagToScope('feat3', true, maxSize); - insertFlagToScope('feat4', true, maxSize); - - const scope = getCurrentScope(); - expect(scope.getScopeData().contexts.flags?.values).toEqual([ - { flag: 'feat2', result: true }, - { flag: 'feat3', result: true }, - { flag: 'feat4', result: true }, - ]); - }); - }); - - describe('insertToFlagBuffer()', () => { - const loggerSpy = vi.spyOn(logger, 'error'); - - afterEach(() => { - loggerSpy.mockClear(); - }); - - it('maintains ordering and evicts the oldest entry', () => { - const buffer: FeatureFlag[] = []; - const maxSize = 3; - insertToFlagBuffer(buffer, 'feat1', true, maxSize); - insertToFlagBuffer(buffer, 'feat2', true, maxSize); - insertToFlagBuffer(buffer, 'feat3', true, maxSize); - insertToFlagBuffer(buffer, 'feat4', true, maxSize); - - expect(buffer).toEqual([ - { flag: 'feat2', result: true }, - { flag: 'feat3', result: true }, - { flag: 'feat4', result: true }, - ]); - }); - - it('does not duplicate same-name flags and updates order and values', () => { - const buffer: FeatureFlag[] = []; - const maxSize = 3; - insertToFlagBuffer(buffer, 'feat1', true, maxSize); - insertToFlagBuffer(buffer, 'feat2', true, maxSize); - insertToFlagBuffer(buffer, 'feat3', true, maxSize); - insertToFlagBuffer(buffer, 'feat3', false, maxSize); - insertToFlagBuffer(buffer, 'feat1', false, maxSize); - - expect(buffer).toEqual([ - { flag: 'feat2', result: true }, - { flag: 'feat3', result: false }, - { flag: 'feat1', result: false }, - ]); - }); - - 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; - insertToFlagBuffer(buffer, 'feat1', true, maxSize); - insertToFlagBuffer(buffer, 'feat2', true, maxSize); - - expect(buffer).toEqual([ - { flag: 'feat1', result: true }, - { flag: 'feat2', result: true }, - ]); - }); - - it('does not accept non-boolean values', () => { - const buffer: FeatureFlag[] = []; - const maxSize = 1000; - insertToFlagBuffer(buffer, 'feat1', 1, maxSize); - insertToFlagBuffer(buffer, 'feat2', 'string', maxSize); - - expect(buffer).toEqual([]); - }); - - it('logs error and is a no-op when buffer is larger than maxSize', () => { - const buffer: FeatureFlag[] = [ - { flag: 'feat1', result: true }, - { flag: 'feat2', result: true }, - ]; - - insertToFlagBuffer(buffer, 'feat1', true, 1); - expect(loggerSpy).toHaveBeenCalledWith( - expect.stringContaining('[Feature Flags] insertToFlagBuffer called on a buffer larger than maxSize'), - ); - expect(buffer).toEqual([ - { flag: 'feat1', result: true }, - { flag: 'feat2', result: true }, - ]); - - insertToFlagBuffer(buffer, 'feat1', true, -2); - expect(loggerSpy).toHaveBeenCalledWith( - expect.stringContaining('[Feature Flags] insertToFlagBuffer called on a buffer larger than maxSize'), - ); - expect(buffer).toEqual([ - { flag: 'feat1', result: true }, - { flag: 'feat2', result: true }, - ]); - }); - }); -}); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f2027370a235..3764fe0f30b0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -125,7 +125,12 @@ export { consoleLoggingIntegration } from './logs/console-integration'; export type { FeatureFlag } from './featureFlags'; // TODO: ^export from utils? -export { _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_insertFlagToScope, _INTERNAL_bufferSpanFeatureFlag, _INTERNAL_freezeSpanFeatureFlags } from './utils/featureFlags'; +export { + _INTERNAL_copyFlagsFromScopeToEvent, + _INTERNAL_insertFlagToScope, + _INTERNAL_bufferSpanFeatureFlag, + _INTERNAL_freezeSpanFeatureFlags, +} from './utils/featureFlags'; export { applyAggregateErrorsToEvent } from './utils-hoist/aggregate-errors'; export { getBreadcrumbLogLevelFromHttpStatusCode } from './utils-hoist/breadcrumb-log-level'; From 4366e5a7831024f2c49be71303a836fc5afc8846 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 10 Jun 2025 11:53:22 -0700 Subject: [PATCH 04/32] Keep browser in docstr example --- .../src/integrations/featureFlags/featureFlagsIntegration.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts b/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts index ad806200adc5..21298989d24c 100644 --- a/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts +++ b/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts @@ -22,8 +22,8 @@ export interface FeatureFlagsIntegration extends Integration { * * @example * ``` - * import * as Sentry from '@sentry/*'; //TODO: - * import { type FeatureFlagsIntegration } from '@sentry/*'; + * import * as Sentry from '@sentry/browser'; + * import { type FeatureFlagsIntegration } from '@sentry/browser'; * * // Setup * Sentry.init(..., integrations: [Sentry.featureFlagsIntegration()]) From 868cd71cd987ebe9e7bf91e05870dab26d09a970 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 10 Jun 2025 12:24:35 -0700 Subject: [PATCH 05/32] Export from same pkgs as zodErrorsIntegration, except remix, solidstart, sveltekit (has *) --- packages/astro/src/index.server.ts | 2 ++ packages/aws-serverless/src/index.ts | 2 ++ packages/bun/src/index.ts | 2 ++ packages/cloudflare/src/index.ts | 2 ++ packages/deno/src/index.ts | 2 ++ packages/google-cloud-serverless/src/index.ts | 2 ++ packages/node/src/index.ts | 2 ++ packages/remix/src/cloudflare/index.ts | 2 ++ packages/sveltekit/src/worker/index.ts | 2 ++ packages/vercel-edge/src/index.ts | 2 ++ 10 files changed, 20 insertions(+) diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 29105cfb4b18..e828c46af051 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -134,6 +134,8 @@ export { logger, consoleLoggingIntegration, wrapMcpServerWithSentry, + featureFlagsIntegration, + type FeatureFlagsIntegration, } from '@sentry/node'; export { init } from './server/sdk'; diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index 942951c165da..27ff9e3f1b9c 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -120,6 +120,8 @@ export { logger, consoleLoggingIntegration, wrapMcpServerWithSentry, + featureFlagsIntegration, + type FeatureFlagsIntegration, } from '@sentry/node'; export { diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 70104de6d7c3..14a44e2d38fc 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -15,6 +15,7 @@ export type { Stacktrace, Thread, User, + FeatureFlagsIntegration, } from '@sentry/core'; export { @@ -139,6 +140,7 @@ export { consoleLoggingIntegration, createSentryWinstonTransport, wrapMcpServerWithSentry, + featureFlagsIntegration, } from '@sentry/node'; export { diff --git a/packages/cloudflare/src/index.ts b/packages/cloudflare/src/index.ts index 6754ffd04f7c..1a366aeb5dd0 100644 --- a/packages/cloudflare/src/index.ts +++ b/packages/cloudflare/src/index.ts @@ -8,6 +8,7 @@ export type { EventHint, ErrorEvent, Exception, + FeatureFlagsIntegration, Session, SeverityLevel, Span, @@ -91,6 +92,7 @@ export { updateSpanName, wrapMcpServerWithSentry, consoleLoggingIntegration, + featureFlagsIntegration, } from '@sentry/core'; export * as logger from './logs/exports'; diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index f388de7cb5ee..12bcedc35270 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -8,6 +8,7 @@ export type { EventHint, ErrorEvent, Exception, + FeatureFlagsIntegration, Session, SeverityLevel, Span, @@ -87,6 +88,7 @@ export { spanToBaggageHeader, updateSpanName, wrapMcpServerWithSentry, + featureFlagsIntegration, } from '@sentry/core'; export { DenoClient } from './client'; diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index f5d593312743..ff17030ed4a5 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -120,6 +120,8 @@ export { logger, consoleLoggingIntegration, wrapMcpServerWithSentry, + featureFlagsIntegration, + type FeatureFlagsIntegration, } from '@sentry/node'; export { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 5a933002bc23..5992e7f27ea9 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -138,6 +138,7 @@ export { consoleLoggingIntegration, consoleIntegration, wrapMcpServerWithSentry, + featureFlagsIntegration, } from '@sentry/core'; export type { @@ -157,6 +158,7 @@ export type { Thread, User, Span, + FeatureFlagsIntegration, } from '@sentry/core'; export { logger }; diff --git a/packages/remix/src/cloudflare/index.ts b/packages/remix/src/cloudflare/index.ts index 3d3d17e1da27..46c443cac39f 100644 --- a/packages/remix/src/cloudflare/index.ts +++ b/packages/remix/src/cloudflare/index.ts @@ -32,6 +32,7 @@ export type { EventHint, ErrorEvent, Exception, + FeatureFlagsIntegration, Session, SeverityLevel, Span, @@ -109,4 +110,5 @@ export { spanToTraceHeader, spanToBaggageHeader, updateSpanName, + featureFlagsIntegration, } from '@sentry/core'; diff --git a/packages/sveltekit/src/worker/index.ts b/packages/sveltekit/src/worker/index.ts index 3614922072ec..2dde8c61a1dc 100644 --- a/packages/sveltekit/src/worker/index.ts +++ b/packages/sveltekit/src/worker/index.ts @@ -82,6 +82,8 @@ export { supabaseIntegration, instrumentSupabaseClient, zodErrorsIntegration, + featureFlagsIntegration, + type FeatureFlagsIntegration, } from '@sentry/cloudflare'; /** diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index ff1231c8a1f8..303d40144ec3 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -8,6 +8,7 @@ export type { EventHint, ErrorEvent, Exception, + FeatureFlagsIntegration, Session, SeverityLevel, Span, @@ -90,6 +91,7 @@ export { spanToBaggageHeader, wrapMcpServerWithSentry, consoleLoggingIntegration, + featureFlagsIntegration, } from '@sentry/core'; export { VercelEdgeClient } from './client'; From 16113300849ca673bcc9d4732d223e0cec88047b Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:46:13 -0700 Subject: [PATCH 06/32] Add on error node tests. TODO export/import buffer sizes as _INTERNAL --- .../integrations/featureFlags/constants.ts | 2 +- .../onError/basic/scenario.ts | 20 ++++++++++ .../onError/basic/test.ts | 32 ++++++++++++++++ .../onError/withScope/scenario.ts | 22 +++++++++++ .../onError/withScope/test.ts | 38 +++++++++++++++++++ .../onSpan/scenario.ts | 28 ++++++++++++++ .../featureFlagsIntegration/onSpan/test.ts | 34 +++++++++++++++++ 7 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts create mode 100644 dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts create mode 100644 dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/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 ba3c35a08241..712b7270b52c 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts @@ -1,3 +1,3 @@ -// Corresponds to constants in featureFlags.ts, in browser utils. +// Corresponds to constants in featureFlags.ts, in @sentry/core utils. export const FLAG_BUFFER_SIZE = 100; export const MAX_FLAGS_PER_SPAN = 10; diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts new file mode 100644 index 000000000000..67fd0c3ebfdd --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts @@ -0,0 +1,20 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +const FLAG_BUFFER_SIZE = 100; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + transport: loggingTransport, + integrations: [Sentry.featureFlagsIntegration()], +}); + +const flagsIntegration = Sentry.getClient()?.getIntegrationByName('FeatureFlags'); +for (let i = 1; i <= FLAG_BUFFER_SIZE; i++) { + flagsIntegration?.addFeatureFlag(`feat${i}`, false); +} +flagsIntegration?.addFeatureFlag(`feat${FLAG_BUFFER_SIZE + 1}`, true); // eviction +flagsIntegration?.addFeatureFlag('feat3', true); // update + +throw new Error('Test error'); diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts new file mode 100644 index 000000000000..6b003bdd7a6e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts @@ -0,0 +1,32 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../../utils/runner'; + +const FLAG_BUFFER_SIZE = 100; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('Flags captured on error with eviction, update, and no async tasks', async () => { + // Based on scenario.ts. + const expectedFlags = [{ flag: 'feat2', result: false }]; + for (let i = 4; i <= FLAG_BUFFER_SIZE; i++) { + expectedFlags.push({ flag: `feat${i}`, result: false }); + } + expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: true }); + expectedFlags.push({ flag: 'feat3', result: true }); + + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Test error' }] }, + contexts: { + flags: { + values: expectedFlags, + }, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts new file mode 100644 index 000000000000..b6537011a87f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts @@ -0,0 +1,22 @@ +import type { Scope } from '@sentry/node'; +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + transport: loggingTransport, + integrations: [Sentry.featureFlagsIntegration()], +}); + +const flagsIntegration = Sentry.getClient()?.getIntegrationByName('FeatureFlags'); +flagsIntegration?.addFeatureFlag('shared', true); + +Sentry.withScope((_scope: Scope) => { + flagsIntegration?.addFeatureFlag('forked', true); + flagsIntegration?.addFeatureFlag('shared', false); + Sentry.captureException(new Error('Error in forked scope')); +}); + +flagsIntegration?.addFeatureFlag('main', true); +throw new Error('Error in main scope'); diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts new file mode 100644 index 000000000000..947b299923e7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts @@ -0,0 +1,38 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('Flags captured on error are isolated by current scope', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Error in forked scope' }] }, + contexts: { + flags: { + values: [ + { flag: 'forked', result: true }, + { flag: 'shared', result: false }, + ], + }, + }, + }, + }) + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Error in main scope' }] }, + contexts: { + flags: { + values: [ + { flag: 'shared', result: true }, + { flag: 'main', result: true }, + ], + }, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts new file mode 100644 index 000000000000..de2e14c93b68 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts @@ -0,0 +1,28 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +const MAX_FLAGS_PER_SPAN = 10; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + // // disable attaching headers to /test/* endpoints + // tracePropagationTargets: [/^(?!.*test).*$/], + tracesSampleRate: 1.0, + transport: loggingTransport, + integrations: [Sentry.featureFlagsIntegration()], +}); + +const flagsIntegration = Sentry.getClient()?.getIntegrationByName('FeatureFlags'); + +Sentry.startSpan({ name: 'test-root-span' }, () => { + Sentry.startSpan({ name: 'test-span' }, () => { + Sentry.startSpan({ name: 'test-nested-span' }, () => { + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + flagsIntegration?.addFeatureFlag(`feat${i}`, false); + } + flagsIntegration?.addFeatureFlag(`feat${MAX_FLAGS_PER_SPAN + 1}`, true); // dropped flag + flagsIntegration?.addFeatureFlag('feat3', true); // update + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts new file mode 100644 index 000000000000..6828caa83d2e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts @@ -0,0 +1,34 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +const MAX_FLAGS_PER_SPAN = 10; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('Flags captured on span attributes with max limit', async () => { + // Based on scenario.ts. + const expectedFlags: Record = {}; + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedFlags[`flag.evaluation.feat${i}`] = i === 3; + } + + await createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + spans: [ + expect.objectContaining({ + description: 'test-span', + data: expect.objectContaining({}), + }), + expect.objectContaining({ + description: 'test-nested-span', + data: expect.objectContaining(expectedFlags), + }), + ], + }, + }) + .start() + .completed(); +}); From f56cc3ec87fb47c79c1e4e572b9956d0815a2b48 Mon Sep 17 00:00:00 2001 From: Tee Ming Date: Wed, 11 Jun 2025 16:26:14 +0800 Subject: [PATCH 07/32] fix(sveltekit): Add import attribute for node exports (#16528) This patch changes the `node` export in the `package.json` file to include an `import` version (similar to the other exports) to fix https://github.com/sveltejs/kit/issues/13869 . In SvelteKit, we're now bundling dependencies on the server that specify SvelteKit as a dependency or peerDependency. This has caused an issue where builds with `@sentry/sveltekit` were being bundled incorrectly. Adding the `import` attribute fixes this so that Vite resolves to the ESM build of Sentry. --------- Co-authored-by: Lukas Stracke --- .../test-applications/sveltekit-2-svelte-5/package.json | 2 +- .../e2e-tests/test-applications/sveltekit-2/package.json | 2 +- .../sveltekit-cloudflare-pages/package.json | 2 +- packages/sveltekit/package.json | 8 ++++++-- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json index cbf60795d4a0..784bd0a6a341 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2-svelte-5/package.json @@ -23,7 +23,7 @@ "@sentry-internal/test-utils": "link:../../../test-utils", "@sentry/core": "latest || *", "@sveltejs/adapter-auto": "^3.0.0", - "@sveltejs/kit": "2.21.2", + "@sveltejs/kit": "^2.21.3", "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^5.0.0-next.115", "svelte-check": "^3.6.0", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json index 54599c04d9d1..84ac60c1fb14 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/package.json @@ -23,7 +23,7 @@ "@sentry/core": "latest || *", "@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-node": "^2.0.0", - "@sveltejs/kit": "2.21.2", + "@sveltejs/kit": "^2.21.3", "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^4.2.8", "svelte-check": "^3.6.0", diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json index 87d3c20c5c7a..230f6127d55d 100644 --- a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json @@ -20,7 +20,7 @@ "devDependencies": { "@playwright/test": "^1.45.3", "@sveltejs/adapter-cloudflare": "^5.0.3", - "@sveltejs/kit": "2.21.2", + "@sveltejs/kit": "^2.21.3", "@sveltejs/vite-plugin-svelte": "^5.0.3", "svelte": "^5.20.2", "svelte-check": "^4.1.4", diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 9204691b5991..0403c2d70b90 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -28,7 +28,10 @@ "import": "./build/esm/index.client.js", "require": "./build/cjs/index.client.js" }, - "node": "./build/cjs/index.server.js" + "node": { + "import": "./build/esm/index.server.js", + "require": "./build/cjs/index.server.js" + } } }, "publishConfig": { @@ -83,5 +86,6 @@ }, "volta": { "extends": "../../package.json" - } + }, + "sideEffects": false } From 70a57e600887a227628e143f2d572d6af4651e91 Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Wed, 11 Jun 2025 10:35:41 +0200 Subject: [PATCH 08/32] chore: Add external contributor to CHANGELOG.md (#16543) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #16528 Co-authored-by: Lms24 <8420481+Lms24@users.noreply.github.com> --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe5144bbb94b..936473d764b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +Work in this release was contributed by @eltigerchino. Thank you for your contribution! + ## 9.28.0 ### Important Changes From 0b0942ff19b22badfc228f165e6dd2e6b20200f4 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 11 Jun 2025 10:42:28 +0200 Subject: [PATCH 09/32] meta(changelog): Update changelog for 9.28.1 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 936473d764b9..40e6d2189e3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 9.28.1 + +- feat(deps): Bump @sentry/cli from 2.45.0 to 2.46.0 ([#16516](https://github.com/getsentry/sentry-javascript/pull/16516)) +- fix(nextjs): Avoid tracing calls to symbolication server on dev ([#16533](https://github.com/getsentry/sentry-javascript/pull/16533)) +- fix(sveltekit): Add import attribute for node exports ([#16528](https://github.com/getsentry/sentry-javascript/pull/16528)) + Work in this release was contributed by @eltigerchino. Thank you for your contribution! ## 9.28.0 From 111db4a8aea212f14ac90c7f02f01112a1e78283 Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Wed, 11 Jun 2025 08:53:45 +0000 Subject: [PATCH 10/32] release: 9.28.1 --- .../browser-integration-tests/package.json | 4 ++-- .../bundle-analyzer-scenarios/package.json | 2 +- dev-packages/clear-cache-gh-action/package.json | 2 +- dev-packages/e2e-tests/package.json | 2 +- .../external-contributor-gh-action/package.json | 2 +- dev-packages/node-integration-tests/package.json | 8 ++++---- dev-packages/opentelemetry-v2-tests/package.json | 2 +- dev-packages/rollup-utils/package.json | 2 +- dev-packages/size-limit-gh-action/package.json | 2 +- dev-packages/test-utils/package.json | 4 ++-- lerna.json | 2 +- packages/angular/package.json | 6 +++--- packages/astro/package.json | 8 ++++---- packages/aws-serverless/package.json | 6 +++--- packages/browser-utils/package.json | 4 ++-- packages/browser/package.json | 14 +++++++------- packages/bun/package.json | 8 ++++---- packages/cloudflare/package.json | 4 ++-- packages/core/package.json | 2 +- packages/deno/package.json | 4 ++-- packages/ember/package.json | 6 +++--- packages/eslint-config-sdk/package.json | 6 +++--- packages/eslint-plugin-sdk/package.json | 2 +- packages/feedback/package.json | 4 ++-- packages/gatsby/package.json | 6 +++--- packages/google-cloud-serverless/package.json | 6 +++--- packages/integration-shims/package.json | 4 ++-- packages/nestjs/package.json | 6 +++--- packages/nextjs/package.json | 14 +++++++------- packages/node/package.json | 6 +++--- packages/nuxt/package.json | 12 ++++++------ packages/opentelemetry/package.json | 4 ++-- packages/profiling-node/package.json | 6 +++--- packages/react-router/package.json | 10 +++++----- packages/react/package.json | 6 +++--- packages/remix/package.json | 10 +++++----- packages/replay-canvas/package.json | 6 +++--- packages/replay-internal/package.json | 8 ++++---- packages/replay-worker/package.json | 2 +- packages/solid/package.json | 6 +++--- packages/solidstart/package.json | 10 +++++----- packages/svelte/package.json | 6 +++--- packages/sveltekit/package.json | 12 ++++++------ packages/tanstackstart-react/package.json | 12 ++++++------ packages/tanstackstart/package.json | 2 +- packages/types/package.json | 4 ++-- packages/typescript/package.json | 2 +- packages/vercel-edge/package.json | 6 +++--- packages/vue/package.json | 6 +++--- packages/wasm/package.json | 6 +++--- 50 files changed, 142 insertions(+), 142 deletions(-) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 7fac52f9974e..d170cdc8b76c 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-integration-tests", - "version": "9.28.0", + "version": "9.28.1", "main": "index.js", "license": "MIT", "engines": { @@ -42,7 +42,7 @@ "@babel/preset-typescript": "^7.16.7", "@playwright/test": "~1.50.0", "@sentry-internal/rrweb": "2.34.0", - "@sentry/browser": "9.28.0", + "@sentry/browser": "9.28.1", "@supabase/supabase-js": "2.49.3", "axios": "1.8.2", "babel-loader": "^8.2.2", diff --git a/dev-packages/bundle-analyzer-scenarios/package.json b/dev-packages/bundle-analyzer-scenarios/package.json index f972d77cbf87..580b10cb7359 100644 --- a/dev-packages/bundle-analyzer-scenarios/package.json +++ b/dev-packages/bundle-analyzer-scenarios/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/bundle-analyzer-scenarios", - "version": "9.28.0", + "version": "9.28.1", "description": "Scenarios to test bundle analysis with", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/dev-packages/bundle-analyzer-scenarios", diff --git a/dev-packages/clear-cache-gh-action/package.json b/dev-packages/clear-cache-gh-action/package.json index 7a066204f66f..9dd02dd0eb6f 100644 --- a/dev-packages/clear-cache-gh-action/package.json +++ b/dev-packages/clear-cache-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/clear-cache-gh-action", "description": "An internal Github Action to clear GitHub caches.", - "version": "9.28.0", + "version": "9.28.1", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 5779ff40dd3b..841453da3f5b 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/e2e-tests", - "version": "9.28.0", + "version": "9.28.1", "license": "MIT", "private": true, "scripts": { diff --git a/dev-packages/external-contributor-gh-action/package.json b/dev-packages/external-contributor-gh-action/package.json index 1945ee0f8aec..56a45ea1f51a 100644 --- a/dev-packages/external-contributor-gh-action/package.json +++ b/dev-packages/external-contributor-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/external-contributor-gh-action", "description": "An internal Github Action to add external contributors to the CHANGELOG.md file.", - "version": "9.28.0", + "version": "9.28.1", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 7cc60ff01193..18595a8e22c4 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/node-integration-tests", - "version": "9.28.0", + "version": "9.28.1", "license": "MIT", "engines": { "node": ">=18" @@ -30,9 +30,9 @@ "@nestjs/common": "11.0.16", "@nestjs/core": "10.4.6", "@nestjs/platform-express": "10.4.6", - "@sentry/aws-serverless": "9.28.0", - "@sentry/core": "9.28.0", - "@sentry/node": "9.28.0", + "@sentry/aws-serverless": "9.28.1", + "@sentry/core": "9.28.1", + "@sentry/node": "9.28.1", "@types/mongodb": "^3.6.20", "@types/mysql": "^2.15.21", "@types/pg": "^8.6.5", diff --git a/dev-packages/opentelemetry-v2-tests/package.json b/dev-packages/opentelemetry-v2-tests/package.json index 22889df5d5c3..e30b856aaee3 100644 --- a/dev-packages/opentelemetry-v2-tests/package.json +++ b/dev-packages/opentelemetry-v2-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/opentelemetry-v2-tests", - "version": "9.28.0", + "version": "9.28.1", "private": true, "description": "Tests for @sentry/opentelemetry with OpenTelemetry v2", "engines": { diff --git a/dev-packages/rollup-utils/package.json b/dev-packages/rollup-utils/package.json index 52e5d757a771..c469e0983f4e 100644 --- a/dev-packages/rollup-utils/package.json +++ b/dev-packages/rollup-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/rollup-utils", - "version": "9.28.0", + "version": "9.28.1", "description": "Rollup utilities used at Sentry for the Sentry JavaScript SDK", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/rollup-utils", diff --git a/dev-packages/size-limit-gh-action/package.json b/dev-packages/size-limit-gh-action/package.json index 95f3788636bc..aeef6e7f48c0 100644 --- a/dev-packages/size-limit-gh-action/package.json +++ b/dev-packages/size-limit-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/size-limit-gh-action", "description": "An internal Github Action to compare the current size of a PR against the one on develop.", - "version": "9.28.0", + "version": "9.28.1", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json index 29e9e0f47832..ea5f1cb7fd63 100644 --- a/dev-packages/test-utils/package.json +++ b/dev-packages/test-utils/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "9.28.0", + "version": "9.28.1", "name": "@sentry-internal/test-utils", "author": "Sentry", "license": "MIT", @@ -45,7 +45,7 @@ }, "devDependencies": { "@playwright/test": "~1.50.0", - "@sentry/core": "9.28.0" + "@sentry/core": "9.28.1" }, "volta": { "extends": "../../package.json" diff --git a/lerna.json b/lerna.json index 44bf3c1e361b..9593d8699a43 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "9.28.0", + "version": "9.28.1", "npmClient": "yarn" } diff --git a/packages/angular/package.json b/packages/angular/package.json index d62c08d0754c..91a4f4fed00a 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/angular", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for Angular", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular", @@ -21,8 +21,8 @@ "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { - "@sentry/browser": "9.28.0", - "@sentry/core": "9.28.0", + "@sentry/browser": "9.28.1", + "@sentry/core": "9.28.1", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/astro/package.json b/packages/astro/package.json index 65bc4a09d95f..481e8a2c011d 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/astro", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for Astro", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/astro", @@ -56,9 +56,9 @@ "astro": ">=3.x || >=4.0.0-beta || >=5.x" }, "dependencies": { - "@sentry/browser": "9.28.0", - "@sentry/core": "9.28.0", - "@sentry/node": "9.28.0", + "@sentry/browser": "9.28.1", + "@sentry/core": "9.28.1", + "@sentry/node": "9.28.1", "@sentry/vite-plugin": "^2.22.6" }, "devDependencies": { diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index 9dee9fa64cf5..4650b1a10465 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/aws-serverless", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for AWS Lambda and AWS Serverless Environments", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/serverless", @@ -68,8 +68,8 @@ "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/instrumentation-aws-lambda": "0.50.3", "@opentelemetry/instrumentation-aws-sdk": "0.49.1", - "@sentry/core": "9.28.0", - "@sentry/node": "9.28.0", + "@sentry/core": "9.28.1", + "@sentry/node": "9.28.1", "@types/aws-lambda": "^8.10.62" }, "devDependencies": { diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index 5f8f9d1c4784..03f47d8905eb 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-utils", - "version": "9.28.0", + "version": "9.28.1", "description": "Browser Utilities for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser-utils", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.28.0" + "@sentry/core": "9.28.1" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/browser/package.json b/packages/browser/package.json index 9f579eef17bf..cacc29415ad3 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/browser", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for browsers", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser", @@ -39,14 +39,14 @@ "access": "public" }, "dependencies": { - "@sentry-internal/browser-utils": "9.28.0", - "@sentry-internal/feedback": "9.28.0", - "@sentry-internal/replay": "9.28.0", - "@sentry-internal/replay-canvas": "9.28.0", - "@sentry/core": "9.28.0" + "@sentry-internal/browser-utils": "9.28.1", + "@sentry-internal/feedback": "9.28.1", + "@sentry-internal/replay": "9.28.1", + "@sentry-internal/replay-canvas": "9.28.1", + "@sentry/core": "9.28.1" }, "devDependencies": { - "@sentry-internal/integration-shims": "9.28.0", + "@sentry-internal/integration-shims": "9.28.1", "fake-indexeddb": "^4.0.1" }, "scripts": { diff --git a/packages/bun/package.json b/packages/bun/package.json index 278dceb591ba..18a854f61da2 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/bun", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for bun", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/bun", @@ -39,9 +39,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.28.0", - "@sentry/node": "9.28.0", - "@sentry/opentelemetry": "9.28.0" + "@sentry/core": "9.28.1", + "@sentry/node": "9.28.1", + "@sentry/opentelemetry": "9.28.1" }, "devDependencies": { "bun-types": "^1.2.9" diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index efd99d148d75..78671f1a4155 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cloudflare", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for Cloudflare Workers and Pages", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/cloudflare", @@ -49,7 +49,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.28.0" + "@sentry/core": "9.28.1" }, "peerDependencies": { "@cloudflare/workers-types": "^4.x" diff --git a/packages/core/package.json b/packages/core/package.json index 7ff3b1bd11ff..0850ba0fb8f7 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/core", - "version": "9.28.0", + "version": "9.28.1", "description": "Base implementation for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/core", diff --git a/packages/deno/package.json b/packages/deno/package.json index 8391b99f3ba1..203131dda6ff 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/deno", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for Deno", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/deno", @@ -24,7 +24,7 @@ "/build" ], "dependencies": { - "@sentry/core": "9.28.0" + "@sentry/core": "9.28.1" }, "scripts": { "deno-types": "node ./scripts/download-deno-types.mjs", diff --git a/packages/ember/package.json b/packages/ember/package.json index ab36fb287cde..b48d6c0cdee1 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/ember", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for Ember.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/ember", @@ -32,8 +32,8 @@ "dependencies": { "@babel/core": "^7.24.4", "@embroider/macros": "^1.16.0", - "@sentry/browser": "9.28.0", - "@sentry/core": "9.28.0", + "@sentry/browser": "9.28.1", + "@sentry/core": "9.28.1", "ember-auto-import": "^2.7.2", "ember-cli-babel": "^8.2.0", "ember-cli-htmlbars": "^6.1.1", diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index c5ac5c927657..62954ddb27e6 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-config-sdk", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK eslint config", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-config-sdk", @@ -22,8 +22,8 @@ "access": "public" }, "dependencies": { - "@sentry-internal/eslint-plugin-sdk": "9.28.0", - "@sentry-internal/typescript": "9.28.0", + "@sentry-internal/eslint-plugin-sdk": "9.28.1", + "@sentry-internal/typescript": "9.28.1", "@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/parser": "^5.48.0", "eslint-config-prettier": "^6.11.0", diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json index 0c9cb55ec002..7a92a90b467f 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-plugin-sdk", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK eslint plugin", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-plugin-sdk", diff --git a/packages/feedback/package.json b/packages/feedback/package.json index 2472c223bffc..a60780ed2d0d 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/feedback", - "version": "9.28.0", + "version": "9.28.1", "description": "Sentry SDK integration for user feedback", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.28.0" + "@sentry/core": "9.28.1" }, "devDependencies": { "preact": "^10.19.4" diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 6c59d09429b0..d1373cd38b8b 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/gatsby", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for Gatsby.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/gatsby", @@ -45,8 +45,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.28.0", - "@sentry/react": "9.28.0", + "@sentry/core": "9.28.1", + "@sentry/react": "9.28.1", "@sentry/webpack-plugin": "3.5.0" }, "peerDependencies": { diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index dd1eaee56fff..8c30d92acfd8 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/google-cloud-serverless", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for Google Cloud Functions", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/google-cloud-serverless", @@ -48,8 +48,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.28.0", - "@sentry/node": "9.28.0", + "@sentry/core": "9.28.1", + "@sentry/node": "9.28.1", "@types/express": "^4.17.14" }, "devDependencies": { diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json index ff59a6697a94..d4d75088f21f 100644 --- a/packages/integration-shims/package.json +++ b/packages/integration-shims/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/integration-shims", - "version": "9.28.0", + "version": "9.28.1", "description": "Shims for integrations in Sentry SDK.", "main": "build/cjs/index.js", "module": "build/esm/index.js", @@ -56,7 +56,7 @@ "url": "https://github.com/getsentry/sentry-javascript/issues" }, "dependencies": { - "@sentry/core": "9.28.0" + "@sentry/core": "9.28.1" }, "engines": { "node": ">=18" diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index f00ed075136a..bd3412ebe18e 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nestjs", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for NestJS", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nestjs", @@ -49,8 +49,8 @@ "@opentelemetry/instrumentation": "0.57.2", "@opentelemetry/instrumentation-nestjs-core": "0.44.1", "@opentelemetry/semantic-conventions": "^1.34.0", - "@sentry/core": "9.28.0", - "@sentry/node": "9.28.0" + "@sentry/core": "9.28.1", + "@sentry/node": "9.28.1" }, "devDependencies": { "@nestjs/common": "^10.0.0", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index c2144126643e..3c9b7909f5c2 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nextjs", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for Next.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nextjs", @@ -79,12 +79,12 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@rollup/plugin-commonjs": "28.0.1", - "@sentry-internal/browser-utils": "9.28.0", - "@sentry/core": "9.28.0", - "@sentry/node": "9.28.0", - "@sentry/opentelemetry": "9.28.0", - "@sentry/react": "9.28.0", - "@sentry/vercel-edge": "9.28.0", + "@sentry-internal/browser-utils": "9.28.1", + "@sentry/core": "9.28.1", + "@sentry/node": "9.28.1", + "@sentry/opentelemetry": "9.28.1", + "@sentry/react": "9.28.1", + "@sentry/vercel-edge": "9.28.1", "@sentry/webpack-plugin": "3.5.0", "chalk": "3.0.0", "resolve": "1.22.8", diff --git a/packages/node/package.json b/packages/node/package.json index 39a4bda25b47..14889e197fa8 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node", - "version": "9.28.0", + "version": "9.28.1", "description": "Sentry Node SDK using OpenTelemetry for performance instrumentation", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node", @@ -95,8 +95,8 @@ "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", "@prisma/instrumentation": "6.8.2", - "@sentry/core": "9.28.0", - "@sentry/opentelemetry": "9.28.0", + "@sentry/core": "9.28.1", + "@sentry/opentelemetry": "9.28.1", "import-in-the-middle": "^1.13.1", "minimatch": "^9.0.0" }, diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index eef65990dbeb..17f2d90f0ca4 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nuxt", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for Nuxt (EXPERIMENTAL)", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nuxt", @@ -43,13 +43,13 @@ }, "dependencies": { "@nuxt/kit": "^3.13.2", - "@sentry/browser": "9.28.0", - "@sentry/core": "9.28.0", - "@sentry/node": "9.28.0", - "@sentry/opentelemetry": "9.28.0", + "@sentry/browser": "9.28.1", + "@sentry/core": "9.28.1", + "@sentry/node": "9.28.1", + "@sentry/opentelemetry": "9.28.1", "@sentry/rollup-plugin": "3.4.0", "@sentry/vite-plugin": "3.2.4", - "@sentry/vue": "9.28.0" + "@sentry/vue": "9.28.1" }, "devDependencies": { "@nuxt/module-builder": "^0.8.4", diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index 934e07c84145..b4d8df9d9f38 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/opentelemetry", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry utilities for OpenTelemetry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/opentelemetry", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.28.0" + "@sentry/core": "9.28.1" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index f51da70d6de5..16d89366f5a6 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/profiling-node", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for Node.js Profiling", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/profiling-node", @@ -63,8 +63,8 @@ }, "dependencies": { "@sentry-internal/node-cpu-profiler": "^2.2.0", - "@sentry/core": "9.28.0", - "@sentry/node": "9.28.0" + "@sentry/core": "9.28.1", + "@sentry/node": "9.28.1" }, "devDependencies": { "@types/node": "^18.19.1" diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 01d7623bb6ce..608e508ae530 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react-router", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for React Router (Framework)", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react-router", @@ -38,11 +38,11 @@ "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "0.57.2", "@opentelemetry/semantic-conventions": "^1.34.0", - "@sentry/browser": "9.28.0", + "@sentry/browser": "9.28.1", "@sentry/cli": "^2.46.0", - "@sentry/core": "9.28.0", - "@sentry/node": "9.28.0", - "@sentry/react": "9.28.0", + "@sentry/core": "9.28.1", + "@sentry/node": "9.28.1", + "@sentry/react": "9.28.1", "@sentry/vite-plugin": "^3.2.4", "glob": "11.0.1" }, diff --git a/packages/react/package.json b/packages/react/package.json index b71b76513725..7a9feadc4510 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for React.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.28.0", - "@sentry/core": "9.28.0", + "@sentry/browser": "9.28.1", + "@sentry/core": "9.28.1", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { diff --git a/packages/remix/package.json b/packages/remix/package.json index a48bf998e104..f676ef676f4b 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/remix", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for Remix", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/remix", @@ -69,10 +69,10 @@ "@opentelemetry/semantic-conventions": "^1.34.0", "@remix-run/router": "1.x", "@sentry/cli": "^2.46.0", - "@sentry/core": "9.28.0", - "@sentry/node": "9.28.0", - "@sentry/opentelemetry": "9.28.0", - "@sentry/react": "9.28.0", + "@sentry/core": "9.28.1", + "@sentry/node": "9.28.1", + "@sentry/opentelemetry": "9.28.1", + "@sentry/react": "9.28.1", "glob": "^10.3.4", "yargs": "^17.6.0" }, diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index ac5184897cb4..d500f139c4db 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay-canvas", - "version": "9.28.0", + "version": "9.28.1", "description": "Replay canvas integration", "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", @@ -69,8 +69,8 @@ "@sentry-internal/rrweb": "2.35.0" }, "dependencies": { - "@sentry-internal/replay": "9.28.0", - "@sentry/core": "9.28.0" + "@sentry-internal/replay": "9.28.1", + "@sentry/core": "9.28.1" }, "engines": { "node": ">=18" diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index ce6d155490bb..cde5138bade4 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay", - "version": "9.28.0", + "version": "9.28.1", "description": "User replays for Sentry", "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", @@ -71,7 +71,7 @@ "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { "@babel/core": "^7.17.5", - "@sentry-internal/replay-worker": "9.28.0", + "@sentry-internal/replay-worker": "9.28.1", "@sentry-internal/rrweb": "2.35.0", "@sentry-internal/rrweb-snapshot": "2.35.0", "fflate": "0.8.2", @@ -79,8 +79,8 @@ "jsdom-worker": "^0.2.1" }, "dependencies": { - "@sentry-internal/browser-utils": "9.28.0", - "@sentry/core": "9.28.0" + "@sentry-internal/browser-utils": "9.28.1", + "@sentry/core": "9.28.1" }, "engines": { "node": ">=18" diff --git a/packages/replay-worker/package.json b/packages/replay-worker/package.json index 916ec5adddba..b94a3eb5f648 100644 --- a/packages/replay-worker/package.json +++ b/packages/replay-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay-worker", - "version": "9.28.0", + "version": "9.28.1", "description": "Worker for @sentry-internal/replay", "main": "build/esm/index.js", "module": "build/esm/index.js", diff --git a/packages/solid/package.json b/packages/solid/package.json index 4c4e2fa10a6a..56414d1944b0 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/solid", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for Solid", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solid", @@ -44,8 +44,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.28.0", - "@sentry/core": "9.28.0" + "@sentry/browser": "9.28.1", + "@sentry/core": "9.28.1" }, "peerDependencies": { "@solidjs/router": "^0.13.4", diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index e150d96ca468..4f8eae7bd512 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/solidstart", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for Solid Start", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solidstart", @@ -66,10 +66,10 @@ } }, "dependencies": { - "@sentry/core": "9.28.0", - "@sentry/node": "9.28.0", - "@sentry/opentelemetry": "9.28.0", - "@sentry/solid": "9.28.0", + "@sentry/core": "9.28.1", + "@sentry/node": "9.28.1", + "@sentry/opentelemetry": "9.28.1", + "@sentry/solid": "9.28.1", "@sentry/vite-plugin": "2.22.6" }, "devDependencies": { diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 76dfefc726e2..7601d52f28b3 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/svelte", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for Svelte", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/svelte", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.28.0", - "@sentry/core": "9.28.0", + "@sentry/browser": "9.28.1", + "@sentry/core": "9.28.1", "magic-string": "^0.30.0" }, "peerDependencies": { diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 0403c2d70b90..a13ac8bfd86f 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/sveltekit", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for SvelteKit", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/sveltekit", @@ -48,11 +48,11 @@ }, "dependencies": { "@babel/parser": "7.26.9", - "@sentry/cloudflare": "9.28.0", - "@sentry/core": "9.28.0", - "@sentry/node": "9.28.0", - "@sentry/opentelemetry": "9.28.0", - "@sentry/svelte": "9.28.0", + "@sentry/cloudflare": "9.28.1", + "@sentry/core": "9.28.1", + "@sentry/node": "9.28.1", + "@sentry/opentelemetry": "9.28.1", + "@sentry/svelte": "9.28.1", "@sentry/vite-plugin": "3.2.4", "magic-string": "0.30.7", "recast": "0.23.11", diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index d330b9b2c5c3..216b22e9b5bb 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/tanstackstart-react", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for TanStack Start React", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tanstackstart-react", @@ -52,11 +52,11 @@ "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.34.0", - "@sentry-internal/browser-utils": "9.28.0", - "@sentry/core": "9.28.0", - "@sentry/node": "9.28.0", - "@sentry/opentelemetry": "9.28.0", - "@sentry/react": "9.28.0" + "@sentry-internal/browser-utils": "9.28.1", + "@sentry/core": "9.28.1", + "@sentry/node": "9.28.1", + "@sentry/opentelemetry": "9.28.1", + "@sentry/react": "9.28.1" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/tanstackstart/package.json b/packages/tanstackstart/package.json index b8a37c3c6e58..6dd439888b45 100644 --- a/packages/tanstackstart/package.json +++ b/packages/tanstackstart/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/tanstackstart", - "version": "9.28.0", + "version": "9.28.1", "description": "Utilities for the Sentry TanStack Start SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tanstackstart", diff --git a/packages/types/package.json b/packages/types/package.json index 4a8e84e663e1..ded4f9e7ee43 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/types", - "version": "9.28.0", + "version": "9.28.1", "description": "Types for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/types", @@ -57,7 +57,7 @@ "yalc:publish": "yalc publish --push --sig" }, "dependencies": { - "@sentry/core": "9.28.0" + "@sentry/core": "9.28.1" }, "volta": { "extends": "../../package.json" diff --git a/packages/typescript/package.json b/packages/typescript/package.json index fd776fd35999..ad3432895e74 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/typescript", - "version": "9.28.0", + "version": "9.28.1", "description": "Typescript configuration used at Sentry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/typescript", diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index bb8da30332a9..c1b2121d64da 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vercel-edge", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for the Vercel Edge Runtime", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vercel-edge", @@ -40,7 +40,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@sentry/core": "9.28.0" + "@sentry/core": "9.28.1" }, "devDependencies": { "@edge-runtime/types": "3.0.1", @@ -48,7 +48,7 @@ "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", - "@sentry/opentelemetry": "9.28.0" + "@sentry/opentelemetry": "9.28.1" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/vue/package.json b/packages/vue/package.json index 6d652d9bd000..4bcc5bdb8adb 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vue", - "version": "9.28.0", + "version": "9.28.1", "description": "Official Sentry SDK for Vue.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vue", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.28.0", - "@sentry/core": "9.28.0" + "@sentry/browser": "9.28.1", + "@sentry/core": "9.28.1" }, "peerDependencies": { "pinia": "2.x || 3.x", diff --git a/packages/wasm/package.json b/packages/wasm/package.json index 926c1de46d6c..12a04d83d915 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/wasm", - "version": "9.28.0", + "version": "9.28.1", "description": "Support for WASM.", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/wasm", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.28.0", - "@sentry/core": "9.28.0" + "@sentry/browser": "9.28.1", + "@sentry/core": "9.28.1" }, "scripts": { "build": "run-p build:transpile build:bundle build:types", From a72ca10395efb6d2f3f1d063f626ad87d82a9c44 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 11 Jun 2025 11:03:08 +0200 Subject: [PATCH 11/32] chore(otel-v2-tests): Fix formatting and linting errors (#16546) turns out repo-wide auto formatting (`yarn fix:prettier` in the root) changed some files here causing unrelated changes in other PRs. Also I added lint and fix commands to this package. --- .../opentelemetry-v2-tests/package.json | 2 + .../test/helpers/isSpan.ts | 2 +- .../test/helpers/mockSdkInit.ts | 2 +- .../test/integration/transactions.test.ts | 79 +++++++++---------- .../opentelemetry-v2-tests/test/trace.test.ts | 4 +- 5 files changed, 44 insertions(+), 45 deletions(-) diff --git a/dev-packages/opentelemetry-v2-tests/package.json b/dev-packages/opentelemetry-v2-tests/package.json index 22889df5d5c3..af6b3e20d5b8 100644 --- a/dev-packages/opentelemetry-v2-tests/package.json +++ b/dev-packages/opentelemetry-v2-tests/package.json @@ -7,6 +7,8 @@ "node": ">=18" }, "scripts": { + "lint": "eslint . --format stylish", + "fix": "eslint . --format stylish --fix", "test": "vitest run", "test:watch": "vitest --watch" }, diff --git a/dev-packages/opentelemetry-v2-tests/test/helpers/isSpan.ts b/dev-packages/opentelemetry-v2-tests/test/helpers/isSpan.ts index 3146551e3da7..a0ba28173b9e 100644 --- a/dev-packages/opentelemetry-v2-tests/test/helpers/isSpan.ts +++ b/dev-packages/opentelemetry-v2-tests/test/helpers/isSpan.ts @@ -1,5 +1,5 @@ import type { Span } from '@opentelemetry/api'; -import { INVALID_TRACEID, INVALID_SPANID, type SpanContext } from '@opentelemetry/api'; +import { type SpanContext, INVALID_SPANID, INVALID_TRACEID } from '@opentelemetry/api'; export const isSpan = (value: unknown): value is Span => { return ( diff --git a/dev-packages/opentelemetry-v2-tests/test/helpers/mockSdkInit.ts b/dev-packages/opentelemetry-v2-tests/test/helpers/mockSdkInit.ts index eb112d017a1c..12372f60ea85 100644 --- a/dev-packages/opentelemetry-v2-tests/test/helpers/mockSdkInit.ts +++ b/dev-packages/opentelemetry-v2-tests/test/helpers/mockSdkInit.ts @@ -3,11 +3,11 @@ import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { ClientOptions, Options } from '@sentry/core'; import { flush, getClient, getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core'; import { setOpenTelemetryContextAsyncContextStrategy } from '../../../../packages/opentelemetry/src/asyncContextStrategy'; +import { SentrySpanProcessor } from '../../../../packages/opentelemetry/src/spanProcessor'; import type { OpenTelemetryClient } from '../../../../packages/opentelemetry/src/types'; import { clearOpenTelemetrySetupCheck } from '../../../../packages/opentelemetry/src/utils/setupCheck'; import { initOtel } from './initOtel'; import { init as initTestClient } from './TestClient'; -import { SentrySpanProcessor } from '../../../../packages/opentelemetry/src/spanProcessor'; const PUBLIC_DSN = 'https://username@domain/123'; 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..7e2bf79f6ec0 100644 --- a/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts +++ b/dev-packages/opentelemetry-v2-tests/test/integration/transactions.test.ts @@ -16,7 +16,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { SENTRY_TRACE_STATE_DSC } from '../../../../packages/opentelemetry/src/constants'; import { startInactiveSpan, startSpan } from '../../../../packages/opentelemetry/src/trace'; import { makeTraceState } from '../../../../packages/opentelemetry/src/utils/makeTraceState'; -import { cleanupOtel, getProvider, getSpanProcessor, mockSdkInit } from '../helpers/mockSdkInit'; +import { cleanupOtel, getSpanProcessor, mockSdkInit } from '../helpers/mockSdkInit'; import type { TestClientInterface } from '../helpers/TestClient'; describe('Integration | Transactions', () => { @@ -514,7 +514,6 @@ describe('Integration | Transactions', () => { }, }); - const provider = getProvider(); const spanProcessor = getSpanProcessor(); const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined; @@ -548,57 +547,56 @@ 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 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; @@ -619,7 +617,6 @@ it('collects child spans that are finished within 5 minutes their parent span ha }, }); - const provider = getProvider(); const spanProcessor = getSpanProcessor(); const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined; diff --git a/dev-packages/opentelemetry-v2-tests/test/trace.test.ts b/dev-packages/opentelemetry-v2-tests/test/trace.test.ts index 84be427a1fb3..52d5e67477d0 100644 --- a/dev-packages/opentelemetry-v2-tests/test/trace.test.ts +++ b/dev-packages/opentelemetry-v2-tests/test/trace.test.ts @@ -28,13 +28,13 @@ import { } from '../../../packages/opentelemetry/src/trace'; import type { AbstractSpan } from '../../../packages/opentelemetry/src/types'; import { getActiveSpan } from '../../../packages/opentelemetry/src/utils/getActiveSpan'; +import { getParentSpanId } from '../../../packages/opentelemetry/src/utils/getParentSpanId'; import { getSamplingDecision } from '../../../packages/opentelemetry/src/utils/getSamplingDecision'; import { getSpanKind } from '../../../packages/opentelemetry/src/utils/getSpanKind'; import { makeTraceState } from '../../../packages/opentelemetry/src/utils/makeTraceState'; import { spanHasAttributes, spanHasName } from '../../../packages/opentelemetry/src/utils/spanTypes'; -import { cleanupOtel, mockSdkInit } from './helpers/mockSdkInit'; import { isSpan } from './helpers/isSpan'; -import { getParentSpanId } from '../../../packages/opentelemetry/src/utils/getParentSpanId'; +import { cleanupOtel, mockSdkInit } from './helpers/mockSdkInit'; describe('trace', () => { beforeEach(() => { From 5fc0388130710fb14c7bcff4d5c502df2a490398 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Wed, 11 Jun 2025 11:21:27 +0200 Subject: [PATCH 12/32] feat(ember): Stop warning for `onError` usage (#16547) This deprecates the `ignoreEmberOnErrorWarning` option, as it is no longer used. Previously, we logged a warnining if we detected that this was used. But the option is now deprecated/will be removed, and since there is no real logic attached to this we can simply remove this (as well as the usage of the global `Ember` namespace) to avoid deprecations. Closes https://github.com/getsentry/sentry-javascript/issues/16504 --------- Co-authored-by: Lukas Stracke --- packages/ember/README.md | 3 --- packages/ember/addon/index.ts | 25 +++---------------- packages/ember/addon/types.ts | 3 +++ .../ember/tests/dummy/config/environment.js | 1 - 4 files changed, 6 insertions(+), 26 deletions(-) diff --git a/packages/ember/README.md b/packages/ember/README.md index e0c9694d7d49..f28d86f194a1 100644 --- a/packages/ember/README.md +++ b/packages/ember/README.md @@ -70,9 +70,6 @@ following Ember specific configuration: ```javascript ENV['@sentry/ember'] = { - // Will silence Ember.onError warning without the need of using Ember debugging tools. - ignoreEmberOnErrorWarning: false, - // Will disable automatic instrumentation of performance. // Manual instrumentation will still be sent. disablePerformance: true, diff --git a/packages/ember/addon/index.ts b/packages/ember/addon/index.ts index 4a7a59566731..bebd89cb09c3 100644 --- a/packages/ember/addon/index.ts +++ b/packages/ember/addon/index.ts @@ -1,7 +1,6 @@ -import { assert, warn } from '@ember/debug'; +import { assert } from '@ember/debug'; import type Route from '@ember/routing/route'; -import { next } from '@ember/runloop'; -import { getOwnConfig, isDevelopingApp, macroCondition } from '@embroider/macros'; +import { getOwnConfig } from '@embroider/macros'; import type { BrowserOptions } from '@sentry/browser'; import { startSpan } from '@sentry/browser'; import * as Sentry from '@sentry/browser'; @@ -12,7 +11,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, } from '@sentry/core'; -import Ember from 'ember'; import type { EmberSentryConfig, GlobalConfig, OwnConfig } from './types'; function _getSentryInitConfig(): EmberSentryConfig['sentry'] { @@ -45,24 +43,7 @@ export function init(_runtimeConfig?: BrowserOptions): Client | undefined { const sentryInitConfig = _getSentryInitConfig(); Object.assign(sentryInitConfig, initConfig); - const client = Sentry.init(initConfig); - - if (macroCondition(isDevelopingApp())) { - if (environmentConfig.ignoreEmberOnErrorWarning) { - return client; - } - next(null, function () { - warn( - 'Ember.onerror found. Using Ember.onerror can hide some errors (such as flushed runloop errors) from Sentry. Use Sentry.captureException to capture errors within Ember.onError or remove it to have errors caught by Sentry directly. This error can be silenced via addon configuration.', - !Ember.onerror, - { - id: '@sentry/ember.ember-onerror-detected', - }, - ); - }); - } - - return client; + return Sentry.init(initConfig); } type RouteConstructor = new (...args: ConstructorParameters) => Route; diff --git a/packages/ember/addon/types.ts b/packages/ember/addon/types.ts index 468cde6c310f..a66a290004f0 100644 --- a/packages/ember/addon/types.ts +++ b/packages/ember/addon/types.ts @@ -5,6 +5,9 @@ type BrowserTracingOptions = Parameters[0]; export type EmberSentryConfig = { sentry: BrowserOptions & { browserTracingOptions?: BrowserTracingOptions }; transitionTimeout: number; + /** + * @deprecated This option is no longer used and will be removed in the next major version. + */ ignoreEmberOnErrorWarning: boolean; disableInstrumentComponents: boolean; disablePerformance: boolean; diff --git a/packages/ember/tests/dummy/config/environment.js b/packages/ember/tests/dummy/config/environment.js index 144a6aebe1fa..96f525aaa568 100644 --- a/packages/ember/tests/dummy/config/environment.js +++ b/packages/ember/tests/dummy/config/environment.js @@ -32,7 +32,6 @@ module.exports = function (environment) { }, }, }, - ignoreEmberOnErrorWarning: true, minimumRunloopQueueDuration: 0, minimumComponentRenderDuration: 0, }; From fee86b076a658b796032e236dbeef5bf212cedb1 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Wed, 11 Jun 2025 11:58:16 +0200 Subject: [PATCH 13/32] fix(browser): Ensure `suppressTracing` does not leak when async (#16545) This changes the behavior of `suppressTracing` to be less problematic (or, problematic in a different way). Today, because in the browser we do not have async context isolation, there is only a single shared top scope. Because of this, if you have code like this: ```js const spanPromise = suppressTracing(async () => { await new Promise(resolve => setTimeout(resolve, 100)); return startInactiveSpan({ name: 'span' }); }); const span = startInactiveSpan({ name: 'span2' }); ``` The span2 will also be suppressed, because `suppressTracing` forks the scope and sets data on it to ensure spans are not recorded. This is problematic as that will result in completely unrelated spans, e.g. pageload/navigation spans, being suppressed while e.g. an ongoing fetch call that is suppressed happens. This PR changes this to instead only suppress tracing synchronously in the browser. This obviously is also not really ideal and can lead to things _not_ being suppressed, but it feels like the better tradeoff for now. --- packages/cloudflare/src/async.ts | 9 ++++++ packages/core/src/tracing/trace.ts | 9 +++++- packages/core/test/lib/tracing/trace.test.ts | 34 ++++++++++++++++++++ packages/opentelemetry/test/trace.test.ts | 32 ++++++++++++++++++ 4 files changed, 83 insertions(+), 1 deletion(-) diff --git a/packages/cloudflare/src/async.ts b/packages/cloudflare/src/async.ts index cd20a8d083de..66f2d439a3ce 100644 --- a/packages/cloudflare/src/async.ts +++ b/packages/cloudflare/src/async.ts @@ -64,7 +64,16 @@ export function setAsyncLocalStorageAsyncContextStrategy(): void { }); } + // In contrast to the browser, we can rely on async context isolation here + function suppressTracing(callback: () => T): T { + return withScope(scope => { + scope.setSDKProcessingMetadata({ __SENTRY_SUPPRESS_TRACING__: true }); + return callback(); + }); + } + setAsyncContextStrategy({ + suppressTracing, withScope, withSetScope, withIsolationScope, diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index a96159692ac3..427e4ebb0bf6 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -253,8 +253,15 @@ export function suppressTracing(callback: () => T): T { } return withScope(scope => { + // Note: We do not wait for the callback to finish before we reset the metadata + // the reason for this is that otherwise, in the browser this can lead to very weird behavior + // as there is only a single top scope, if the callback takes longer to finish, + // other, unrelated spans may also be suppressed, which we do not want + // so instead, we only suppress tracing synchronoysly in the browser scope.setSDKProcessingMetadata({ [SUPPRESS_TRACING_KEY]: true }); - return callback(); + const res = callback(); + scope.setSDKProcessingMetadata({ [SUPPRESS_TRACING_KEY]: undefined }); + return res; }); } diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 83f4b150a6d6..6d25afe13d3e 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -1975,6 +1975,40 @@ describe('suppressTracing', () => { expect(spanIsSampled(child)).toBe(false); }); }); + + it('works with parallel processes', async () => { + const span = suppressTracing(() => { + return startInactiveSpan({ name: 'span' }); + }); + + // Note: This is unintuitive, but it is the expected behavior + // because we only suppress tracing synchronously in the browser + const span2Promise = suppressTracing(async () => { + await new Promise(resolve => setTimeout(resolve, 100)); + return startInactiveSpan({ name: 'span2' }); + }); + + const span3Promise = suppressTracing(async () => { + const span = startInactiveSpan({ name: 'span3' }); + await new Promise(resolve => setTimeout(resolve, 100)); + return span; + }); + + const span4 = suppressTracing(() => { + return startInactiveSpan({ name: 'span' }); + }); + + const span5 = startInactiveSpan({ name: 'span5' }); + + const span2 = await span2Promise; + const span3 = await span3Promise; + + expect(spanIsSampled(span)).toBe(false); + expect(spanIsSampled(span2)).toBe(true); + expect(spanIsSampled(span3)).toBe(false); + expect(spanIsSampled(span4)).toBe(false); + expect(spanIsSampled(span5)).toBe(true); + }); }); describe('startNewTrace', () => { diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index f9aed823a4a4..d8432172a601 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -1921,6 +1921,38 @@ describe('suppressTracing', () => { expect(spanIsSampled(child)).toBe(false); }); }); + + it('works with parallel processes', async () => { + const span = suppressTracing(() => { + return startInactiveSpan({ name: 'span' }); + }); + + const span2Promise = suppressTracing(async () => { + await new Promise(resolve => setTimeout(resolve, 100)); + return startInactiveSpan({ name: 'span2' }); + }); + + const span3Promise = suppressTracing(async () => { + const span = startInactiveSpan({ name: 'span3' }); + await new Promise(resolve => setTimeout(resolve, 100)); + return span; + }); + + const span4 = suppressTracing(() => { + return startInactiveSpan({ name: 'span' }); + }); + + const span5 = startInactiveSpan({ name: 'span5' }); + + const span2 = await span2Promise; + const span3 = await span3Promise; + + expect(spanIsSampled(span)).toBe(false); + expect(spanIsSampled(span2)).toBe(false); + expect(spanIsSampled(span3)).toBe(false); + expect(spanIsSampled(span4)).toBe(false); + expect(spanIsSampled(span5)).toBe(true); + }); }); function getSpanName(span: AbstractSpan): string | undefined { From 88e3f8fb04ed4bdb0e7dc0925b61e1a05e72fb43 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Wed, 11 Jun 2025 16:32:43 +0200 Subject: [PATCH 14/32] feat(node): Allow to force activate `vercelAiIntegration` (#16551) By default, the instrumentation will register span processors only when the ai package is used. This is done to avoid overhead of span processing for users that do not even use this package. However, it seems that in some environments, esp. in Next.js, the instrumentation is not added correctly, thus never running this, and not converting spans correctly. For now, this PR adds an escape hatch to manually opt-in to this to still get correct spans: ```js vercelAiIntegration({ force: true }) ``` --- .../node/src/integrations/tracing/vercelai/index.ts | 10 ++++++++-- .../node/src/integrations/tracing/vercelai/types.ts | 6 ++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/node/src/integrations/tracing/vercelai/index.ts b/packages/node/src/integrations/tracing/vercelai/index.ts index 2c5faf04acef..d2f73e02adc3 100644 --- a/packages/node/src/integrations/tracing/vercelai/index.ts +++ b/packages/node/src/integrations/tracing/vercelai/index.ts @@ -33,7 +33,7 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { instrumentation = instrumentVercelAi(); }, setup(client) { - instrumentation?.callWhenPatched(() => { + function registerProcessors(): void { client.on('spanStart', span => { const { data: attributes, description: name } = spanToJSON(span); @@ -188,7 +188,13 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { return event; }); - }); + } + + if (options.force) { + registerProcessors(); + } else { + instrumentation?.callWhenPatched(registerProcessors); + } }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/integrations/tracing/vercelai/types.ts b/packages/node/src/integrations/tracing/vercelai/types.ts index 50434b70604f..35cfeb33a112 100644 --- a/packages/node/src/integrations/tracing/vercelai/types.ts +++ b/packages/node/src/integrations/tracing/vercelai/types.ts @@ -56,6 +56,12 @@ export interface VercelAiOptions { * or if you set `isEnabled` to `true` in your ai SDK method telemetry settings */ recordOutputs?: boolean; + + /** + * By default, the instrumentation will register span processors only when the ai package is used. + * If you want to register the span processors even when the ai package usage cannot be detected, you can set `force` to `true`. + */ + force?: boolean; } export interface VercelAiIntegration extends Integration { From eb5960418a9c55505985b5de6e4302d786d377ea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 16:27:44 +0000 Subject: [PATCH 15/32] feat(deps): Bump @sentry/rollup-plugin from 3.4.0 to 3.5.0 (#16524) --- packages/nuxt/package.json | 2 +- yarn.lock | 29 +++++------------------------ 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 17f2d90f0ca4..0cbea66817fc 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -47,7 +47,7 @@ "@sentry/core": "9.28.1", "@sentry/node": "9.28.1", "@sentry/opentelemetry": "9.28.1", - "@sentry/rollup-plugin": "3.4.0", + "@sentry/rollup-plugin": "3.5.0", "@sentry/vite-plugin": "3.2.4", "@sentry/vue": "9.28.1" }, diff --git a/yarn.lock b/yarn.lock index 050e00b29f6a..02a6f885056b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6571,11 +6571,6 @@ resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.2.4.tgz#c0877df6e5ce227bf51754bf27da2fa5227af847" integrity sha512-yBzRn3GEUSv1RPtE4xB4LnuH74ZxtdoRJ5cmQ9i6mzlmGDxlrnKuvem5++AolZTE9oJqAD3Tx2rd1PqmpWnLoA== -"@sentry/babel-plugin-component-annotate@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.4.0.tgz#f47a7652e16f84556df82cbc38f0004bca1335d1" - integrity sha512-tSzfc3aE7m0PM0Aj7HBDet5llH9AB9oc+tBQ8AvOqUSnWodLrNCuWeQszJ7mIBovD3figgCU3h0cvI6U5cDtsg== - "@sentry/babel-plugin-component-annotate@3.5.0": version "3.5.0" resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.5.0.tgz#1b0d01f903b725da876117d551610085c3dd21c7" @@ -6609,20 +6604,6 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/bundler-plugin-core@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.4.0.tgz#3a3459aba94cbeb093347f5730f15df25153fd0a" - integrity sha512-X1Q41AsQ6xcT6hB4wYmBDBukndKM/inT4IsR7pdKLi7ICpX2Qq6lisamBAEPCgEvnLpazSFguaiC0uiwMKAdqw== - dependencies: - "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "3.4.0" - "@sentry/cli" "2.42.2" - dotenv "^16.3.1" - find-up "^5.0.0" - glob "^9.3.2" - magic-string "0.30.8" - unplugin "1.0.1" - "@sentry/bundler-plugin-core@3.5.0": version "3.5.0" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.5.0.tgz#b62af5be1b1a862e7062181655829c556c7d7c0b" @@ -6751,12 +6732,12 @@ "@sentry/cli-win32-i686" "2.46.0" "@sentry/cli-win32-x64" "2.46.0" -"@sentry/rollup-plugin@3.4.0": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@sentry/rollup-plugin/-/rollup-plugin-3.4.0.tgz#326618d6fe91a030ee4ab335e1bab35f201090b0" - integrity sha512-oqDcjV+aaTZZ7oOadk90KlShOYfKEEQsvbZtzHl7HPHNt5kmtTaQyWphPIDt2Z9OCK8QF5T8GLsr1MCOXJ6vqA== +"@sentry/rollup-plugin@3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@sentry/rollup-plugin/-/rollup-plugin-3.5.0.tgz#9015c48e00257f8440597167498499804371329b" + integrity sha512-aMPCvdNMkv//LZYjYCJsEcNiNiaQFinBO75+9NJVEe1OrKNdGqDi3hky2ll7zuY+xozEtZCZcUKJJz/aAYAS8A== dependencies: - "@sentry/bundler-plugin-core" "3.4.0" + "@sentry/bundler-plugin-core" "3.5.0" unplugin "1.0.1" "@sentry/vite-plugin@2.22.6", "@sentry/vite-plugin@^2.22.6": From ba3728d782e2c7bfbb0c9ffc205de3a9b9237201 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 11 Jun 2025 13:30:38 -0400 Subject: [PATCH 16/32] feat(node): Introduce `ignoreLayersType` option to koa integration (#16553) ref https://linear.app/getsentry/issue/FE-503/investigate-nested-middleware-spans-in-webfx-koa-application The Koa integration in `@sentry/node` was updated to expose the `ignoreLayersType` option from `@opentelemetry/instrumentation-koa`, aligning its configuration with the GraphQL integration. https://www.npmjs.com/package/@opentelemetry/instrumentation-koa

Koa Instrumentation Options

Options | Type | Example | Description -- | -- | -- | -- ignoreLayersType | KoaLayerType[] | ['middleware'] | Ignore layers of specified type. requestHook | KoaRequestCustomAttributeFunction | (span, info) => {} | Function for adding custom attributes to Koa middleware layers. Receives params: Span, KoaRequestInfo.

ignoreLayersType accepts an array of KoaLayerType which can take the following string values:

  • router,
  • middleware.
--------- Co-authored-by: Cursor Agent --- packages/node/src/integrations/tracing/koa.ts | 81 +++++++++++------- .../test/integrations/tracing/koa.test.ts | 83 +++++++++++++++++++ 2 files changed, 134 insertions(+), 30 deletions(-) create mode 100644 packages/node/test/integrations/tracing/koa.test.ts diff --git a/packages/node/src/integrations/tracing/koa.ts b/packages/node/src/integrations/tracing/koa.ts index e2a2a52264ae..43b901afebee 100644 --- a/packages/node/src/integrations/tracing/koa.ts +++ b/packages/node/src/integrations/tracing/koa.ts @@ -1,6 +1,7 @@ +import type { KoaInstrumentationConfig, KoaLayerType } from '@opentelemetry/instrumentation-koa'; import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; -import type { IntegrationFn, Span } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/core'; import { captureException, defineIntegration, @@ -8,27 +9,51 @@ import { getIsolationScope, logger, SEMANTIC_ATTRIBUTE_SENTRY_OP, - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; import { DEBUG_BUILD } from '../../debug-build'; import { generateInstrumentOnce } from '../../otel/instrument'; +import { addOriginToSpan } from '../../utils/addOriginToSpan'; import { ensureIsWrapped } from '../../utils/ensureIsWrapped'; +interface KoaOptions { + /** + * Ignore layers of specified types + */ + ignoreLayersType?: Array<'middleware' | 'router'>; +} + const INTEGRATION_NAME = 'Koa'; export const instrumentKoa = generateInstrumentOnce( INTEGRATION_NAME, - () => - new KoaInstrumentation({ + KoaInstrumentation, + (options: KoaOptions = {}) => { + return { + ignoreLayersType: options.ignoreLayersType as KoaLayerType[], requestHook(span, info) { - addKoaSpanAttributes(span); + addOriginToSpan(span, 'auto.http.otel.koa'); + + const attributes = spanToJSON(span).data; + + // this is one of: middleware, router + const type = attributes['koa.type']; + if (type) { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, `${type}.koa`); + } + + // Also update the name + const name = attributes['koa.name']; + if (typeof name === 'string') { + // Somehow, name is sometimes `''` for middleware spans + // See: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2220 + span.updateName(name || '< unknown >'); + } if (getIsolationScope() === getDefaultIsolationScope()) { DEBUG_BUILD && logger.warn('Isolation scope is default isolation scope - skipping setting transactionName'); return; } - const attributes = spanToJSON(span).data; const route = attributes[ATTR_HTTP_ROUTE]; // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const method = info.context?.request?.method?.toUpperCase() || 'GET'; @@ -36,14 +61,15 @@ export const instrumentKoa = generateInstrumentOnce( getIsolationScope().setTransactionName(`${method} ${route}`); } }, - }), + } satisfies KoaInstrumentationConfig; + }, ); -const _koaIntegration = (() => { +const _koaIntegration = ((options: KoaOptions = {}) => { return { name: INTEGRATION_NAME, setupOnce() { - instrumentKoa(); + instrumentKoa(options); }, }; }) satisfies IntegrationFn; @@ -55,6 +81,8 @@ const _koaIntegration = (() => { * * For more information, see the [koa documentation](https://docs.sentry.io/platforms/javascript/guides/koa/). * + * @param {KoaOptions} options Configuration options for the Koa integration. + * * @example * ```javascript * const Sentry = require('@sentry/node'); @@ -63,6 +91,20 @@ const _koaIntegration = (() => { * integrations: [Sentry.koaIntegration()], * }) * ``` + * + * @example + * ```javascript + * // To ignore middleware spans + * const Sentry = require('@sentry/node'); + * + * Sentry.init({ + * integrations: [ + * Sentry.koaIntegration({ + * ignoreLayersType: ['middleware'] + * }) + * ], + * }) + * ``` */ export const koaIntegration = defineIntegration(_koaIntegration); @@ -101,24 +143,3 @@ export const setupKoaErrorHandler = (app: { use: (arg0: (ctx: any, next: any) => ensureIsWrapped(app.use, 'koa'); }; - -function addKoaSpanAttributes(span: Span): void { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto.http.otel.koa'); - - const attributes = spanToJSON(span).data; - - // this is one of: middleware, router - const type = attributes['koa.type']; - - if (type) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, `${type}.koa`); - } - - // Also update the name - const name = attributes['koa.name']; - if (typeof name === 'string') { - // Somehow, name is sometimes `''` for middleware spans - // See: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2220 - span.updateName(name || '< unknown >'); - } -} diff --git a/packages/node/test/integrations/tracing/koa.test.ts b/packages/node/test/integrations/tracing/koa.test.ts new file mode 100644 index 000000000000..9ca221dfba03 --- /dev/null +++ b/packages/node/test/integrations/tracing/koa.test.ts @@ -0,0 +1,83 @@ +import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; +import { type MockInstance, beforeEach, describe, expect, it, vi } from 'vitest'; +import { instrumentKoa, koaIntegration } from '../../../src/integrations/tracing/koa'; +import { INSTRUMENTED } from '../../../src/otel/instrument'; + +vi.mock('@opentelemetry/instrumentation-koa'); + +describe('Koa', () => { + beforeEach(() => { + vi.clearAllMocks(); + delete INSTRUMENTED.Koa; + + (KoaInstrumentation as unknown as MockInstance).mockImplementation(() => { + return { + setTracerProvider: () => undefined, + setMeterProvider: () => undefined, + getConfig: () => ({}), + setConfig: () => ({}), + enable: () => undefined, + }; + }); + }); + + it('defaults are correct for instrumentKoa', () => { + instrumentKoa({}); + + expect(KoaInstrumentation).toHaveBeenCalledTimes(1); + expect(KoaInstrumentation).toHaveBeenCalledWith({ + ignoreLayersType: undefined, + requestHook: expect.any(Function), + }); + }); + + it('passes ignoreLayersType option to instrumentation', () => { + instrumentKoa({ ignoreLayersType: ['middleware'] }); + + expect(KoaInstrumentation).toHaveBeenCalledTimes(1); + expect(KoaInstrumentation).toHaveBeenCalledWith({ + ignoreLayersType: ['middleware'], + requestHook: expect.any(Function), + }); + }); + + it('passes multiple ignoreLayersType values to instrumentation', () => { + instrumentKoa({ ignoreLayersType: ['middleware', 'router'] }); + + expect(KoaInstrumentation).toHaveBeenCalledTimes(1); + expect(KoaInstrumentation).toHaveBeenCalledWith({ + ignoreLayersType: ['middleware', 'router'], + requestHook: expect.any(Function), + }); + }); + + it('defaults are correct for koaIntegration', () => { + koaIntegration().setupOnce!(); + + expect(KoaInstrumentation).toHaveBeenCalledTimes(1); + expect(KoaInstrumentation).toHaveBeenCalledWith({ + ignoreLayersType: undefined, + requestHook: expect.any(Function), + }); + }); + + it('passes options from koaIntegration to instrumentation', () => { + koaIntegration({ ignoreLayersType: ['middleware'] }).setupOnce!(); + + expect(KoaInstrumentation).toHaveBeenCalledTimes(1); + expect(KoaInstrumentation).toHaveBeenCalledWith({ + ignoreLayersType: ['middleware'], + requestHook: expect.any(Function), + }); + }); + + it('passes multiple options from koaIntegration to instrumentation', () => { + koaIntegration({ ignoreLayersType: ['router', 'middleware'] }).setupOnce!(); + + expect(KoaInstrumentation).toHaveBeenCalledTimes(1); + expect(KoaInstrumentation).toHaveBeenCalledWith({ + ignoreLayersType: ['router', 'middleware'], + requestHook: expect.any(Function), + }); + }); +}); From 65310d51f151ef826b9acc9cf0d1deace8bae02b Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Wed, 11 Jun 2025 15:24:41 -0400 Subject: [PATCH 17/32] chore: Document `process.env.DEBUG` in node integration tests README (#16558) A new "Debugging Tests" section was added to `dev-packages/node-integration-tests/README.md`. This section documents the `DEBUG` environment variable, which enables verbose logging for the integration test suite. When `DEBUG=1` is set (e.g., `DEBUG=1 yarn test`), the test runner provides detailed output, including: * Test scenario startup information (path, flags, DSN). * Docker Compose output when tests use `withDockerCompose`. * Child process stdout and stderr. * HTTP requests made during tests. * Process errors and exceptions. * Line-by-line output from test scenarios. This addition improves discoverability and understanding of the debugging capabilities, aiding in troubleshooting failing tests and analyzing test execution flow. --------- Co-authored-by: Cursor Agent --- dev-packages/node-integration-tests/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/dev-packages/node-integration-tests/README.md b/dev-packages/node-integration-tests/README.md index c920f05d5e31..b2d8db2124d1 100644 --- a/dev-packages/node-integration-tests/README.md +++ b/dev-packages/node-integration-tests/README.md @@ -47,3 +47,20 @@ To run tests with Vitest's watch mode: To filter tests by their title: `yarn test -t "set different properties of a scope"` + +## Debugging Tests + +To enable verbose logging during test execution, set the `DEBUG` environment variable: + +`DEBUG=1 yarn test` + +When `DEBUG` is enabled, the test runner will output: + +- Test scenario startup information (path, flags, DSN) +- Docker Compose output when using `withDockerCompose` +- Child process stdout and stderr output +- HTTP requests made during tests +- Process errors and exceptions +- Line-by-line output from test scenarios + +This is particularly useful when debugging failing tests or understanding the test execution flow. From 6fc18fe4a020ae8060bdcf72d97cf3ba83111158 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 12 Jun 2025 09:26:58 +0200 Subject: [PATCH 18/32] feat(browser): Update `web-vitals` to 5.0.2 (#16492) Bump vendored `web-vitals` library. Important changes from the original library: - For now until the next SDK major, we'll keep reporting FID. `web-vitals` removed the already deprecated APIs for it in v5 but we simply keep them from v4 - `web-vitals` further removed compatibility for older iOS Safari versions. Unfortunately, [we still support Safari 14.4 ](https://docs.sentry.io/platforms/javascript/troubleshooting/supported-browsers/) which is the last version that doesn't yet fully support the `visibilitychange` event. This requires us to keep the `onHidden` helper around which also listens to `pagehide` events that this Safari version supports. I adjusted our integration tests to keep one around that fails if we remove this special handling in a future upgrade (also added some context for future us). I will follow up with at least one more PR to do some more refactorings but I decided to keep them minimal in this PR to get better diffs for reviewing: - rename the `get*` files to `on*`, since this is how they're named now in the official library closes #16310 --- .size-limit.js | 4 +- .../metrics/web-vitals-inp-late/test.ts | 5 +- .../web-vitals-inp-parametrized-late/test.ts | 5 +- .../web-vitals-inp-parametrized/test.ts | 5 +- .../tracing/metrics/web-vitals-inp/init.js | 1 + .../tracing/metrics/web-vitals-inp/test.ts | 13 +- packages/browser-utils/src/metrics/cls.ts | 1 + .../src/metrics/web-vitals/README.md | 10 +- .../src/metrics/web-vitals/getCLS.ts | 53 ++---- .../src/metrics/web-vitals/getFID.ts | 5 + .../src/metrics/web-vitals/getINP.ts | 47 ++++-- .../src/metrics/web-vitals/getLCP.ts | 51 +++--- .../web-vitals/lib/InteractionManager.ts | 151 ++++++++++++++++++ .../metrics/web-vitals/lib/LCPEntryManager.ts | 26 +++ .../web-vitals/lib/LayoutShiftManager.ts | 55 +++++++ .../metrics/web-vitals/lib/bindReporter.ts | 2 +- .../web-vitals/lib/generateUniqueID.ts | 2 +- .../web-vitals/lib/getActivationStart.ts | 2 +- .../web-vitals/lib/getNavigationEntry.ts | 8 +- .../web-vitals/lib/getVisibilityWatcher.ts | 24 ++- .../src/metrics/web-vitals/lib/initMetric.ts | 4 +- .../src/metrics/web-vitals/lib/initUnique.ts | 29 ++++ .../src/metrics/web-vitals/lib/observe.ts | 14 +- .../src/metrics/web-vitals/lib/onHidden.ts | 8 +- .../lib/{whenIdle.ts => whenIdleOrHidden.ts} | 19 +-- .../src/metrics/web-vitals/onFCP.ts | 4 +- .../src/metrics/web-vitals/onTTFB.ts | 2 +- .../src/metrics/web-vitals/types.ts | 47 +++++- .../src/metrics/web-vitals/types/base.ts | 16 +- .../src/metrics/web-vitals/types/cls.ts | 7 +- .../src/metrics/web-vitals/types/inp.ts | 114 +++++++++---- .../src/metrics/web-vitals/types/lcp.ts | 9 +- 32 files changed, 563 insertions(+), 180 deletions(-) create mode 100644 packages/browser-utils/src/metrics/web-vitals/lib/InteractionManager.ts create mode 100644 packages/browser-utils/src/metrics/web-vitals/lib/LCPEntryManager.ts create mode 100644 packages/browser-utils/src/metrics/web-vitals/lib/LayoutShiftManager.ts create mode 100644 packages/browser-utils/src/metrics/web-vitals/lib/initUnique.ts rename packages/browser-utils/src/metrics/web-vitals/lib/{whenIdle.ts => whenIdleOrHidden.ts} (67%) diff --git a/.size-limit.js b/.size-limit.js index c3105a772987..c1725577c856 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -120,7 +120,7 @@ module.exports = [ import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'), ignore: ['react/jsx-runtime'], gzip: true, - limit: '40.5 KB', + limit: '41 KB', }, // Vue SDK (ESM) { @@ -215,7 +215,7 @@ module.exports = [ import: createImport('init'), ignore: ['$app/stores'], gzip: true, - limit: '39 KB', + limit: '40 KB', }, // Node SDK (ESM) { diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts index 7d448325b6ef..942230b4594e 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-late/test.ts @@ -4,6 +4,7 @@ import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests, + hidePage, properFullEnvelopeRequestParser, shouldSkipTracingTest, } from '../../../../utils/helpers'; @@ -33,9 +34,7 @@ sentryTest('should capture an INP click event span after pageload', async ({ bro await page.waitForTimeout(500); // Page hide to trigger INP - await page.evaluate(() => { - window.dispatchEvent(new Event('pagehide')); - }); + await hidePage(page); // Get the INP span envelope const spanEnvelope = (await spanEnvelopePromise)[0]; diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts index 8056cd88c3e5..435ed8398668 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized-late/test.ts @@ -4,6 +4,7 @@ import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests, + hidePage, properFullEnvelopeRequestParser, shouldSkipTracingTest, } from '../../../../utils/helpers'; @@ -35,9 +36,7 @@ sentryTest( await page.waitForTimeout(500); // Page hide to trigger INP - await page.evaluate(() => { - window.dispatchEvent(new Event('pagehide')); - }); + await hidePage(page); // Get the INP span envelope const spanEnvelope = (await spanEnvelopePromise)[0]; diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts index 46f943b08551..9d83d2608893 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp-parametrized/test.ts @@ -3,6 +3,7 @@ import type { SpanEnvelope } from '@sentry/core'; import { sentryTest } from '../../../../utils/fixtures'; import { getMultipleSentryEnvelopeRequests, + hidePage, properFullEnvelopeRequestParser, shouldSkipTracingTest, } from '../../../../utils/helpers'; @@ -33,9 +34,7 @@ sentryTest( await page.waitForTimeout(500); // Page hide to trigger INP - await page.evaluate(() => { - window.dispatchEvent(new Event('pagehide')); - }); + await hidePage(page); // Get the INP span envelope const spanEnvelope = (await spanEnvelopePromise)[0]; diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/init.js index a941877ff88e..546698dc3d11 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/init.js @@ -14,6 +14,7 @@ Sentry.init({ }), ], tracesSampleRate: 1, + debug: true, }); const client = Sentry.getClient(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts index ac8dccd13dce..bf85d0ad99af 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-inp/test.ts @@ -4,6 +4,7 @@ import { sentryTest } from '../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest, getMultipleSentryEnvelopeRequests, + hidePage, properFullEnvelopeRequestParser, shouldSkipTracingTest, } from '../../../../utils/helpers'; @@ -32,9 +33,7 @@ sentryTest('should capture an INP click event span during pageload', async ({ br await page.waitForTimeout(500); // Page hide to trigger INP - await page.evaluate(() => { - window.dispatchEvent(new Event('pagehide')); - }); + await hidePage(page); // Get the INP span envelope const spanEnvelope = (await spanEnvelopePromise)[0]; @@ -118,6 +117,14 @@ sentryTest( }); // Page hide to trigger INP + + // Important: Purposefully not using hidePage() here to test the hidden state + // via the `pagehide` event. This is necessary because iOS Safari 14.4 + // still doesn't fully emit the `visibilitychange` events but it's the lower + // bound for Safari on iOS that we support. + // If this test times out or fails, it's likely because we tried updating + // the web-vitals library which officially already dropped support for + // this iOS version await page.evaluate(() => { window.dispatchEvent(new Event('pagehide')); }); diff --git a/packages/browser-utils/src/metrics/cls.ts b/packages/browser-utils/src/metrics/cls.ts index a6c53800ccc1..1d35ff53853f 100644 --- a/packages/browser-utils/src/metrics/cls.ts +++ b/packages/browser-utils/src/metrics/cls.ts @@ -58,6 +58,7 @@ export function trackClsAsStandaloneSpan(): void { standaloneClsEntry = entry; }, true); + // TODO: Figure out if we can switch to using whenIdleOrHidden instead of onHidden // use pagehide event from web-vitals onHidden(() => { _collectClsOnce(); diff --git a/packages/browser-utils/src/metrics/web-vitals/README.md b/packages/browser-utils/src/metrics/web-vitals/README.md index c4b2b1a1c0cf..4f9d29e5f02f 100644 --- a/packages/browser-utils/src/metrics/web-vitals/README.md +++ b/packages/browser-utils/src/metrics/web-vitals/README.md @@ -2,10 +2,10 @@ > A modular library for measuring the [Web Vitals](https://web.dev/vitals/) metrics on real users. -This was vendored from: https://github.com/GoogleChrome/web-vitals: v3.5.2 +This was vendored from: https://github.com/GoogleChrome/web-vitals: v5.0.2 The commit SHA used is: -[3d2b3dc8576cc003618952fa39902fab764a53e2](https://github.com/GoogleChrome/web-vitals/tree/3d2b3dc8576cc003618952fa39902fab764a53e2) +[463abbd425cda01ed65e0b5d18be9f559fe446cb](https://github.com/GoogleChrome/web-vitals/tree/463abbd425cda01ed65e0b5d18be9f559fe446cb) Current vendored web vitals are: @@ -27,6 +27,12 @@ web-vitals only report once per pageload. ## CHANGELOG +https://github.com/getsentry/sentry-javascript/pull/16492 + +- Bumped from Web Vitals 4.2.5 to 5.0.2 + - Mainly fixes some INP, LCP and FCP edge cases + - Original library removed FID; we still keep it around for now + https://github.com/getsentry/sentry-javascript/pull/14439 - Bumped from Web Vitals v3.5.2 to v4.2.4 diff --git a/packages/browser-utils/src/metrics/web-vitals/getCLS.ts b/packages/browser-utils/src/metrics/web-vitals/getCLS.ts index a9b6f9f26999..1b4d50a7c44e 100644 --- a/packages/browser-utils/src/metrics/web-vitals/getCLS.ts +++ b/packages/browser-utils/src/metrics/web-vitals/getCLS.ts @@ -14,10 +14,12 @@ * limitations under the License. */ +import { WINDOW } from '../../types'; import { bindReporter } from './lib/bindReporter'; import { initMetric } from './lib/initMetric'; +import { initUnique } from './lib/initUnique'; +import { LayoutShiftManager } from './lib/LayoutShiftManager'; import { observe } from './lib/observe'; -import { onHidden } from './lib/onHidden'; import { runOnce } from './lib/runOnce'; import { onFCP } from './onFCP'; import type { CLSMetric, MetricRatingThresholds, ReportOpts } from './types'; @@ -54,58 +56,37 @@ export const onCLS = (onReport: (metric: CLSMetric) => void, opts: ReportOpts = const metric = initMetric('CLS', 0); let report: ReturnType; - let sessionValue = 0; - let sessionEntries: LayoutShift[] = []; + const layoutShiftManager = initUnique(opts, LayoutShiftManager); const handleEntries = (entries: LayoutShift[]) => { - entries.forEach(entry => { - // Only count layout shifts without recent user input. - if (!entry.hadRecentInput) { - const firstSessionEntry = sessionEntries[0]; - const lastSessionEntry = sessionEntries[sessionEntries.length - 1]; - - // If the entry occurred less than 1 second after the previous entry - // and less than 5 seconds after the first entry in the session, - // include the entry in the current session. Otherwise, start a new - // session. - if ( - sessionValue && - firstSessionEntry && - lastSessionEntry && - entry.startTime - lastSessionEntry.startTime < 1000 && - entry.startTime - firstSessionEntry.startTime < 5000 - ) { - sessionValue += entry.value; - sessionEntries.push(entry); - } else { - sessionValue = entry.value; - sessionEntries = [entry]; - } - } - }); + for (const entry of entries) { + layoutShiftManager._processEntry(entry); + } // If the current session value is larger than the current CLS value, // update CLS and the entries contributing to it. - if (sessionValue > metric.value) { - metric.value = sessionValue; - metric.entries = sessionEntries; + if (layoutShiftManager._sessionValue > metric.value) { + metric.value = layoutShiftManager._sessionValue; + metric.entries = layoutShiftManager._sessionEntries; report(); } }; const po = observe('layout-shift', handleEntries); if (po) { - report = bindReporter(onReport, metric, CLSThresholds, opts.reportAllChanges); + report = bindReporter(onReport, metric, CLSThresholds, opts!.reportAllChanges); - onHidden(() => { - handleEntries(po.takeRecords() as CLSMetric['entries']); - report(true); + WINDOW.document?.addEventListener('visibilitychange', () => { + if (WINDOW.document?.visibilityState === 'hidden') { + handleEntries(po.takeRecords() as CLSMetric['entries']); + report(true); + } }); // Queue a task to report (if nothing else triggers a report first). // This allows CLS to be reported as soon as FCP fires when // `reportAllChanges` is true. - setTimeout(report, 0); + WINDOW?.setTimeout?.(report); } }), ); diff --git a/packages/browser-utils/src/metrics/web-vitals/getFID.ts b/packages/browser-utils/src/metrics/web-vitals/getFID.ts index e8fd4fa908e7..b549f4c07c7c 100644 --- a/packages/browser-utils/src/metrics/web-vitals/getFID.ts +++ b/packages/browser-utils/src/metrics/web-vitals/getFID.ts @@ -12,6 +12,10 @@ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. + * + * // Sentry: web-vitals removed FID reporting from v5. We're keeping it around + * for the time being. + * // TODO(v10): Remove FID reporting! */ import { bindReporter } from './lib/bindReporter'; @@ -60,6 +64,7 @@ export const onFID = (onReport: (metric: FIDMetric) => void, opts: ReportOpts = report = bindReporter(onReport, metric, FIDThresholds, opts.reportAllChanges); if (po) { + // sentry: TODO: Figure out if we can use new whinIdleOrHidden insteard of onHidden onHidden( runOnce(() => { handleEntries(po.takeRecords() as FIDMetric['entries']); diff --git a/packages/browser-utils/src/metrics/web-vitals/getINP.ts b/packages/browser-utils/src/metrics/web-vitals/getINP.ts index af5bd05fb413..f5efbcbc3afc 100644 --- a/packages/browser-utils/src/metrics/web-vitals/getINP.ts +++ b/packages/browser-utils/src/metrics/web-vitals/getINP.ts @@ -14,31 +14,37 @@ * limitations under the License. */ -import { WINDOW } from '../../types'; import { bindReporter } from './lib/bindReporter'; import { initMetric } from './lib/initMetric'; -import { DEFAULT_DURATION_THRESHOLD, estimateP98LongestInteraction, processInteractionEntry } from './lib/interactions'; +import { initUnique } from './lib/initUnique'; +import { InteractionManager } from './lib/InteractionManager'; import { observe } from './lib/observe'; import { onHidden } from './lib/onHidden'; import { initInteractionCountPolyfill } from './lib/polyfills/interactionCountPolyfill'; import { whenActivated } from './lib/whenActivated'; -import { whenIdle } from './lib/whenIdle'; -import type { INPMetric, MetricRatingThresholds, ReportOpts } from './types'; +import { whenIdleOrHidden } from './lib/whenIdleOrHidden'; +import type { INPMetric, INPReportOpts, MetricRatingThresholds } from './types'; /** Thresholds for INP. See https://web.dev/articles/inp#what_is_a_good_inp_score */ export const INPThresholds: MetricRatingThresholds = [200, 500]; +// The default `durationThreshold` used across this library for observing +// `event` entries via PerformanceObserver. +const DEFAULT_DURATION_THRESHOLD = 40; + /** * Calculates the [INP](https://web.dev/articles/inp) value for the current * page and calls the `callback` function once the value is ready, along with * the `event` performance entries reported for that interaction. The reported * value is a `DOMHighResTimeStamp`. * - * A custom `durationThreshold` configuration option can optionally be passed to - * control what `event-timing` entries are considered for INP reporting. The - * default threshold is `40`, which means INP scores of less than 40 are - * reported as 0. Note that this will not affect your 75th percentile INP value - * unless that value is also less than 40 (well below the recommended + * A custom `durationThreshold` configuration option can optionally be passed + * to control what `event-timing` entries are considered for INP reporting. The + * default threshold is `40`, which means INP scores of less than 40 will not + * be reported. To avoid reporting no interactions in these cases, the library + * will fall back to the input delay of the first interaction. Note that this + * will not affect your 75th percentile INP value unless that value is also + * less than 40 (well below the recommended * [good](https://web.dev/articles/inp#what_is_a_good_inp_score) threshold). * * If the `reportAllChanges` configuration option is set to `true`, the @@ -55,9 +61,9 @@ export const INPThresholds: MetricRatingThresholds = [200, 500]; * hidden. As a result, the `callback` function might be called multiple times * during the same page load._ */ -export const onINP = (onReport: (metric: INPMetric) => void, opts: ReportOpts = {}) => { +export const onINP = (onReport: (metric: INPMetric) => void, opts: INPReportOpts = {}) => { // Return if the browser doesn't support all APIs needed to measure INP. - if (!('PerformanceEventTiming' in WINDOW && 'interactionId' in PerformanceEventTiming.prototype)) { + if (!(globalThis.PerformanceEventTiming && 'interactionId' in PerformanceEventTiming.prototype)) { return; } @@ -69,6 +75,8 @@ export const onINP = (onReport: (metric: INPMetric) => void, opts: ReportOpts = // eslint-disable-next-line prefer-const let report: ReturnType; + const interactionManager = initUnique(opts, InteractionManager); + const handleEntries = (entries: INPMetric['entries']) => { // Queue the `handleEntries()` callback in the next idle task. // This is needed to increase the chances that all event entries that @@ -76,13 +84,15 @@ export const onINP = (onReport: (metric: INPMetric) => void, opts: ReportOpts = // have been dispatched. Note: there is currently an experiment // running in Chrome (EventTimingKeypressAndCompositionInteractionId) // 123+ that if rolled out fully may make this no longer necessary. - whenIdle(() => { - entries.forEach(processInteractionEntry); + whenIdleOrHidden(() => { + for (const entry of entries) { + interactionManager._processEntry(entry); + } - const inp = estimateP98LongestInteraction(); + const inp = interactionManager._estimateP98LongestInteraction(); - if (inp && inp.latency !== metric.value) { - metric.value = inp.latency; + if (inp && inp._latency !== metric.value) { + metric.value = inp._latency; metric.entries = inp.entries; report(); } @@ -96,7 +106,7 @@ export const onINP = (onReport: (metric: INPMetric) => void, opts: ReportOpts = // and performance. Running this callback for any interaction that spans // just one or two frames is likely not worth the insight that could be // gained. - durationThreshold: opts.durationThreshold != null ? opts.durationThreshold : DEFAULT_DURATION_THRESHOLD, + durationThreshold: opts.durationThreshold ?? DEFAULT_DURATION_THRESHOLD, }); report = bindReporter(onReport, metric, INPThresholds, opts.reportAllChanges); @@ -106,6 +116,9 @@ export const onINP = (onReport: (metric: INPMetric) => void, opts: ReportOpts = // where the first interaction is less than the `durationThreshold`. po.observe({ type: 'first-input', buffered: true }); + // sentry: we use onHidden instead of directly listening to visibilitychange + // because some browsers we still support (Safari <14.4) don't fully support + // `visibilitychange` or have known bugs w.r.t the `visibilitychange` event. onHidden(() => { handleEntries(po.takeRecords() as INPMetric['entries']); report(true); diff --git a/packages/browser-utils/src/metrics/web-vitals/getLCP.ts b/packages/browser-utils/src/metrics/web-vitals/getLCP.ts index 17fd374e7611..0f2f821d9bcc 100644 --- a/packages/browser-utils/src/metrics/web-vitals/getLCP.ts +++ b/packages/browser-utils/src/metrics/web-vitals/getLCP.ts @@ -19,18 +19,17 @@ import { bindReporter } from './lib/bindReporter'; import { getActivationStart } from './lib/getActivationStart'; import { getVisibilityWatcher } from './lib/getVisibilityWatcher'; import { initMetric } from './lib/initMetric'; +import { initUnique } from './lib/initUnique'; +import { LCPEntryManager } from './lib/LCPEntryManager'; import { observe } from './lib/observe'; -import { onHidden } from './lib/onHidden'; import { runOnce } from './lib/runOnce'; import { whenActivated } from './lib/whenActivated'; -import { whenIdle } from './lib/whenIdle'; +import { whenIdleOrHidden } from './lib/whenIdleOrHidden'; import type { LCPMetric, MetricRatingThresholds, ReportOpts } from './types'; /** Thresholds for LCP. See https://web.dev/articles/lcp#what_is_a_good_lcp_score */ export const LCPThresholds: MetricRatingThresholds = [2500, 4000]; -const reportedMetricIDs: Record = {}; - /** * Calculates the [LCP](https://web.dev/articles/lcp) value for the current page and * calls the `callback` function once the value is ready (along with the @@ -48,28 +47,32 @@ export const onLCP = (onReport: (metric: LCPMetric) => void, opts: ReportOpts = const metric = initMetric('LCP'); let report: ReturnType; + const lcpEntryManager = initUnique(opts, LCPEntryManager); + const handleEntries = (entries: LCPMetric['entries']) => { // If reportAllChanges is set then call this function for each entry, // otherwise only consider the last one. - if (!opts.reportAllChanges) { + if (!opts!.reportAllChanges) { // eslint-disable-next-line no-param-reassign entries = entries.slice(-1); } - entries.forEach(entry => { + for (const entry of entries) { + lcpEntryManager._processEntry(entry); + // Only report if the page wasn't hidden prior to LCP. if (entry.startTime < visibilityWatcher.firstHiddenTime) { // The startTime attribute returns the value of the renderTime if it is // not 0, and the value of the loadTime otherwise. The activationStart // reference is used because LCP should be relative to page activation - // rather than navigation start if the page was pre-rendered. But in cases + // rather than navigation start if the page was prerendered. But in cases // where `activationStart` occurs after the LCP, this time should be // clamped at 0. metric.value = Math.max(entry.startTime - getActivationStart(), 0); metric.entries = [entry]; report(); } - }); + } }; const po = observe('largest-contentful-paint', handleEntries); @@ -77,31 +80,29 @@ export const onLCP = (onReport: (metric: LCPMetric) => void, opts: ReportOpts = if (po) { report = bindReporter(onReport, metric, LCPThresholds, opts.reportAllChanges); + // Ensure this logic only runs once, since it can be triggered from + // any of three different event listeners below. const stopListening = runOnce(() => { - if (!reportedMetricIDs[metric.id]) { - handleEntries(po.takeRecords() as LCPMetric['entries']); - po.disconnect(); - reportedMetricIDs[metric.id] = true; - report(true); - } + handleEntries(po.takeRecords() as LCPMetric['entries']); + po.disconnect(); + report(true); }); - // Stop listening after input. Note: while scrolling is an input that - // stops LCP observation, it's unreliable since it can be programmatically - // generated. See: https://github.com/GoogleChrome/web-vitals/issues/75 - ['keydown', 'click'].forEach(type => { - // Wrap in a setTimeout so the callback is run in a separate task - // to avoid extending the keyboard/click handler to reduce INP impact + // Stop listening after input or visibilitychange. + // Note: while scrolling is an input that stops LCP observation, it's + // unreliable since it can be programmatically generated. + // See: https://github.com/GoogleChrome/web-vitals/issues/75 + for (const type of ['keydown', 'click', 'visibilitychange']) { + // Wrap the listener in an idle callback so it's run in a separate + // task to reduce potential INP impact. // https://github.com/GoogleChrome/web-vitals/issues/383 if (WINDOW.document) { - addEventListener(type, () => whenIdle(stopListening as () => void), { - once: true, + addEventListener(type, () => whenIdleOrHidden(stopListening), { capture: true, + once: true, }); } - }); - - onHidden(stopListening); + } } }); }; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/InteractionManager.ts b/packages/browser-utils/src/metrics/web-vitals/lib/InteractionManager.ts new file mode 100644 index 000000000000..033cdb2cb836 --- /dev/null +++ b/packages/browser-utils/src/metrics/web-vitals/lib/InteractionManager.ts @@ -0,0 +1,151 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getInteractionCount } from './polyfills/interactionCountPolyfill.js'; + +export interface Interaction { + _latency: number; + // While the `id` and `entries` properties are also internal and could be + // mangled by prefixing with an underscore, since they correspond to public + // symbols there is no need to mangle them as the library will compress + // better if we reuse the existing names. + id: number; + entries: PerformanceEventTiming[]; +} + +// To prevent unnecessary memory usage on pages with lots of interactions, +// store at most 10 of the longest interactions to consider as INP candidates. +const MAX_INTERACTIONS_TO_CONSIDER = 10; + +// Used to store the interaction count after a bfcache restore, since p98 +// interaction latencies should only consider the current navigation. +let prevInteractionCount = 0; + +/** + * Returns the interaction count since the last bfcache restore (or for the + * full page lifecycle if there were no bfcache restores). + */ +const getInteractionCountForNavigation = () => { + return getInteractionCount() - prevInteractionCount; +}; + +/** + * + */ +export class InteractionManager { + /** + * A list of longest interactions on the page (by latency) sorted so the + * longest one is first. The list is at most MAX_INTERACTIONS_TO_CONSIDER + * long. + */ + // eslint-disable-next-line @sentry-internal/sdk/no-class-field-initializers, @typescript-eslint/explicit-member-accessibility + _longestInteractionList: Interaction[] = []; + + /** + * A mapping of longest interactions by their interaction ID. + * This is used for faster lookup. + */ + // eslint-disable-next-line @sentry-internal/sdk/no-class-field-initializers, @typescript-eslint/explicit-member-accessibility + _longestInteractionMap: Map = new Map(); + + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + _onBeforeProcessingEntry?: (entry: PerformanceEventTiming) => void; + + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + _onAfterProcessingINPCandidate?: (interaction: Interaction) => void; + + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility, jsdoc/require-jsdoc + _resetInteractions() { + prevInteractionCount = getInteractionCount(); + this._longestInteractionList.length = 0; + this._longestInteractionMap.clear(); + } + + /** + * Returns the estimated p98 longest interaction based on the stored + * interaction candidates and the interaction count for the current page. + */ + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + _estimateP98LongestInteraction() { + const candidateInteractionIndex = Math.min( + this._longestInteractionList.length - 1, + Math.floor(getInteractionCountForNavigation() / 50), + ); + + return this._longestInteractionList[candidateInteractionIndex]; + } + + /** + * Takes a performance entry and adds it to the list of worst interactions + * if its duration is long enough to make it among the worst. If the + * entry is part of an existing interaction, it is merged and the latency + * and entries list is updated as needed. + */ + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + _processEntry(entry: PerformanceEventTiming) { + this._onBeforeProcessingEntry?.(entry); + + // Skip further processing for entries that cannot be INP candidates. + if (!(entry.interactionId || entry.entryType === 'first-input')) return; + + // The least-long of the 10 longest interactions. + const minLongestInteraction = this._longestInteractionList.at(-1); + + let interaction = this._longestInteractionMap.get(entry.interactionId!); + + // Only process the entry if it's possibly one of the ten longest, + // or if it's part of an existing interaction. + if ( + interaction || + this._longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER || + // If the above conditions are false, `minLongestInteraction` will be set. + entry.duration > minLongestInteraction!._latency + ) { + // If the interaction already exists, update it. Otherwise create one. + if (interaction) { + // If the new entry has a longer duration, replace the old entries, + // otherwise add to the array. + if (entry.duration > interaction._latency) { + interaction.entries = [entry]; + interaction._latency = entry.duration; + } else if (entry.duration === interaction._latency && entry.startTime === interaction.entries[0]!.startTime) { + interaction.entries.push(entry); + } + } else { + interaction = { + id: entry.interactionId!, + entries: [entry], + _latency: entry.duration, + }; + this._longestInteractionMap.set(interaction.id, interaction); + this._longestInteractionList.push(interaction); + } + + // Sort the entries by latency (descending) and keep only the top ten. + this._longestInteractionList.sort((a, b) => b._latency - a._latency); + if (this._longestInteractionList.length > MAX_INTERACTIONS_TO_CONSIDER) { + const removedInteractions = this._longestInteractionList.splice(MAX_INTERACTIONS_TO_CONSIDER); + + for (const interaction of removedInteractions) { + this._longestInteractionMap.delete(interaction.id); + } + } + + // Call any post-processing on the interaction + this._onAfterProcessingINPCandidate?.(interaction); + } + } +} diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/LCPEntryManager.ts b/packages/browser-utils/src/metrics/web-vitals/lib/LCPEntryManager.ts new file mode 100644 index 000000000000..752c6c41469b --- /dev/null +++ b/packages/browser-utils/src/metrics/web-vitals/lib/LCPEntryManager.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// eslint-disable-next-line jsdoc/require-jsdoc +export class LCPEntryManager { + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + _onBeforeProcessingEntry?: (entry: LargestContentfulPaint) => void; + + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility, jsdoc/require-jsdoc + _processEntry(entry: LargestContentfulPaint) { + this._onBeforeProcessingEntry?.(entry); + } +} diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/LayoutShiftManager.ts b/packages/browser-utils/src/metrics/web-vitals/lib/LayoutShiftManager.ts new file mode 100644 index 000000000000..76de0eb8290c --- /dev/null +++ b/packages/browser-utils/src/metrics/web-vitals/lib/LayoutShiftManager.ts @@ -0,0 +1,55 @@ +/* eslint-disable jsdoc/require-jsdoc */ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class LayoutShiftManager { + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + _onAfterProcessingUnexpectedShift?: (entry: LayoutShift) => void; + + // eslint-disable-next-line @sentry-internal/sdk/no-class-field-initializers, @typescript-eslint/explicit-member-accessibility + _sessionValue = 0; + // eslint-disable-next-line @sentry-internal/sdk/no-class-field-initializers, @typescript-eslint/explicit-member-accessibility + _sessionEntries: LayoutShift[] = []; + + // eslint-disable-next-line @typescript-eslint/explicit-member-accessibility + _processEntry(entry: LayoutShift) { + // Only count layout shifts without recent user input. + if (entry.hadRecentInput) return; + + const firstSessionEntry = this._sessionEntries[0]; + const lastSessionEntry = this._sessionEntries.at(-1); + + // If the entry occurred less than 1 second after the previous entry + // and less than 5 seconds after the first entry in the session, + // include the entry in the current session. Otherwise, start a new + // session. + if ( + this._sessionValue && + firstSessionEntry && + lastSessionEntry && + entry.startTime - lastSessionEntry.startTime < 1000 && + entry.startTime - firstSessionEntry.startTime < 5000 + ) { + this._sessionValue += entry.value; + this._sessionEntries.push(entry); + } else { + this._sessionValue = entry.value; + this._sessionEntries = [entry]; + } + + this._onAfterProcessingUnexpectedShift?.(entry); + } +} diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/bindReporter.ts b/packages/browser-utils/src/metrics/web-vitals/lib/bindReporter.ts index 43fdc8d9e541..2eba91d9effb 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/bindReporter.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/bindReporter.ts @@ -37,7 +37,7 @@ export const bindReporter = ( return (forceReport?: boolean) => { if (metric.value >= 0) { if (forceReport || reportAllChanges) { - delta = metric.value - (prevValue || 0); + delta = metric.value - (prevValue ?? 0); // Report the metric if there's a non-zero delta or if no previous // value exists (which can happen in the case of the document becoming diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/generateUniqueID.ts b/packages/browser-utils/src/metrics/web-vitals/lib/generateUniqueID.ts index 637d01398e0a..983ebc81ea4a 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/generateUniqueID.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/generateUniqueID.ts @@ -20,5 +20,5 @@ * @return {string} */ export const generateUniqueID = () => { - return `v4-${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`; + return `v5-${Date.now()}-${Math.floor(Math.random() * (9e12 - 1)) + 1e12}`; }; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/getActivationStart.ts b/packages/browser-utils/src/metrics/web-vitals/lib/getActivationStart.ts index 4bdafc0c718c..33677466faf9 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/getActivationStart.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/getActivationStart.ts @@ -18,5 +18,5 @@ import { getNavigationEntry } from './getNavigationEntry'; export const getActivationStart = (): number => { const navEntry = getNavigationEntry(); - return navEntry?.activationStart || 0; + return navEntry?.activationStart ?? 0; }; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/getNavigationEntry.ts b/packages/browser-utils/src/metrics/web-vitals/lib/getNavigationEntry.ts index f2c85f6127bc..77c68999b918 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/getNavigationEntry.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/getNavigationEntry.ts @@ -21,12 +21,12 @@ import { WINDOW } from '../../../types'; export const getNavigationEntry = (checkResponseStart = true): PerformanceNavigationTiming | void => { const navigationEntry = WINDOW.performance?.getEntriesByType?.('navigation')[0]; // Check to ensure the `responseStart` property is present and valid. - // In some cases no value is reported by the browser (for + // In some cases a zero value is reported by the browser (for // privacy/security reasons), and in other cases (bugs) the value is // negative or is larger than the current page time. Ignore these cases: - // https://github.com/GoogleChrome/web-vitals/issues/137 - // https://github.com/GoogleChrome/web-vitals/issues/162 - // https://github.com/GoogleChrome/web-vitals/issues/275 + // - https://github.com/GoogleChrome/web-vitals/issues/137 + // - https://github.com/GoogleChrome/web-vitals/issues/162 + // - https://github.com/GoogleChrome/web-vitals/issues/275 if ( // sentry-specific change: // We don't want to check for responseStart for our own use of `getNavigationEntry` diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts b/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts index b658be9475e9..3a6c0a2e42a9 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/getVisibilityWatcher.ts @@ -15,6 +15,7 @@ */ import { WINDOW } from '../../../types'; +import { getActivationStart } from './getActivationStart'; let firstHiddenTime = -1; @@ -24,7 +25,7 @@ const initHiddenTime = () => { // that visibility state is always 'hidden' during prerendering, so we have // to ignore that case until prerendering finishes (see: `prerenderingchange` // event logic below). - return WINDOW.document!.visibilityState === 'hidden' && !WINDOW.document!.prerendering ? 0 : Infinity; + return WINDOW.document?.visibilityState === 'hidden' && !WINDOW.document?.prerendering ? 0 : Infinity; }; const onVisibilityUpdate = (event: Event) => { @@ -61,11 +62,22 @@ const removeChangeListeners = () => { export const getVisibilityWatcher = () => { if (WINDOW.document && firstHiddenTime < 0) { - // If the document is hidden when this code runs, assume it was hidden - // since navigation start. This isn't a perfect heuristic, but it's the - // best we can do until an API is available to support querying past - // visibilityState. - firstHiddenTime = initHiddenTime(); + // Check if we have a previous hidden `visibility-state` performance entry. + const activationStart = getActivationStart(); + const firstVisibilityStateHiddenTime = !WINDOW.document.prerendering + ? globalThis.performance + .getEntriesByType('visibility-state') + .filter(e => e.name === 'hidden' && e.startTime > activationStart)[0]?.startTime + : undefined; + + // Prefer that, but if it's not available and the document is hidden when + // this code runs, assume it was hidden since navigation start. This isn't + // a perfect heuristic, but it's the best we can do until the + // `visibility-state` performance entry becomes available in all browsers. + firstHiddenTime = firstVisibilityStateHiddenTime ?? initHiddenTime(); + // We're still going to listen to for changes so we can handle things like + // bfcache restores and/or prerender without having to examine individual + // timestamps in detail. addChangeListeners(); } return { diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/initMetric.ts b/packages/browser-utils/src/metrics/web-vitals/lib/initMetric.ts index b2cfbc609a25..8771a5966c9f 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/initMetric.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/initMetric.ts @@ -20,7 +20,7 @@ import { generateUniqueID } from './generateUniqueID'; import { getActivationStart } from './getActivationStart'; import { getNavigationEntry } from './getNavigationEntry'; -export const initMetric = (name: MetricName, value?: number) => { +export const initMetric = (name: MetricName, value: number = -1) => { const navEntry = getNavigationEntry(); let navigationType: MetricType['navigationType'] = 'navigate'; @@ -39,7 +39,7 @@ export const initMetric = (name: MetricNa return { name, - value: typeof value === 'undefined' ? -1 : value, + value, rating: 'good' as const, // If needed, will be updated when reported. `const` to keep the type from widening to `string`. delta: 0, entries, diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/initUnique.ts b/packages/browser-utils/src/metrics/web-vitals/lib/initUnique.ts new file mode 100644 index 000000000000..1eda48705b08 --- /dev/null +++ b/packages/browser-utils/src/metrics/web-vitals/lib/initUnique.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const instanceMap: WeakMap = new WeakMap(); + +/** + * A function that accepts and identity object and a class object and returns + * either a new instance of that class or an existing instance, if the + * identity object was previously used. + */ +export function initUnique(identityObj: object, ClassObj: new () => T): T { + if (!instanceMap.get(identityObj)) { + instanceMap.set(identityObj, new ClassObj()); + } + return instanceMap.get(identityObj)! as T; +} diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts b/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts index ad71468b6fb6..9af0116cd0b1 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/observe.ts @@ -41,7 +41,7 @@ interface PerformanceEntryMap { export const observe = ( type: K, callback: (entries: PerformanceEntryMap[K]) => void, - opts?: PerformanceObserverInit, + opts: PerformanceObserverInit = {}, ): PerformanceObserver | undefined => { try { if (PerformanceObserver.supportedEntryTypes.includes(type)) { @@ -54,18 +54,10 @@ export const observe = ( callback(list.getEntries() as PerformanceEntryMap[K]); }); }); - po.observe( - Object.assign( - { - type, - buffered: true, - }, - opts || {}, - ) as PerformanceObserverInit, - ); + po.observe({ type, buffered: true, ...opts }); return po; } - } catch (e) { + } catch { // Do nothing. } return; diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts b/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts index f1640d4fcdac..1844a616a479 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/onHidden.ts @@ -21,13 +21,13 @@ export interface OnHiddenCallback { } // Sentry-specific change: -// This function's logic was NOT updated to web-vitals 4.2.4 but we continue -// to use the web-vitals 3.5.2 due to us having stricter browser support. +// This function's logic was NOT updated to web-vitals 4.2.4 or 5.x but we continue +// to use the web-vitals 3.5.2 versiondue to us having stricter browser support. // PR with context that made the changes: https://github.com/GoogleChrome/web-vitals/pull/442/files#r1530492402 // The PR removed listening to the `pagehide` event, in favour of only listening to `visibilitychange` event. -// This is "more correct" but some browsers we still support (Safari 12.1-14.0) don't fully support `visibilitychange` +// This is "more correct" but some browsers we still support (Safari <14.4) don't fully support `visibilitychange` // or have known bugs w.r.t the `visibilitychange` event. -// TODO (v9): If we decide to drop support for Safari 12.1-14.0, we can use the logic from web-vitals 4.2.4 +// TODO (v10): If we decide to drop support for Safari 14.4, we can use the logic from web-vitals 4.2.4 // In this case, we also need to update the integration tests that currently trigger the `pagehide` event to // simulate the page being hidden. export const onHidden = (cb: OnHiddenCallback) => { diff --git a/packages/browser-utils/src/metrics/web-vitals/lib/whenIdle.ts b/packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts similarity index 67% rename from packages/browser-utils/src/metrics/web-vitals/lib/whenIdle.ts rename to packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts index 8914c45d7bb3..32dae5f30f8b 100644 --- a/packages/browser-utils/src/metrics/web-vitals/lib/whenIdle.ts +++ b/packages/browser-utils/src/metrics/web-vitals/lib/whenIdleOrHidden.ts @@ -14,27 +14,28 @@ * limitations under the License. */ -import { WINDOW } from '../../../types'; -import { onHidden } from './onHidden'; -import { runOnce } from './runOnce'; +import { WINDOW } from '../../../types.js'; +import { onHidden } from './onHidden.js'; +import { runOnce } from './runOnce.js'; /** * Runs the passed callback during the next idle period, or immediately * if the browser's visibility state is (or becomes) hidden. */ -export const whenIdle = (cb: () => void): number => { +export const whenIdleOrHidden = (cb: () => void) => { const rIC = WINDOW.requestIdleCallback || WINDOW.setTimeout; - let handle = -1; - // eslint-disable-next-line no-param-reassign - cb = runOnce(cb) as () => void; // If the document is hidden, run the callback immediately, otherwise // race an idle callback with the next `visibilitychange` event. if (WINDOW.document?.visibilityState === 'hidden') { cb(); } else { - handle = rIC(cb); + // eslint-disable-next-line no-param-reassign + cb = runOnce(cb); + rIC(cb); + // sentry: we use onHidden instead of directly listening to visibilitychange + // because some browsers we still support (Safari <14.4) don't fully support + // `visibilitychange` or have known bugs w.r.t the `visibilitychange` event. onHidden(cb); } - return handle; }; diff --git a/packages/browser-utils/src/metrics/web-vitals/onFCP.ts b/packages/browser-utils/src/metrics/web-vitals/onFCP.ts index d01001ad48ec..12fd51e29ef7 100644 --- a/packages/browser-utils/src/metrics/web-vitals/onFCP.ts +++ b/packages/browser-utils/src/metrics/web-vitals/onFCP.ts @@ -38,7 +38,7 @@ export const onFCP = (onReport: (metric: FCPMetric) => void, opts: ReportOpts = let report: ReturnType; const handleEntries = (entries: FCPMetric['entries']) => { - entries.forEach(entry => { + for (const entry of entries) { if (entry.name === 'first-contentful-paint') { po!.disconnect(); @@ -53,7 +53,7 @@ export const onFCP = (onReport: (metric: FCPMetric) => void, opts: ReportOpts = report(true); } } - }); + } }; const po = observe('paint', handleEntries); diff --git a/packages/browser-utils/src/metrics/web-vitals/onTTFB.ts b/packages/browser-utils/src/metrics/web-vitals/onTTFB.ts index 235895d093aa..4633b3cd83cb 100644 --- a/packages/browser-utils/src/metrics/web-vitals/onTTFB.ts +++ b/packages/browser-utils/src/metrics/web-vitals/onTTFB.ts @@ -36,7 +36,7 @@ const whenReady = (callback: () => void) => { addEventListener('load', () => whenReady(callback), true); } else { // Queue a task so the callback runs after `loadEventEnd`. - setTimeout(callback, 0); + setTimeout(callback); } }; diff --git a/packages/browser-utils/src/metrics/web-vitals/types.ts b/packages/browser-utils/src/metrics/web-vitals/types.ts index 5a17b811db96..033fbee09926 100644 --- a/packages/browser-utils/src/metrics/web-vitals/types.ts +++ b/packages/browser-utils/src/metrics/web-vitals/types.ts @@ -19,7 +19,7 @@ export * from './types/polyfills'; export * from './types/cls'; export * from './types/fcp'; -export * from './types/fid'; +export * from './types/fid'; // FIX was removed in 5.0.2 but we keep it around for now export * from './types/inp'; export * from './types/lcp'; export * from './types/ttfb'; @@ -65,7 +65,7 @@ declare global { // https://wicg.github.io/layout-instability/#sec-layout-shift-attribution interface LayoutShiftAttribution { - node?: Node; + node: Node | null; previousRect: DOMRectReadOnly; currentRect: DOMRectReadOnly; } @@ -87,9 +87,48 @@ declare global { readonly element: Element | null; } + // https://w3c.github.io/long-animation-frame/#sec-PerformanceLongAnimationFrameTiming + export type ScriptInvokerType = + | 'classic-script' + | 'module-script' + | 'event-listener' + | 'user-callback' + | 'resolve-promise' + | 'reject-promise'; + + // https://w3c.github.io/long-animation-frame/#sec-PerformanceLongAnimationFrameTiming + export type ScriptWindowAttribution = 'self' | 'descendant' | 'ancestor' | 'same-page' | 'other'; + + // https://w3c.github.io/long-animation-frame/#sec-PerformanceLongAnimationFrameTiming + interface PerformanceScriptTiming extends PerformanceEntry { + /* Overloading PerformanceEntry */ + readonly startTime: DOMHighResTimeStamp; + readonly duration: DOMHighResTimeStamp; + readonly name: string; + readonly entryType: string; + + readonly invokerType: ScriptInvokerType; + readonly invoker: string; + readonly executionStart: DOMHighResTimeStamp; + readonly sourceURL: string; + readonly sourceFunctionName: string; + readonly sourceCharPosition: number; + readonly pauseDuration: DOMHighResTimeStamp; + readonly forcedStyleAndLayoutDuration: DOMHighResTimeStamp; + readonly window?: Window; + readonly windowAttribution: ScriptWindowAttribution; + } + // https://w3c.github.io/long-animation-frame/#sec-PerformanceLongAnimationFrameTiming interface PerformanceLongAnimationFrameTiming extends PerformanceEntry { - renderStart: DOMHighResTimeStamp; - duration: DOMHighResTimeStamp; + readonly startTime: DOMHighResTimeStamp; + readonly duration: DOMHighResTimeStamp; + readonly name: string; + readonly entryType: string; + readonly renderStart: DOMHighResTimeStamp; + readonly styleAndLayoutStart: DOMHighResTimeStamp; + readonly blockingDuration: DOMHighResTimeStamp; + readonly firstUIEventTimestamp: DOMHighResTimeStamp; + readonly scripts: PerformanceScriptTiming[]; } } diff --git a/packages/browser-utils/src/metrics/web-vitals/types/base.ts b/packages/browser-utils/src/metrics/web-vitals/types/base.ts index 846744d96da5..d8315b817f4a 100644 --- a/packages/browser-utils/src/metrics/web-vitals/types/base.ts +++ b/packages/browser-utils/src/metrics/web-vitals/types/base.ts @@ -24,6 +24,7 @@ import type { TTFBMetric, TTFBMetricWithAttribution } from './ttfb'; export interface Metric { /** * The name of the metric (in acronym form). + * // sentry: re-added FID here since we continue supporting it for now */ name: 'CLS' | 'FCP' | 'FID' | 'INP' | 'LCP' | 'TTFB'; @@ -78,6 +79,7 @@ export interface Metric { } /** The union of supported metric types. */ +// sentry: re-added FIDMetric here since we continue supporting it for now export type MetricType = CLSMetric | FCPMetric | FIDMetric | INPMetric | LCPMetric | TTFBMetric; /** The union of supported metric attribution types. */ @@ -104,9 +106,21 @@ export type MetricWithAttribution = */ export type MetricRatingThresholds = [number, number]; +/** + * @deprecated Use metric-specific function types instead, such as: + * `(metric: LCPMetric) => void`. If a single callback type is needed for + * multiple metrics, use `(metric: MetricType) => void`. + */ +export interface ReportCallback { + (metric: MetricType): void; +} + export interface ReportOpts { reportAllChanges?: boolean; - durationThreshold?: number; +} + +export interface AttributionReportOpts extends ReportOpts { + generateTarget?: (el: Node | null) => string; } /** diff --git a/packages/browser-utils/src/metrics/web-vitals/types/cls.ts b/packages/browser-utils/src/metrics/web-vitals/types/cls.ts index 1d17c2d3eedb..5acaaa27c9ab 100644 --- a/packages/browser-utils/src/metrics/web-vitals/types/cls.ts +++ b/packages/browser-utils/src/metrics/web-vitals/types/cls.ts @@ -31,9 +31,10 @@ export interface CLSMetric extends Metric { */ export interface CLSAttribution { /** - * A selector identifying the first element (in document order) that - * shifted when the single largest layout shift contributing to the page's - * CLS score occurred. + * By default, a selector identifying the first element (in document order) + * that shifted when the single largest layout shift that contributed to the + * page's CLS score occurred. If the `generateTarget` configuration option + * was passed, then this will instead be the return value of that function. */ largestShiftTarget?: string; /** diff --git a/packages/browser-utils/src/metrics/web-vitals/types/inp.ts b/packages/browser-utils/src/metrics/web-vitals/types/inp.ts index c19be79a1ce0..e73743866301 100644 --- a/packages/browser-utils/src/metrics/web-vitals/types/inp.ts +++ b/packages/browser-utils/src/metrics/web-vitals/types/inp.ts @@ -14,7 +14,15 @@ * limitations under the License. */ -import type { LoadState, Metric } from './base'; +import type { AttributionReportOpts, LoadState, Metric, ReportOpts } from './base'; + +export interface INPReportOpts extends ReportOpts { + durationThreshold?: number; +} + +export interface INPAttributionReportOpts extends AttributionReportOpts { + durationThreshold?: number; +} /** * An INP-specific version of the Metric object. @@ -24,6 +32,22 @@ export interface INPMetric extends Metric { entries: PerformanceEventTiming[]; } +export interface INPLongestScriptSummary { + /** + * The longest Long Animation Frame script entry that intersects the INP + * interaction. + */ + entry: PerformanceScriptTiming; + /** + * The INP subpart where the longest script ran. + */ + subpart: 'input-delay' | 'processing-duration' | 'presentation-delay'; + /** + * The amount of time the longest script intersected the INP duration. + */ + intersectingDuration: number; +} + /** * An object containing potentially-helpful debugging information that * can be sent along with the INP value for the current page visit in order @@ -31,37 +55,20 @@ export interface INPMetric extends Metric { */ export interface INPAttribution { /** - * A selector identifying the element that the user first interacted with - * as part of the frame where the INP candidate interaction occurred. - * If this value is an empty string, that generally means the element was - * removed from the DOM after the interaction. + * By default, a selector identifying the element that the user first + * interacted with as part of the frame where the INP candidate interaction + * occurred. If this value is an empty string, that generally means the + * element was removed from the DOM after the interaction. If the + * `generateTarget` configuration option was passed, then this will instead + * be the return value of that function. */ interactionTarget: string; - /** - * A reference to the HTML element identified by `interactionTargetSelector`. - * NOTE: for attribution purpose, a selector identifying the element is - * typically more useful than the element itself. However, the element is - * also made available in case additional context is needed. - */ - interactionTargetElement: Node | undefined; /** * The time when the user first interacted during the frame where the INP * candidate interaction occurred (if more than one interaction occurred * within the frame, only the first time is reported). */ interactionTime: DOMHighResTimeStamp; - /** - * The best-guess timestamp of the next paint after the interaction. - * In general, this timestamp is the same as the `startTime + duration` of - * the event timing entry. However, since `duration` values are rounded to - * the nearest 8ms, it can sometimes appear that the paint occurred before - * processing ended (which cannot happen). This value clamps the paint time - * so it's always after `processingEnd` from the Event Timing API and - * `renderStart` from the Long Animation Frame API (where available). - * It also averages the duration values for all entries in the same - * animation frame, which should be closer to the "real" value. - */ - nextPaintTime: DOMHighResTimeStamp; /** * The type of interaction, based on the event type of the `event` entry * that corresponds to the interaction (i.e. the first `event` entry @@ -70,20 +77,19 @@ export interface INPAttribution { * and for "keydown" or "keyup" events this will be "keyboard". */ interactionType: 'pointer' | 'keyboard'; + /** + * The best-guess timestamp of the next paint after the interaction. + * In general, this timestamp is the same as the `startTime + duration` of + * the event timing entry. However, since duration values are rounded to the + * nearest 8ms (and can be rounded down), this value is clamped to always be + * reported after the processing times. + */ + nextPaintTime: DOMHighResTimeStamp; /** * An array of Event Timing entries that were processed within the same * animation frame as the INP candidate interaction. */ processedEventEntries: PerformanceEventTiming[]; - /** - * If the browser supports the Long Animation Frame API, this array will - * include any `long-animation-frame` entries that intersect with the INP - * candidate interaction's `startTime` and the `processingEnd` time of the - * last event processed within that animation frame. If the browser does not - * support the Long Animation Frame API or no `long-animation-frame` entries - * are detect, this array will be empty. - */ - longAnimationFrameEntries: PerformanceLongAnimationFrameTiming[]; /** * The time from when the user interacted with the page until when the * browser was first able to start processing event listeners for that @@ -112,6 +118,48 @@ export interface INPAttribution { * (e.g. usually in the `dom-interactive` phase) it can result in long delays. */ loadState: LoadState; + /** + * If the browser supports the Long Animation Frame API, this array will + * include any `long-animation-frame` entries that intersect with the INP + * candidate interaction's `startTime` and the `processingEnd` time of the + * last event processed within that animation frame. If the browser does not + * support the Long Animation Frame API or no `long-animation-frame` entries + * are detected, this array will be empty. + */ + longAnimationFrameEntries: PerformanceLongAnimationFrameTiming[]; + /** + * Summary information about the longest script entry intersecting the INP + * duration. Note, only script entries above 5 milliseconds are reported by + * the Long Animation Frame API. + */ + longestScript?: INPLongestScriptSummary; + /** + * The total duration of Long Animation Frame scripts that intersect the INP + * duration excluding any forced style and layout (that is included in + * totalStyleAndLayout). Note, this is limited to scripts > 5 milliseconds. + */ + totalScriptDuration?: number; + /** + * The total style and layout duration from any Long Animation Frames + * intersecting the INP interaction. This includes any end-of-frame style and + * layout duration + any forced style and layout duration. + */ + totalStyleAndLayoutDuration?: number; + /** + * The off main-thread presentation delay from the end of the last Long + * Animation Frame (where available) until the INP end point. + */ + totalPaintDuration?: number; + /** + * The total unattributed time not included in any of the previous totals. + * This includes scripts < 5 milliseconds and other timings not attributed + * by Long Animation Frame (including when a frame is < 50ms and so has no + * Long Animation Frame). + * When no Long Animation Frames are present this will be undefined, rather + * than everything being unattributed to make it clearer when it's expected + * to be small. + */ + totalUnattributedDuration?: number; } /** diff --git a/packages/browser-utils/src/metrics/web-vitals/types/lcp.ts b/packages/browser-utils/src/metrics/web-vitals/types/lcp.ts index 2dd5ea34f798..293531b3d45c 100644 --- a/packages/browser-utils/src/metrics/web-vitals/types/lcp.ts +++ b/packages/browser-utils/src/metrics/web-vitals/types/lcp.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { Metric } from './base'; +import type { Metric } from './base.js'; /** * An LCP-specific version of the Metric object. @@ -31,9 +31,12 @@ export interface LCPMetric extends Metric { */ export interface LCPAttribution { /** - * The element corresponding to the largest contentful paint for the page. + * By default, a selector identifying the element corresponding to the + * largest contentful paint for the page. If the `generateTarget` + * configuration option was passed, then this will instead be the return + * value of that function. */ - element?: string; + target?: string; /** * The URL (if applicable) of the LCP image resource. If the LCP element * is a text node, this value will not be set. From 6b656b4009f8f8ec51f379ed517494a0deaaaeef Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 12 Jun 2025 13:35:21 +0200 Subject: [PATCH 19/32] fix(vue): Ensure root component render span always ends (#16488) This PR fixes a bug discovered in https://github.com/getsentry/sentry-javascript/pull/16486#discussion_r2128214297 where the root component span would not end correctly if `trackComponents` was `false`. Also added a comment to explain the purpose of the root component `ui.vue.render` span. We might want to look into renaming or removing this span in the future but for now, let's fix the behaviour. --- packages/vue/src/tracing.ts | 18 ++++++--- .../vue/test/tracing/tracingMixin.test.ts | 39 ++++++++++--------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index 5aadfdd876be..202face147d9 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -31,8 +31,8 @@ const HOOKS: { [key in Operation]: Hook[] } = { update: ['beforeUpdate', 'updated'], }; -/** Finish top-level component span and activity with a debounce configured using `timeout` option */ -function finishRootComponentSpan(vm: VueSentry, timestamp: number, timeout: number): void { +/** End the top-level component span and activity with a debounce configured using `timeout` option */ +function maybeEndRootComponentSpan(vm: VueSentry, timestamp: number, timeout: number): void { if (vm.$_sentryRootComponentSpanTimer) { clearTimeout(vm.$_sentryRootComponentSpanTimer); } @@ -66,6 +66,8 @@ export const createTracingMixins = (options: Partial = {}): Mixi const mixins: Mixins = {}; + const rootComponentSpanFinalTimeout = options.timeout || 2000; + for (const operation of hooks) { // Retrieve corresponding hooks from Vue lifecycle. // eg. mount => ['beforeMount', 'mounted'] @@ -91,6 +93,9 @@ export const createTracingMixins = (options: Partial = {}): Mixi }, onlyIfParent: true, }); + + // call debounced end function once directly, just in case no child components call it + maybeEndRootComponentSpan(this, timestampInSeconds(), rootComponentSpanFinalTimeout); } // 2. Component tracking filter @@ -102,7 +107,10 @@ export const createTracingMixins = (options: Partial = {}): Mixi ? findTrackComponent(options.trackComponents, componentName) : options.trackComponents); + // We always want to track root component if (!shouldTrack) { + // even if we don't track `this` component, we still want to end the root span eventually + maybeEndRootComponentSpan(this, timestampInSeconds(), rootComponentSpanFinalTimeout); return; } @@ -117,7 +125,7 @@ export const createTracingMixins = (options: Partial = {}): Mixi if (activeSpan) { // Cancel any existing span for this operation (safety measure) // We're actually not sure if it will ever be the case that cleanup hooks were not called. - // However, we had users report that spans didn't get finished, so we finished the span before + // However, we had users report that spans didn't end, so we end the span before // starting a new one, just to be sure. const oldSpan = this.$_sentryComponentSpans[operation]; if (oldSpan) { @@ -142,8 +150,8 @@ export const createTracingMixins = (options: Partial = {}): Mixi if (!span) return; // Skip if no span was created in the "before" hook span.end(); - // For any "after" hook, also schedule the root component span to finish - finishRootComponentSpan(this, timestampInSeconds(), options.timeout || 2000); + // For any "after" hook, also schedule the root component span to end + maybeEndRootComponentSpan(this, timestampInSeconds(), rootComponentSpanFinalTimeout); } }; } diff --git a/packages/vue/test/tracing/tracingMixin.test.ts b/packages/vue/test/tracing/tracingMixin.test.ts index d67690271ed2..2c08a20c61cd 100644 --- a/packages/vue/test/tracing/tracingMixin.test.ts +++ b/packages/vue/test/tracing/tracingMixin.test.ts @@ -27,11 +27,10 @@ vi.mock('../../src/vendor/components', () => { }; }); -const mockSpanFactory = (): { name?: string; op?: string; end: Mock; startChild: Mock } => ({ +const mockSpanFactory = (): { name?: string; op?: string; end: Mock } => ({ name: undefined, op: undefined, end: vi.fn(), - startChild: vi.fn(), }); vi.useFakeTimers(); @@ -127,23 +126,25 @@ describe('Vue Tracing Mixins', () => { ); }); - it('should finish root component span on timer after component spans end', () => { - // todo/fixme: This root component span is only finished if trackComponents is true --> it should probably be always finished - const mixins = createTracingMixins({ trackComponents: true, timeout: 1000 }); - const rootMockSpan = mockSpanFactory(); - mockRootInstance.$_sentryRootComponentSpan = rootMockSpan; - - // Create and finish a component span - mixins.beforeMount.call(mockVueInstance); - mixins.mounted.call(mockVueInstance); - - // Root component span should not end immediately - expect(rootMockSpan.end).not.toHaveBeenCalled(); - - // After timeout, root component span should end - vi.advanceTimersByTime(1001); - expect(rootMockSpan.end).toHaveBeenCalled(); - }); + it.each([true, false])( + 'should finish root component span on timer after component spans end, if trackComponents is %s', + trackComponents => { + const mixins = createTracingMixins({ trackComponents, timeout: 1000 }); + const rootMockSpan = mockSpanFactory(); + mockRootInstance.$_sentryRootComponentSpan = rootMockSpan; + + // Create and finish a component span + mixins.beforeMount.call(mockVueInstance); + mixins.mounted.call(mockVueInstance); + + // Root component span should not end immediately + expect(rootMockSpan.end).not.toHaveBeenCalled(); + + // After timeout, root component span should end + vi.advanceTimersByTime(1001); + expect(rootMockSpan.end).toHaveBeenCalled(); + }, + ); }); describe('Component Span Lifecycle', () => { From cbfada0b10adfefb3e686bfedde4b96e7fb52b57 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 12 Jun 2025 15:57:28 +0200 Subject: [PATCH 20/32] test(node): Fix nestjs-11 E2E test by pinning version See https://github.com/nestjs/nest/issues/15273 --- .../e2e-tests/test-applications/nestjs-11/package.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/package.json b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json index 9ba374954190..f5302ee3531e 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json @@ -44,5 +44,10 @@ "ts-loader": "^9.4.3", "tsconfig-paths": "^4.2.0", "typescript": "~5.0.0" + }, + "pnpm": { + "overrides": { + "minimatch": "10.0.1" + } } } From e26e33426bc2a848325331ea3b5100542fb88438 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 12 Jun 2025 16:04:10 +0200 Subject: [PATCH 21/32] test(trpc): Fix E2E test by pinning to 11.3.0 See: https://github.com/trpc/trpc/issues/6821 --- .../e2e-tests/test-applications/nextjs-t3/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json b/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json index 94da7baed3ab..c9c47cc7ce54 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-t3/package.json @@ -17,9 +17,9 @@ "@sentry/nextjs": "latest || *", "@t3-oss/env-nextjs": "^0.10.1", "@tanstack/react-query": "^5.50.0", - "@trpc/client": "^11.0.0-rc.446", - "@trpc/react-query": "^11.0.0-rc.446", - "@trpc/server": "^11.0.0-rc.446", + "@trpc/client": "~11.3.0", + "@trpc/react-query": "~11.3.0", + "@trpc/server": "~11.3.0", "geist": "^1.3.0", "next": "14.2.29", "react": "18.3.1", From 6882abfa01ff88df49737dd0839104257d3970c7 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 12 Jun 2025 13:39:35 +0200 Subject: [PATCH 22/32] meta(changelog): Update changelog for 9.29.0 --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40e6d2189e3a..e70a011716be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,23 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 9.29.0 + +### Important Changes + +- **feat(browser): Update `web-vitals` to 5.0.2 ([#16492](https://github.com/getsentry/sentry-javascript/pull/16492))** + +This release upgrades the `web-vitals` library to version 5.0.2. This upgrade could slightly change the collected web vital values and potentially also influence alerts and performance scores in the Sentry UI. + +### Other Changes + +- feat(deps): Bump @sentry/rollup-plugin from 3.4.0 to 3.5.0 ([#16524](https://github.com/getsentry/sentry-javascript/pull/16524)) +- feat(ember): Stop warning for `onError` usage ([#16547](https://github.com/getsentry/sentry-javascript/pull/16547)) +- feat(node): Allow to force activate `vercelAiIntegration` ([#16551](https://github.com/getsentry/sentry-javascript/pull/16551)) +- feat(node): Introduce `ignoreLayersType` option to koa integration ([#16553](https://github.com/getsentry/sentry-javascript/pull/16553)) +- fix(browser): Ensure `suppressTracing` does not leak when async ([#16545](https://github.com/getsentry/sentry-javascript/pull/16545)) +- fix(vue): Ensure root component render span always ends ([#16488](https://github.com/getsentry/sentry-javascript/pull/16488)) + ## 9.28.1 - feat(deps): Bump @sentry/cli from 2.45.0 to 2.46.0 ([#16516](https://github.com/getsentry/sentry-javascript/pull/16516)) From 65d6bd4313ec06b809190125d18005374cbf2922 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 12 Jun 2025 11:08:54 -0400 Subject: [PATCH 23/32] chore(deps): Upgrade tpml to 1.0.5 (#16512) resolves https://linear.app/getsentry/issue/FE-483 --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 02a6f885056b..0deb47ad0828 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27707,9 +27707,9 @@ tmp@^0.2.1, tmp@~0.2.1: rimraf "^3.0.0" tmpl@1.0.x: - version "1.0.4" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" - integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= + version "1.0.5" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== to-object-path@^0.3.0: version "0.3.0" From 6d70326ddb6c62224c3d1a547753dcff830566cc Mon Sep 17 00:00:00 2001 From: getsentry-bot Date: Thu, 12 Jun 2025 15:26:17 +0000 Subject: [PATCH 24/32] release: 9.29.0 --- .../browser-integration-tests/package.json | 4 ++-- .../bundle-analyzer-scenarios/package.json | 2 +- dev-packages/clear-cache-gh-action/package.json | 2 +- dev-packages/e2e-tests/package.json | 2 +- .../external-contributor-gh-action/package.json | 2 +- dev-packages/node-integration-tests/package.json | 8 ++++---- dev-packages/opentelemetry-v2-tests/package.json | 2 +- dev-packages/rollup-utils/package.json | 2 +- dev-packages/size-limit-gh-action/package.json | 2 +- dev-packages/test-utils/package.json | 4 ++-- lerna.json | 2 +- packages/angular/package.json | 6 +++--- packages/astro/package.json | 8 ++++---- packages/aws-serverless/package.json | 6 +++--- packages/browser-utils/package.json | 4 ++-- packages/browser/package.json | 14 +++++++------- packages/bun/package.json | 8 ++++---- packages/cloudflare/package.json | 4 ++-- packages/core/package.json | 2 +- packages/deno/package.json | 4 ++-- packages/ember/package.json | 6 +++--- packages/eslint-config-sdk/package.json | 6 +++--- packages/eslint-plugin-sdk/package.json | 2 +- packages/feedback/package.json | 4 ++-- packages/gatsby/package.json | 6 +++--- packages/google-cloud-serverless/package.json | 6 +++--- packages/integration-shims/package.json | 4 ++-- packages/nestjs/package.json | 6 +++--- packages/nextjs/package.json | 14 +++++++------- packages/node/package.json | 6 +++--- packages/nuxt/package.json | 12 ++++++------ packages/opentelemetry/package.json | 4 ++-- packages/profiling-node/package.json | 6 +++--- packages/react-router/package.json | 10 +++++----- packages/react/package.json | 6 +++--- packages/remix/package.json | 10 +++++----- packages/replay-canvas/package.json | 6 +++--- packages/replay-internal/package.json | 8 ++++---- packages/replay-worker/package.json | 2 +- packages/solid/package.json | 6 +++--- packages/solidstart/package.json | 10 +++++----- packages/svelte/package.json | 6 +++--- packages/sveltekit/package.json | 12 ++++++------ packages/tanstackstart-react/package.json | 12 ++++++------ packages/tanstackstart/package.json | 2 +- packages/types/package.json | 4 ++-- packages/typescript/package.json | 2 +- packages/vercel-edge/package.json | 6 +++--- packages/vue/package.json | 6 +++--- packages/wasm/package.json | 6 +++--- 50 files changed, 142 insertions(+), 142 deletions(-) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index d170cdc8b76c..77d92584c4d5 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-integration-tests", - "version": "9.28.1", + "version": "9.29.0", "main": "index.js", "license": "MIT", "engines": { @@ -42,7 +42,7 @@ "@babel/preset-typescript": "^7.16.7", "@playwright/test": "~1.50.0", "@sentry-internal/rrweb": "2.34.0", - "@sentry/browser": "9.28.1", + "@sentry/browser": "9.29.0", "@supabase/supabase-js": "2.49.3", "axios": "1.8.2", "babel-loader": "^8.2.2", diff --git a/dev-packages/bundle-analyzer-scenarios/package.json b/dev-packages/bundle-analyzer-scenarios/package.json index 580b10cb7359..aa7eab033496 100644 --- a/dev-packages/bundle-analyzer-scenarios/package.json +++ b/dev-packages/bundle-analyzer-scenarios/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/bundle-analyzer-scenarios", - "version": "9.28.1", + "version": "9.29.0", "description": "Scenarios to test bundle analysis with", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/dev-packages/bundle-analyzer-scenarios", diff --git a/dev-packages/clear-cache-gh-action/package.json b/dev-packages/clear-cache-gh-action/package.json index 9dd02dd0eb6f..cbd41272ea06 100644 --- a/dev-packages/clear-cache-gh-action/package.json +++ b/dev-packages/clear-cache-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/clear-cache-gh-action", "description": "An internal Github Action to clear GitHub caches.", - "version": "9.28.1", + "version": "9.29.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 841453da3f5b..b01f713f0854 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/e2e-tests", - "version": "9.28.1", + "version": "9.29.0", "license": "MIT", "private": true, "scripts": { diff --git a/dev-packages/external-contributor-gh-action/package.json b/dev-packages/external-contributor-gh-action/package.json index 56a45ea1f51a..f76e365958c6 100644 --- a/dev-packages/external-contributor-gh-action/package.json +++ b/dev-packages/external-contributor-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/external-contributor-gh-action", "description": "An internal Github Action to add external contributors to the CHANGELOG.md file.", - "version": "9.28.1", + "version": "9.29.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 18595a8e22c4..abc6e60288f0 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/node-integration-tests", - "version": "9.28.1", + "version": "9.29.0", "license": "MIT", "engines": { "node": ">=18" @@ -30,9 +30,9 @@ "@nestjs/common": "11.0.16", "@nestjs/core": "10.4.6", "@nestjs/platform-express": "10.4.6", - "@sentry/aws-serverless": "9.28.1", - "@sentry/core": "9.28.1", - "@sentry/node": "9.28.1", + "@sentry/aws-serverless": "9.29.0", + "@sentry/core": "9.29.0", + "@sentry/node": "9.29.0", "@types/mongodb": "^3.6.20", "@types/mysql": "^2.15.21", "@types/pg": "^8.6.5", diff --git a/dev-packages/opentelemetry-v2-tests/package.json b/dev-packages/opentelemetry-v2-tests/package.json index 8674b30b91de..847319e24ea5 100644 --- a/dev-packages/opentelemetry-v2-tests/package.json +++ b/dev-packages/opentelemetry-v2-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/opentelemetry-v2-tests", - "version": "9.28.1", + "version": "9.29.0", "private": true, "description": "Tests for @sentry/opentelemetry with OpenTelemetry v2", "engines": { diff --git a/dev-packages/rollup-utils/package.json b/dev-packages/rollup-utils/package.json index c469e0983f4e..e17cbe622826 100644 --- a/dev-packages/rollup-utils/package.json +++ b/dev-packages/rollup-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/rollup-utils", - "version": "9.28.1", + "version": "9.29.0", "description": "Rollup utilities used at Sentry for the Sentry JavaScript SDK", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/rollup-utils", diff --git a/dev-packages/size-limit-gh-action/package.json b/dev-packages/size-limit-gh-action/package.json index aeef6e7f48c0..a2d7499d1b0f 100644 --- a/dev-packages/size-limit-gh-action/package.json +++ b/dev-packages/size-limit-gh-action/package.json @@ -1,7 +1,7 @@ { "name": "@sentry-internal/size-limit-gh-action", "description": "An internal Github Action to compare the current size of a PR against the one on develop.", - "version": "9.28.1", + "version": "9.29.0", "license": "MIT", "engines": { "node": ">=18" diff --git a/dev-packages/test-utils/package.json b/dev-packages/test-utils/package.json index ea5f1cb7fd63..ba8509d936cf 100644 --- a/dev-packages/test-utils/package.json +++ b/dev-packages/test-utils/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "9.28.1", + "version": "9.29.0", "name": "@sentry-internal/test-utils", "author": "Sentry", "license": "MIT", @@ -45,7 +45,7 @@ }, "devDependencies": { "@playwright/test": "~1.50.0", - "@sentry/core": "9.28.1" + "@sentry/core": "9.29.0" }, "volta": { "extends": "../../package.json" diff --git a/lerna.json b/lerna.json index 9593d8699a43..68c0734ce9e0 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "9.28.1", + "version": "9.29.0", "npmClient": "yarn" } diff --git a/packages/angular/package.json b/packages/angular/package.json index 91a4f4fed00a..bd00adb107fc 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/angular", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for Angular", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/angular", @@ -21,8 +21,8 @@ "rxjs": "^6.5.5 || ^7.x" }, "dependencies": { - "@sentry/browser": "9.28.1", - "@sentry/core": "9.28.1", + "@sentry/browser": "9.29.0", + "@sentry/core": "9.29.0", "tslib": "^2.4.1" }, "devDependencies": { diff --git a/packages/astro/package.json b/packages/astro/package.json index 481e8a2c011d..043c2006a7b8 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/astro", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for Astro", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/astro", @@ -56,9 +56,9 @@ "astro": ">=3.x || >=4.0.0-beta || >=5.x" }, "dependencies": { - "@sentry/browser": "9.28.1", - "@sentry/core": "9.28.1", - "@sentry/node": "9.28.1", + "@sentry/browser": "9.29.0", + "@sentry/core": "9.29.0", + "@sentry/node": "9.29.0", "@sentry/vite-plugin": "^2.22.6" }, "devDependencies": { diff --git a/packages/aws-serverless/package.json b/packages/aws-serverless/package.json index 4650b1a10465..bb78eb8814e6 100644 --- a/packages/aws-serverless/package.json +++ b/packages/aws-serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/aws-serverless", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for AWS Lambda and AWS Serverless Environments", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/serverless", @@ -68,8 +68,8 @@ "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/instrumentation-aws-lambda": "0.50.3", "@opentelemetry/instrumentation-aws-sdk": "0.49.1", - "@sentry/core": "9.28.1", - "@sentry/node": "9.28.1", + "@sentry/core": "9.29.0", + "@sentry/node": "9.29.0", "@types/aws-lambda": "^8.10.62" }, "devDependencies": { diff --git a/packages/browser-utils/package.json b/packages/browser-utils/package.json index 03f47d8905eb..87d3be8f8389 100644 --- a/packages/browser-utils/package.json +++ b/packages/browser-utils/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/browser-utils", - "version": "9.28.1", + "version": "9.29.0", "description": "Browser Utilities for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser-utils", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.28.1" + "@sentry/core": "9.29.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/browser/package.json b/packages/browser/package.json index cacc29415ad3..92537631928a 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/browser", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for browsers", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/browser", @@ -39,14 +39,14 @@ "access": "public" }, "dependencies": { - "@sentry-internal/browser-utils": "9.28.1", - "@sentry-internal/feedback": "9.28.1", - "@sentry-internal/replay": "9.28.1", - "@sentry-internal/replay-canvas": "9.28.1", - "@sentry/core": "9.28.1" + "@sentry-internal/browser-utils": "9.29.0", + "@sentry-internal/feedback": "9.29.0", + "@sentry-internal/replay": "9.29.0", + "@sentry-internal/replay-canvas": "9.29.0", + "@sentry/core": "9.29.0" }, "devDependencies": { - "@sentry-internal/integration-shims": "9.28.1", + "@sentry-internal/integration-shims": "9.29.0", "fake-indexeddb": "^4.0.1" }, "scripts": { diff --git a/packages/bun/package.json b/packages/bun/package.json index 18a854f61da2..4210165a14f6 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/bun", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for bun", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/bun", @@ -39,9 +39,9 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.28.1", - "@sentry/node": "9.28.1", - "@sentry/opentelemetry": "9.28.1" + "@sentry/core": "9.29.0", + "@sentry/node": "9.29.0", + "@sentry/opentelemetry": "9.29.0" }, "devDependencies": { "bun-types": "^1.2.9" diff --git a/packages/cloudflare/package.json b/packages/cloudflare/package.json index 78671f1a4155..64a37d85ecc9 100644 --- a/packages/cloudflare/package.json +++ b/packages/cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/cloudflare", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for Cloudflare Workers and Pages", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/cloudflare", @@ -49,7 +49,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.28.1" + "@sentry/core": "9.29.0" }, "peerDependencies": { "@cloudflare/workers-types": "^4.x" diff --git a/packages/core/package.json b/packages/core/package.json index 0850ba0fb8f7..fce47b9aeb41 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/core", - "version": "9.28.1", + "version": "9.29.0", "description": "Base implementation for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/core", diff --git a/packages/deno/package.json b/packages/deno/package.json index 203131dda6ff..eeaeab8d79ad 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/deno", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for Deno", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/deno", @@ -24,7 +24,7 @@ "/build" ], "dependencies": { - "@sentry/core": "9.28.1" + "@sentry/core": "9.29.0" }, "scripts": { "deno-types": "node ./scripts/download-deno-types.mjs", diff --git a/packages/ember/package.json b/packages/ember/package.json index b48d6c0cdee1..7e6b9f53e4bf 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/ember", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for Ember.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/ember", @@ -32,8 +32,8 @@ "dependencies": { "@babel/core": "^7.24.4", "@embroider/macros": "^1.16.0", - "@sentry/browser": "9.28.1", - "@sentry/core": "9.28.1", + "@sentry/browser": "9.29.0", + "@sentry/core": "9.29.0", "ember-auto-import": "^2.7.2", "ember-cli-babel": "^8.2.0", "ember-cli-htmlbars": "^6.1.1", diff --git a/packages/eslint-config-sdk/package.json b/packages/eslint-config-sdk/package.json index 62954ddb27e6..515ef9da83ad 100644 --- a/packages/eslint-config-sdk/package.json +++ b/packages/eslint-config-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-config-sdk", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK eslint config", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-config-sdk", @@ -22,8 +22,8 @@ "access": "public" }, "dependencies": { - "@sentry-internal/eslint-plugin-sdk": "9.28.1", - "@sentry-internal/typescript": "9.28.1", + "@sentry-internal/eslint-plugin-sdk": "9.29.0", + "@sentry-internal/typescript": "9.29.0", "@typescript-eslint/eslint-plugin": "^5.48.0", "@typescript-eslint/parser": "^5.48.0", "eslint-config-prettier": "^6.11.0", diff --git a/packages/eslint-plugin-sdk/package.json b/packages/eslint-plugin-sdk/package.json index 7a92a90b467f..3869e7877d1b 100644 --- a/packages/eslint-plugin-sdk/package.json +++ b/packages/eslint-plugin-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/eslint-plugin-sdk", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK eslint plugin", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/eslint-plugin-sdk", diff --git a/packages/feedback/package.json b/packages/feedback/package.json index a60780ed2d0d..4eac24d986ab 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/feedback", - "version": "9.28.1", + "version": "9.29.0", "description": "Sentry SDK integration for user feedback", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/feedback", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.28.1" + "@sentry/core": "9.29.0" }, "devDependencies": { "preact": "^10.19.4" diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index d1373cd38b8b..44364ccad038 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/gatsby", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for Gatsby.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/gatsby", @@ -45,8 +45,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.28.1", - "@sentry/react": "9.28.1", + "@sentry/core": "9.29.0", + "@sentry/react": "9.29.0", "@sentry/webpack-plugin": "3.5.0" }, "peerDependencies": { diff --git a/packages/google-cloud-serverless/package.json b/packages/google-cloud-serverless/package.json index 8c30d92acfd8..072f1719f9f7 100644 --- a/packages/google-cloud-serverless/package.json +++ b/packages/google-cloud-serverless/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/google-cloud-serverless", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for Google Cloud Functions", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/google-cloud-serverless", @@ -48,8 +48,8 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.28.1", - "@sentry/node": "9.28.1", + "@sentry/core": "9.29.0", + "@sentry/node": "9.29.0", "@types/express": "^4.17.14" }, "devDependencies": { diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json index d4d75088f21f..51889334de55 100644 --- a/packages/integration-shims/package.json +++ b/packages/integration-shims/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/integration-shims", - "version": "9.28.1", + "version": "9.29.0", "description": "Shims for integrations in Sentry SDK.", "main": "build/cjs/index.js", "module": "build/esm/index.js", @@ -56,7 +56,7 @@ "url": "https://github.com/getsentry/sentry-javascript/issues" }, "dependencies": { - "@sentry/core": "9.28.1" + "@sentry/core": "9.29.0" }, "engines": { "node": ">=18" diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index bd3412ebe18e..7774e839d182 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nestjs", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for NestJS", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nestjs", @@ -49,8 +49,8 @@ "@opentelemetry/instrumentation": "0.57.2", "@opentelemetry/instrumentation-nestjs-core": "0.44.1", "@opentelemetry/semantic-conventions": "^1.34.0", - "@sentry/core": "9.28.1", - "@sentry/node": "9.28.1" + "@sentry/core": "9.29.0", + "@sentry/node": "9.29.0" }, "devDependencies": { "@nestjs/common": "^10.0.0", diff --git a/packages/nextjs/package.json b/packages/nextjs/package.json index 3c9b7909f5c2..70b216a795f4 100644 --- a/packages/nextjs/package.json +++ b/packages/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nextjs", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for Next.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nextjs", @@ -79,12 +79,12 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@rollup/plugin-commonjs": "28.0.1", - "@sentry-internal/browser-utils": "9.28.1", - "@sentry/core": "9.28.1", - "@sentry/node": "9.28.1", - "@sentry/opentelemetry": "9.28.1", - "@sentry/react": "9.28.1", - "@sentry/vercel-edge": "9.28.1", + "@sentry-internal/browser-utils": "9.29.0", + "@sentry/core": "9.29.0", + "@sentry/node": "9.29.0", + "@sentry/opentelemetry": "9.29.0", + "@sentry/react": "9.29.0", + "@sentry/vercel-edge": "9.29.0", "@sentry/webpack-plugin": "3.5.0", "chalk": "3.0.0", "resolve": "1.22.8", diff --git a/packages/node/package.json b/packages/node/package.json index 14889e197fa8..630d79f25594 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node", - "version": "9.28.1", + "version": "9.29.0", "description": "Sentry Node SDK using OpenTelemetry for performance instrumentation", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node", @@ -95,8 +95,8 @@ "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", "@prisma/instrumentation": "6.8.2", - "@sentry/core": "9.28.1", - "@sentry/opentelemetry": "9.28.1", + "@sentry/core": "9.29.0", + "@sentry/opentelemetry": "9.29.0", "import-in-the-middle": "^1.13.1", "minimatch": "^9.0.0" }, diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index 0cbea66817fc..902b6618804f 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/nuxt", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for Nuxt (EXPERIMENTAL)", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nuxt", @@ -43,13 +43,13 @@ }, "dependencies": { "@nuxt/kit": "^3.13.2", - "@sentry/browser": "9.28.1", - "@sentry/core": "9.28.1", - "@sentry/node": "9.28.1", - "@sentry/opentelemetry": "9.28.1", + "@sentry/browser": "9.29.0", + "@sentry/core": "9.29.0", + "@sentry/node": "9.29.0", + "@sentry/opentelemetry": "9.29.0", "@sentry/rollup-plugin": "3.5.0", "@sentry/vite-plugin": "3.2.4", - "@sentry/vue": "9.28.1" + "@sentry/vue": "9.29.0" }, "devDependencies": { "@nuxt/module-builder": "^0.8.4", diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index b4d8df9d9f38..0d613e764ffc 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/opentelemetry", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry utilities for OpenTelemetry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/opentelemetry", @@ -39,7 +39,7 @@ "access": "public" }, "dependencies": { - "@sentry/core": "9.28.1" + "@sentry/core": "9.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", diff --git a/packages/profiling-node/package.json b/packages/profiling-node/package.json index 16d89366f5a6..94b3c1b46e95 100644 --- a/packages/profiling-node/package.json +++ b/packages/profiling-node/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/profiling-node", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for Node.js Profiling", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/profiling-node", @@ -63,8 +63,8 @@ }, "dependencies": { "@sentry-internal/node-cpu-profiler": "^2.2.0", - "@sentry/core": "9.28.1", - "@sentry/node": "9.28.1" + "@sentry/core": "9.29.0", + "@sentry/node": "9.29.0" }, "devDependencies": { "@types/node": "^18.19.1" diff --git a/packages/react-router/package.json b/packages/react-router/package.json index 608e508ae530..b1be9a3212e4 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react-router", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for React Router (Framework)", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react-router", @@ -38,11 +38,11 @@ "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "0.57.2", "@opentelemetry/semantic-conventions": "^1.34.0", - "@sentry/browser": "9.28.1", + "@sentry/browser": "9.29.0", "@sentry/cli": "^2.46.0", - "@sentry/core": "9.28.1", - "@sentry/node": "9.28.1", - "@sentry/react": "9.28.1", + "@sentry/core": "9.29.0", + "@sentry/node": "9.29.0", + "@sentry/react": "9.29.0", "@sentry/vite-plugin": "^3.2.4", "glob": "11.0.1" }, diff --git a/packages/react/package.json b/packages/react/package.json index 7a9feadc4510..502a5e3c4a2c 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/react", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for React.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/react", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.28.1", - "@sentry/core": "9.28.1", + "@sentry/browser": "9.29.0", + "@sentry/core": "9.29.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { diff --git a/packages/remix/package.json b/packages/remix/package.json index f676ef676f4b..6b8ed269a792 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/remix", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for Remix", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/remix", @@ -69,10 +69,10 @@ "@opentelemetry/semantic-conventions": "^1.34.0", "@remix-run/router": "1.x", "@sentry/cli": "^2.46.0", - "@sentry/core": "9.28.1", - "@sentry/node": "9.28.1", - "@sentry/opentelemetry": "9.28.1", - "@sentry/react": "9.28.1", + "@sentry/core": "9.29.0", + "@sentry/node": "9.29.0", + "@sentry/opentelemetry": "9.29.0", + "@sentry/react": "9.29.0", "glob": "^10.3.4", "yargs": "^17.6.0" }, diff --git a/packages/replay-canvas/package.json b/packages/replay-canvas/package.json index d500f139c4db..1053628ab359 100644 --- a/packages/replay-canvas/package.json +++ b/packages/replay-canvas/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay-canvas", - "version": "9.28.1", + "version": "9.29.0", "description": "Replay canvas integration", "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", @@ -69,8 +69,8 @@ "@sentry-internal/rrweb": "2.35.0" }, "dependencies": { - "@sentry-internal/replay": "9.28.1", - "@sentry/core": "9.28.1" + "@sentry-internal/replay": "9.29.0", + "@sentry/core": "9.29.0" }, "engines": { "node": ">=18" diff --git a/packages/replay-internal/package.json b/packages/replay-internal/package.json index cde5138bade4..c3508dd8ee69 100644 --- a/packages/replay-internal/package.json +++ b/packages/replay-internal/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay", - "version": "9.28.1", + "version": "9.29.0", "description": "User replays for Sentry", "main": "build/npm/cjs/index.js", "module": "build/npm/esm/index.js", @@ -71,7 +71,7 @@ "homepage": "https://docs.sentry.io/platforms/javascript/session-replay/", "devDependencies": { "@babel/core": "^7.17.5", - "@sentry-internal/replay-worker": "9.28.1", + "@sentry-internal/replay-worker": "9.29.0", "@sentry-internal/rrweb": "2.35.0", "@sentry-internal/rrweb-snapshot": "2.35.0", "fflate": "0.8.2", @@ -79,8 +79,8 @@ "jsdom-worker": "^0.2.1" }, "dependencies": { - "@sentry-internal/browser-utils": "9.28.1", - "@sentry/core": "9.28.1" + "@sentry-internal/browser-utils": "9.29.0", + "@sentry/core": "9.29.0" }, "engines": { "node": ">=18" diff --git a/packages/replay-worker/package.json b/packages/replay-worker/package.json index b94a3eb5f648..787c716de040 100644 --- a/packages/replay-worker/package.json +++ b/packages/replay-worker/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/replay-worker", - "version": "9.28.1", + "version": "9.29.0", "description": "Worker for @sentry-internal/replay", "main": "build/esm/index.js", "module": "build/esm/index.js", diff --git a/packages/solid/package.json b/packages/solid/package.json index 56414d1944b0..02bafacfa10c 100644 --- a/packages/solid/package.json +++ b/packages/solid/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/solid", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for Solid", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solid", @@ -44,8 +44,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.28.1", - "@sentry/core": "9.28.1" + "@sentry/browser": "9.29.0", + "@sentry/core": "9.29.0" }, "peerDependencies": { "@solidjs/router": "^0.13.4", diff --git a/packages/solidstart/package.json b/packages/solidstart/package.json index 4f8eae7bd512..3c9692f5ecef 100644 --- a/packages/solidstart/package.json +++ b/packages/solidstart/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/solidstart", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for Solid Start", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/solidstart", @@ -66,10 +66,10 @@ } }, "dependencies": { - "@sentry/core": "9.28.1", - "@sentry/node": "9.28.1", - "@sentry/opentelemetry": "9.28.1", - "@sentry/solid": "9.28.1", + "@sentry/core": "9.29.0", + "@sentry/node": "9.29.0", + "@sentry/opentelemetry": "9.29.0", + "@sentry/solid": "9.29.0", "@sentry/vite-plugin": "2.22.6" }, "devDependencies": { diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 7601d52f28b3..0dc928edd079 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/svelte", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for Svelte", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/svelte", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.28.1", - "@sentry/core": "9.28.1", + "@sentry/browser": "9.29.0", + "@sentry/core": "9.29.0", "magic-string": "^0.30.0" }, "peerDependencies": { diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index a13ac8bfd86f..22bc71c1dbc0 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/sveltekit", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for SvelteKit", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/sveltekit", @@ -48,11 +48,11 @@ }, "dependencies": { "@babel/parser": "7.26.9", - "@sentry/cloudflare": "9.28.1", - "@sentry/core": "9.28.1", - "@sentry/node": "9.28.1", - "@sentry/opentelemetry": "9.28.1", - "@sentry/svelte": "9.28.1", + "@sentry/cloudflare": "9.29.0", + "@sentry/core": "9.29.0", + "@sentry/node": "9.29.0", + "@sentry/opentelemetry": "9.29.0", + "@sentry/svelte": "9.29.0", "@sentry/vite-plugin": "3.2.4", "magic-string": "0.30.7", "recast": "0.23.11", diff --git a/packages/tanstackstart-react/package.json b/packages/tanstackstart-react/package.json index 216b22e9b5bb..d94931286aae 100644 --- a/packages/tanstackstart-react/package.json +++ b/packages/tanstackstart-react/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/tanstackstart-react", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for TanStack Start React", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tanstackstart-react", @@ -52,11 +52,11 @@ "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/semantic-conventions": "^1.34.0", - "@sentry-internal/browser-utils": "9.28.1", - "@sentry/core": "9.28.1", - "@sentry/node": "9.28.1", - "@sentry/opentelemetry": "9.28.1", - "@sentry/react": "9.28.1" + "@sentry-internal/browser-utils": "9.29.0", + "@sentry/core": "9.29.0", + "@sentry/node": "9.29.0", + "@sentry/opentelemetry": "9.29.0", + "@sentry/react": "9.29.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/tanstackstart/package.json b/packages/tanstackstart/package.json index 6dd439888b45..316a3ffd85ba 100644 --- a/packages/tanstackstart/package.json +++ b/packages/tanstackstart/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/tanstackstart", - "version": "9.28.1", + "version": "9.29.0", "description": "Utilities for the Sentry TanStack Start SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/tanstackstart", diff --git a/packages/types/package.json b/packages/types/package.json index ded4f9e7ee43..64e97a37f075 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/types", - "version": "9.28.1", + "version": "9.29.0", "description": "Types for all Sentry JavaScript SDKs", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/types", @@ -57,7 +57,7 @@ "yalc:publish": "yalc publish --push --sig" }, "dependencies": { - "@sentry/core": "9.28.1" + "@sentry/core": "9.29.0" }, "volta": { "extends": "../../package.json" diff --git a/packages/typescript/package.json b/packages/typescript/package.json index ad3432895e74..d53d7315e2d1 100644 --- a/packages/typescript/package.json +++ b/packages/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/typescript", - "version": "9.28.1", + "version": "9.29.0", "description": "Typescript configuration used at Sentry", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/typescript", diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index c1b2121d64da..31738c545fb0 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vercel-edge", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for the Vercel Edge Runtime", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vercel-edge", @@ -40,7 +40,7 @@ }, "dependencies": { "@opentelemetry/api": "^1.9.0", - "@sentry/core": "9.28.1" + "@sentry/core": "9.29.0" }, "devDependencies": { "@edge-runtime/types": "3.0.1", @@ -48,7 +48,7 @@ "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", - "@sentry/opentelemetry": "9.28.1" + "@sentry/opentelemetry": "9.29.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/vue/package.json b/packages/vue/package.json index 4bcc5bdb8adb..d64a48484aa6 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/vue", - "version": "9.28.1", + "version": "9.29.0", "description": "Official Sentry SDK for Vue.js", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/vue", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.28.1", - "@sentry/core": "9.28.1" + "@sentry/browser": "9.29.0", + "@sentry/core": "9.29.0" }, "peerDependencies": { "pinia": "2.x || 3.x", diff --git a/packages/wasm/package.json b/packages/wasm/package.json index 12a04d83d915..2e1276dc476a 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/wasm", - "version": "9.28.1", + "version": "9.29.0", "description": "Support for WASM.", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/wasm", @@ -39,8 +39,8 @@ "access": "public" }, "dependencies": { - "@sentry/browser": "9.28.1", - "@sentry/core": "9.28.1" + "@sentry/browser": "9.29.0", + "@sentry/core": "9.29.0" }, "scripts": { "build": "run-p build:transpile build:bundle build:types", From 9cce949eb820e7e7f28057f3718bbe47cfb835d7 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Thu, 12 Jun 2025 13:58:07 -0700 Subject: [PATCH 25/32] Add flags to attrs directly on eval --- .../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 +-- .../onSpan/scenario.ts | 2 - .../test/integration/transactions.test.ts | 76 +++++++++---------- .../featureFlags/launchdarkly/integration.ts | 9 +-- .../featureFlags/openfeature/integration.ts | 9 +-- .../featureFlags/statsig/integration.ts | 9 +-- .../featureFlags/unleash/integration.ts | 9 +-- packages/core/src/index.ts | 1 - .../featureFlags/featureFlagsIntegration.ts | 8 -- packages/core/src/utils-hoist/worldwide.ts | 5 +- packages/core/src/utils/featureFlags.ts | 30 +++----- 15 files changed, 76 insertions(+), 137 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/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts index de2e14c93b68..d21fb7a9979e 100644 --- a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts @@ -6,8 +6,6 @@ const MAX_FLAGS_PER_SPAN = 10; Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', sampleRate: 1.0, - // // disable attaching headers to /test/* endpoints - // tracePropagationTargets: [/^(?!.*test).*$/], tracesSampleRate: 1.0, transport: loggingTransport, integrations: [Sentry.featureFlagsIntegration()], 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/launchdarkly/integration.ts b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts index c68a8d889bda..568dc23fd21d 100644 --- a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts +++ b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts @@ -1,8 +1,7 @@ -import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; +import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { _INTERNAL_bufferSpanFeatureFlag, _INTERNAL_copyFlagsFromScopeToEvent, - _INTERNAL_freezeSpanFeatureFlags, _INTERNAL_insertFlagToScope, defineIntegration, } from '@sentry/core'; @@ -27,12 +26,6 @@ export const launchDarklyIntegration = defineIntegration(() => { return { name: 'LaunchDarkly', - setup(client: Client) { - client.on('spanEnd', (span: Span) => { - _INTERNAL_freezeSpanFeatureFlags(span); - }); - }, - processEvent(event: Event, _hint: EventHint, _client: Client): Event { return _INTERNAL_copyFlagsFromScopeToEvent(event); }, diff --git a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts index ffe5bbbbee72..962e462b9df7 100644 --- a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts +++ b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts @@ -13,11 +13,10 @@ * 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 { _INTERNAL_bufferSpanFeatureFlag, _INTERNAL_copyFlagsFromScopeToEvent, - _INTERNAL_freezeSpanFeatureFlags, _INTERNAL_insertFlagToScope, defineIntegration, } from '@sentry/core'; @@ -27,12 +26,6 @@ export const openFeatureIntegration = defineIntegration(() => { return { name: 'OpenFeature', - setup(client: Client) { - client.on('spanEnd', (span: Span) => { - _INTERNAL_freezeSpanFeatureFlags(span); - }); - }, - processEvent(event: Event, _hint: EventHint, _client: Client): Event { return _INTERNAL_copyFlagsFromScopeToEvent(event); }, diff --git a/packages/browser/src/integrations/featureFlags/statsig/integration.ts b/packages/browser/src/integrations/featureFlags/statsig/integration.ts index b57d6befb186..897fe193e515 100644 --- a/packages/browser/src/integrations/featureFlags/statsig/integration.ts +++ b/packages/browser/src/integrations/featureFlags/statsig/integration.ts @@ -1,8 +1,7 @@ -import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; +import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { _INTERNAL_bufferSpanFeatureFlag, _INTERNAL_copyFlagsFromScopeToEvent, - _INTERNAL_freezeSpanFeatureFlags, _INTERNAL_insertFlagToScope, defineIntegration, } from '@sentry/core'; @@ -36,11 +35,7 @@ export const statsigIntegration = defineIntegration( return { name: 'Statsig', - setup(client: Client) { - client.on('spanEnd', (span: Span) => { - _INTERNAL_freezeSpanFeatureFlags(span); - }); - + setup(_client: Client) { statsigClient.on('gate_evaluation', (event: { gate: FeatureGate }) => { _INTERNAL_insertFlagToScope(event.gate.name, event.gate.value); _INTERNAL_bufferSpanFeatureFlag(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 e861d066ca1c..014f26a9d4bb 100644 --- a/packages/browser/src/integrations/featureFlags/unleash/integration.ts +++ b/packages/browser/src/integrations/featureFlags/unleash/integration.ts @@ -1,8 +1,7 @@ -import type { Client, Event, EventHint, IntegrationFn, Span } from '@sentry/core'; +import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { _INTERNAL_bufferSpanFeatureFlag, _INTERNAL_copyFlagsFromScopeToEvent, - _INTERNAL_freezeSpanFeatureFlags, _INTERNAL_insertFlagToScope, defineIntegration, fill, @@ -42,12 +41,6 @@ export const unleashIntegration = defineIntegration( return { name: 'Unleash', - setup(client: Client) { - client.on('spanEnd', (span: Span) => { - _INTERNAL_freezeSpanFeatureFlags(span); - }); - }, - setupOnce() { const unleashClientPrototype = unleashClientClass.prototype as UnleashClient; fill(unleashClientPrototype, 'isEnabled', _wrappedIsEnabled); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3764fe0f30b0..9f78873ecfb7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -129,7 +129,6 @@ export { _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_insertFlagToScope, _INTERNAL_bufferSpanFeatureFlag, - _INTERNAL_freezeSpanFeatureFlags, } from './utils/featureFlags'; export { applyAggregateErrorsToEvent } from './utils-hoist/aggregate-errors'; diff --git a/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts b/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts index 21298989d24c..880bfc033cef 100644 --- a/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts +++ b/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts @@ -2,11 +2,9 @@ import { type Client } from '../../client'; import { defineIntegration } from '../../integration'; import { type Event, type EventHint } from '../../types-hoist/event'; import { type Integration, type IntegrationFn } from '../../types-hoist/integration'; -import { type Span } from '../../types-hoist/span'; import { _INTERNAL_bufferSpanFeatureFlag, _INTERNAL_copyFlagsFromScopeToEvent, - _INTERNAL_freezeSpanFeatureFlags, _INTERNAL_insertFlagToScope, } from '../../utils/featureFlags'; @@ -42,12 +40,6 @@ export const featureFlagsIntegration = defineIntegration(() => { return { name: 'FeatureFlags', - setup(client: Client) { - client.on('spanEnd', (span: Span) => { - _INTERNAL_freezeSpanFeatureFlags(span); - }); - }, - processEvent(event: Event, _hint: EventHint, _client: Client): Event { return _INTERNAL_copyFlagsFromScopeToEvent(event); }, diff --git a/packages/core/src/utils-hoist/worldwide.ts b/packages/core/src/utils-hoist/worldwide.ts index 69e64c7ac98d..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'; @@ -59,9 +58,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 */ diff --git a/packages/core/src/utils/featureFlags.ts b/packages/core/src/utils/featureFlags.ts index ced582feef43..618b879d65cf 100644 --- a/packages/core/src/utils/featureFlags.ts +++ b/packages/core/src/utils/featureFlags.ts @@ -24,7 +24,7 @@ const FLAG_BUFFER_SIZE = 100; 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.'; @@ -121,8 +121,9 @@ export function _INTERNAL_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. @@ -140,22 +141,13 @@ export function _INTERNAL_bufferSpanFeatureFlag( const span = getActiveSpan(); if (span) { - const flags = spanFlagMap.get(span) || []; - _INTERNAL_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 _INTERNAL_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])), - ); - } -} From 52385ee2de1cbde80815f5df560d8bc612eb53c4 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Thu, 12 Jun 2025 14:00:09 -0700 Subject: [PATCH 26/32] Rename util --- .../integrations/featureFlags/launchdarkly/integration.ts | 4 ++-- .../integrations/featureFlags/openfeature/integration.ts | 6 +++--- .../src/integrations/featureFlags/statsig/integration.ts | 4 ++-- .../src/integrations/featureFlags/unleash/integration.ts | 4 ++-- packages/core/src/index.ts | 2 +- .../integrations/featureFlags/featureFlagsIntegration.ts | 4 ++-- packages/core/src/utils/featureFlags.ts | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts index 568dc23fd21d..822e4b1d7f80 100644 --- a/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts +++ b/packages/browser/src/integrations/featureFlags/launchdarkly/integration.ts @@ -1,6 +1,6 @@ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { - _INTERNAL_bufferSpanFeatureFlag, + _INTERNAL_addFeatureFlagToActiveSpan, _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_insertFlagToScope, defineIntegration, @@ -50,7 +50,7 @@ export function buildLaunchDarklyFlagUsedHandler(): LDInspectionFlagUsedHandler */ method: (flagKey: string, flagDetail: LDEvaluationDetail, _context: LDContext) => { _INTERNAL_insertFlagToScope(flagKey, flagDetail.value); - _INTERNAL_bufferSpanFeatureFlag(flagKey, flagDetail.value); + _INTERNAL_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 962e462b9df7..85aedbf779f9 100644 --- a/packages/browser/src/integrations/featureFlags/openfeature/integration.ts +++ b/packages/browser/src/integrations/featureFlags/openfeature/integration.ts @@ -15,7 +15,7 @@ */ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { - _INTERNAL_bufferSpanFeatureFlag, + _INTERNAL_addFeatureFlagToActiveSpan, _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_insertFlagToScope, defineIntegration, @@ -41,7 +41,7 @@ export class OpenFeatureIntegrationHook implements OpenFeatureHook { */ public after(_hookContext: Readonly>, evaluationDetails: EvaluationDetails): void { _INTERNAL_insertFlagToScope(evaluationDetails.flagKey, evaluationDetails.value); - _INTERNAL_bufferSpanFeatureFlag(evaluationDetails.flagKey, evaluationDetails.value); + _INTERNAL_addFeatureFlagToActiveSpan(evaluationDetails.flagKey, evaluationDetails.value); } /** @@ -49,6 +49,6 @@ export class OpenFeatureIntegrationHook implements OpenFeatureHook { */ public error(hookContext: Readonly>, _error: unknown, _hookHints?: HookHints): void { _INTERNAL_insertFlagToScope(hookContext.flagKey, hookContext.defaultValue); - _INTERNAL_bufferSpanFeatureFlag(hookContext.flagKey, hookContext.defaultValue); + _INTERNAL_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 897fe193e515..9aef234045b5 100644 --- a/packages/browser/src/integrations/featureFlags/statsig/integration.ts +++ b/packages/browser/src/integrations/featureFlags/statsig/integration.ts @@ -1,6 +1,6 @@ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { - _INTERNAL_bufferSpanFeatureFlag, + _INTERNAL_addFeatureFlagToActiveSpan, _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_insertFlagToScope, defineIntegration, @@ -38,7 +38,7 @@ export const statsigIntegration = defineIntegration( setup(_client: Client) { statsigClient.on('gate_evaluation', (event: { gate: FeatureGate }) => { _INTERNAL_insertFlagToScope(event.gate.name, event.gate.value); - _INTERNAL_bufferSpanFeatureFlag(event.gate.name, event.gate.value); + _INTERNAL_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 014f26a9d4bb..699c797edecf 100644 --- a/packages/browser/src/integrations/featureFlags/unleash/integration.ts +++ b/packages/browser/src/integrations/featureFlags/unleash/integration.ts @@ -1,6 +1,6 @@ import type { Client, Event, EventHint, IntegrationFn } from '@sentry/core'; import { - _INTERNAL_bufferSpanFeatureFlag, + _INTERNAL_addFeatureFlagToActiveSpan, _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_insertFlagToScope, defineIntegration, @@ -71,7 +71,7 @@ function _wrappedIsEnabled( if (typeof toggleName === 'string' && typeof result === 'boolean') { _INTERNAL_insertFlagToScope(toggleName, result); - _INTERNAL_bufferSpanFeatureFlag(toggleName, result); + _INTERNAL_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/core/src/index.ts b/packages/core/src/index.ts index 9f78873ecfb7..3d87729f4639 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -128,7 +128,7 @@ export type { FeatureFlag } from './featureFlags'; export { _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_insertFlagToScope, - _INTERNAL_bufferSpanFeatureFlag, + _INTERNAL_addFeatureFlagToActiveSpan, } from './utils/featureFlags'; export { applyAggregateErrorsToEvent } from './utils-hoist/aggregate-errors'; diff --git a/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts b/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts index 880bfc033cef..fddc1944ceab 100644 --- a/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts +++ b/packages/core/src/integrations/featureFlags/featureFlagsIntegration.ts @@ -3,7 +3,7 @@ import { defineIntegration } from '../../integration'; import { type Event, type EventHint } from '../../types-hoist/event'; import { type Integration, type IntegrationFn } from '../../types-hoist/integration'; import { - _INTERNAL_bufferSpanFeatureFlag, + _INTERNAL_addFeatureFlagToActiveSpan, _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_insertFlagToScope, } from '../../utils/featureFlags'; @@ -46,7 +46,7 @@ export const featureFlagsIntegration = defineIntegration(() => { addFeatureFlag(name: string, value: unknown): void { _INTERNAL_insertFlagToScope(name, value); - _INTERNAL_bufferSpanFeatureFlag(name, value); + _INTERNAL_addFeatureFlagToActiveSpan(name, value); }, }; }) as IntegrationFn; diff --git a/packages/core/src/utils/featureFlags.ts b/packages/core/src/utils/featureFlags.ts index 618b879d65cf..ee9146e630b5 100644 --- a/packages/core/src/utils/featureFlags.ts +++ b/packages/core/src/utils/featureFlags.ts @@ -129,7 +129,7 @@ export function _INTERNAL_insertToFlagBuffer( * @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 _INTERNAL_bufferSpanFeatureFlag( +export function _INTERNAL_addFeatureFlagToActiveSpan( name: string, value: unknown, maxFlagsPerSpan: number = MAX_FLAGS_PER_SPAN, From 90d9289b7b14753c947c1ac52006252ffbb57124 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Thu, 12 Jun 2025 14:05:26 -0700 Subject: [PATCH 27/32] Move FF type to core utils file --- packages/core/src/featureFlags.ts | 1 - packages/core/src/index.ts | 3 +-- packages/core/src/types-hoist/context.ts | 2 +- packages/core/src/utils/featureFlags.ts | 3 ++- 4 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 packages/core/src/featureFlags.ts diff --git a/packages/core/src/featureFlags.ts b/packages/core/src/featureFlags.ts deleted file mode 100644 index f80e17ef7f9d..000000000000 --- a/packages/core/src/featureFlags.ts +++ /dev/null @@ -1 +0,0 @@ -export type FeatureFlag = { readonly flag: string; readonly result: boolean }; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3d87729f4639..4bbbf67f6e32 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -123,8 +123,7 @@ export type { ReportDialogOptions } from './report-dialog'; export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_captureSerializedLog } from './logs/exports'; export { consoleLoggingIntegration } from './logs/console-integration'; -export type { FeatureFlag } from './featureFlags'; -// TODO: ^export from utils? +export type { FeatureFlag } from './utils/featureFlags'; export { _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_insertFlagToScope, diff --git a/packages/core/src/types-hoist/context.ts b/packages/core/src/types-hoist/context.ts index 0ad6eebf6ac3..a3673f84a968 100644 --- a/packages/core/src/types-hoist/context.ts +++ b/packages/core/src/types-hoist/context.ts @@ -1,4 +1,4 @@ -import type { FeatureFlag } from '../featureFlags'; +import type { FeatureFlag } from '../utils/featureFlags'; import type { SpanLinkJSON } from './link'; import type { Primitive } from './misc'; import type { SpanOrigin } from './span'; diff --git a/packages/core/src/utils/featureFlags.ts b/packages/core/src/utils/featureFlags.ts index ee9146e630b5..ba9145c316e8 100644 --- a/packages/core/src/utils/featureFlags.ts +++ b/packages/core/src/utils/featureFlags.ts @@ -1,6 +1,5 @@ import { getCurrentScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; -import { type FeatureFlag } from '../featureFlags'; import { type Event } from '../types-hoist/event'; import { type Span } from '../types-hoist/span'; import { logger } from '../utils-hoist/logger'; @@ -13,6 +12,8 @@ import { getActiveSpan } from './spanUtils'; * from oldest to newest. */ +export type FeatureFlag = { readonly flag: string; readonly result: boolean }; + /** * Max size of the LRU flag buffer stored in Sentry scope and event contexts. */ From bca65ddea6b69d6f79a7023b626c1f8d143be3e2 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Thu, 12 Jun 2025 14:55:47 -0700 Subject: [PATCH 28/32] Export buffer sizes from core as _INTERNAL_ --- .../suites/integrations/featureFlags/constants.ts | 3 --- .../featureFlags/featureFlags/onError/basic/test.ts | 2 +- .../featureFlags/featureFlags/onSpan/test.ts | 2 +- .../featureFlags/launchdarkly/onError/basic/test.ts | 2 +- .../featureFlags/launchdarkly/onSpan/test.ts | 2 +- .../featureFlags/openfeature/onError/basic/test.ts | 2 +- .../openfeature/onError/errorHook/test.ts | 2 +- .../featureFlags/openfeature/onSpan/test.ts | 2 +- .../featureFlags/statsig/onError/basic/test.ts | 2 +- .../integrations/featureFlags/statsig/onSpan/test.ts | 2 +- .../featureFlags/unleash/onError/basic/test.ts | 2 +- .../integrations/featureFlags/unleash/onSpan/test.ts | 2 +- .../featureFlagsIntegration/onError/basic/test.ts | 3 +-- .../featureFlagsIntegration/onSpan/test.ts | 3 +-- packages/core/src/index.ts | 2 ++ packages/core/src/utils/featureFlags.ts | 12 ++++++++---- 16 files changed, 23 insertions(+), 22 deletions(-) delete mode 100644 dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.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 deleted file mode 100644 index 712b7270b52c..000000000000 --- a/dev-packages/browser-integration-tests/suites/integrations/featureFlags/constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Corresponds to constants in featureFlags.ts, in @sentry/core 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 742cdd42109b..3233c9047649 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,11 +1,11 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; 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/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/featureFlags/onSpan/test.ts index 476b76d03475..6516d8e36abb 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 @@ -1,4 +1,5 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { type EventAndTraceHeader, @@ -7,7 +8,6 @@ import { 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()) { 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 1c1a04595187..bc3e0afdc292 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,11 +1,11 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; 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/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/launchdarkly/onSpan/test.ts index 965f00f91fa0..eb7eb003c838 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 @@ -1,4 +1,5 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { type EventAndTraceHeader, @@ -7,7 +8,6 @@ import { 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()) { 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 84deca47415d..5953f1e0b087 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,11 +1,11 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; 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/onError/errorHook/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onError/errorHook/test.ts index c2de7f54abd7..89654c82eda1 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,11 +1,11 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; 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/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/openfeature/onSpan/test.ts index f3b43425477f..5ade5d01b3d5 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 @@ -1,4 +1,5 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { type EventAndTraceHeader, @@ -7,7 +8,6 @@ import { 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()) { 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 331dbb8ad433..134b29417d53 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,11 +1,11 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; 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/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/statsig/onSpan/test.ts index dec534f9ffab..1ea192f98850 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 @@ -1,4 +1,5 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { type EventAndTraceHeader, @@ -7,7 +8,6 @@ import { 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()) { 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 341bbbd03e96..6e2760b69600 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,11 +1,11 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; 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/onSpan/test.ts b/dev-packages/browser-integration-tests/suites/integrations/featureFlags/unleash/onSpan/test.ts index b2607ffa4c07..984bba3bc0e3 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 @@ -1,4 +1,5 @@ import { expect } from '@playwright/test'; +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; import { sentryTest } from '../../../../../utils/fixtures'; import { type EventAndTraceHeader, @@ -7,7 +8,6 @@ import { 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()) { diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts index 6b003bdd7a6e..74ff1c125b45 100644 --- a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts @@ -1,8 +1,7 @@ +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; import { afterAll, test } from 'vitest'; import { cleanupChildProcesses, createRunner } from '../../../../../utils/runner'; -const FLAG_BUFFER_SIZE = 100; - afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts index 6828caa83d2e..4a417a3c3959 100644 --- a/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts +++ b/dev-packages/node-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts @@ -1,8 +1,7 @@ +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; import { afterAll, expect, test } from 'vitest'; import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; -const MAX_FLAGS_PER_SPAN = 10; - afterAll(() => { cleanupChildProcesses(); }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 4bbbf67f6e32..d880773a7891 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -128,6 +128,8 @@ export { _INTERNAL_copyFlagsFromScopeToEvent, _INTERNAL_insertFlagToScope, _INTERNAL_addFeatureFlagToActiveSpan, + _INTERNAL_FLAG_BUFFER_SIZE, + _INTERNAL_MAX_FLAGS_PER_SPAN, } from './utils/featureFlags'; export { applyAggregateErrorsToEvent } from './utils-hoist/aggregate-errors'; diff --git a/packages/core/src/utils/featureFlags.ts b/packages/core/src/utils/featureFlags.ts index ba9145c316e8..302885bf3792 100644 --- a/packages/core/src/utils/featureFlags.ts +++ b/packages/core/src/utils/featureFlags.ts @@ -17,12 +17,12 @@ export type FeatureFlag = { readonly flag: string; readonly result: boolean }; /** * Max size of the LRU flag buffer stored in Sentry scope and event contexts. */ -const FLAG_BUFFER_SIZE = 100; +export const _INTERNAL_FLAG_BUFFER_SIZE = 100; /** * Max number of flag evaluations to record per span. */ -const MAX_FLAGS_PER_SPAN = 10; +export const _INTERNAL_MAX_FLAGS_PER_SPAN = 10; // Global map of spans to feature flag buffers. Populated by feature flag integrations. GLOBAL_OBJ._spanToFlagBufferMap = new WeakMap>(); @@ -59,7 +59,11 @@ export function _INTERNAL_copyFlagsFromScopeToEvent(event: Event): Event { * @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 _INTERNAL_insertFlagToScope(name: string, value: unknown, maxSize: number = FLAG_BUFFER_SIZE): void { +export function _INTERNAL_insertFlagToScope( + name: string, + value: unknown, + maxSize: number = _INTERNAL_FLAG_BUFFER_SIZE, +): void { const scopeContexts = getCurrentScope().getScopeData().contexts; if (!scopeContexts.flags) { scopeContexts.flags = { values: [] }; @@ -133,7 +137,7 @@ export function _INTERNAL_insertToFlagBuffer( export function _INTERNAL_addFeatureFlagToActiveSpan( name: string, value: unknown, - maxFlagsPerSpan: number = MAX_FLAGS_PER_SPAN, + maxFlagsPerSpan: number = _INTERNAL_MAX_FLAGS_PER_SPAN, ): void { const spanFlagMap = GLOBAL_OBJ._spanToFlagBufferMap; if (!spanFlagMap || typeof value !== 'boolean') { From c92a9b05091585b0a2bcdddb35cb0e1a64baca82 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Fri, 13 Jun 2025 08:36:33 +0200 Subject: [PATCH 29/32] feat(node): Ensure `modulesIntegration` works in more environments (#16566) Extracted out from https://github.com/getsentry/sentry-javascript/pull/16565 I noticed that our `modulesIntegration` is pretty limited: 1. It does nothing on EMS 2. It does not work on Next.js (even though that is CJS) This PR makes this a bit more robust (not perfect): 1. It generally now also tries to look at `process.cwd() + 'package.json'` and take the dependencies and devDependencies from there. this should generally work in esm apps now as well, at least at a basic level. You do not get all dependencies and versions may be ranges, but better than nothing. 2. For Next.js, we inject a modules list based off the package.json at build time, as we do not have proper access to this at runtime. --- .../tests/server-components.test.ts | 8 ++ .../suites/modules/instrument.mjs | 8 ++ .../suites/modules/server.js | 22 +++++ .../suites/modules/server.mjs | 13 +++ .../suites/modules/test.ts | 48 ++++++++++ packages/nextjs/src/config/webpack.ts | 26 ++++++ packages/node/src/integrations/modules.ts | 92 ++++++++++++------- packages/node/src/sdk/index.ts | 6 +- 8 files changed, 187 insertions(+), 36 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/modules/instrument.mjs create mode 100644 dev-packages/node-integration-tests/suites/modules/server.js create mode 100644 dev-packages/node-integration-tests/suites/modules/server.mjs create mode 100644 dev-packages/node-integration-tests/suites/modules/test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts index 498c9b969ed9..8208cd603c98 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts @@ -123,4 +123,12 @@ test('Should capture an error and transaction for a app router page', async ({ p expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true); expect(transactionEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined(); + + // Modules are set for Next.js + expect(errorEvent.modules).toEqual( + expect.objectContaining({ + '@sentry/nextjs': expect.any(String), + '@playwright/test': expect.any(String), + }), + ); }); diff --git a/dev-packages/node-integration-tests/suites/modules/instrument.mjs b/dev-packages/node-integration-tests/suites/modules/instrument.mjs new file mode 100644 index 000000000000..9ffde125d498 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/modules/instrument.mjs @@ -0,0 +1,8 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); diff --git a/dev-packages/node-integration-tests/suites/modules/server.js b/dev-packages/node-integration-tests/suites/modules/server.js new file mode 100644 index 000000000000..9b24c0845ac0 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/modules/server.js @@ -0,0 +1,22 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.get('/test1', () => { + throw new Error('error_1'); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/modules/server.mjs b/dev-packages/node-integration-tests/suites/modules/server.mjs new file mode 100644 index 000000000000..6edeb78c703f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/modules/server.mjs @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/node'; +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import express from 'express'; + +const app = express(); + +app.get('/test1', () => { + throw new Error('error_1'); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/modules/test.ts b/dev-packages/node-integration-tests/suites/modules/test.ts new file mode 100644 index 000000000000..89fe98c62867 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/modules/test.ts @@ -0,0 +1,48 @@ +import { SDK_VERSION } from '@sentry/core'; +import { join } from 'path'; +import { afterAll, describe, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../utils/runner'; + +describe('modulesIntegration', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('CJS', async () => { + const runner = createRunner(__dirname, 'server.js') + .withMockSentryServer() + .expect({ + event: { + modules: { + // exact version comes from require.caches + express: '4.21.1', + // this comes from package.json + '@sentry/node': SDK_VERSION, + yargs: '^16.2.0', + }, + }, + }) + .start(); + runner.makeRequest('get', '/test1', { expectError: true }); + await runner.completed(); + }); + + test('ESM', async () => { + const runner = createRunner(__dirname, 'server.mjs') + .withInstrument(join(__dirname, 'instrument.mjs')) + .withMockSentryServer() + .expect({ + event: { + modules: { + // this comes from package.json + express: '^4.21.1', + '@sentry/node': SDK_VERSION, + yargs: '^16.2.0', + }, + }, + }) + .start(); + runner.makeRequest('get', '/test1', { expectError: true }); + await runner.completed(); + }); +}); diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 322f2e320624..8898b3495ba9 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -410,6 +410,14 @@ export function constructWebpackConfigFunction( ); } + // We inject a map of dependencies that the nextjs app has, as we cannot reliably extract them at runtime, sadly + newConfig.plugins = newConfig.plugins || []; + newConfig.plugins.push( + new buildContext.webpack.DefinePlugin({ + __SENTRY_SERVER_MODULES__: JSON.stringify(_getModules(projectDir)), + }), + ); + return newConfig; }; } @@ -825,3 +833,21 @@ function addOtelWarningIgnoreRule(newConfig: WebpackConfigObjectWithModuleRules) newConfig.ignoreWarnings.push(...ignoreRules); } } + +function _getModules(projectDir: string): Record { + try { + const packageJson = path.join(projectDir, 'package.json'); + const packageJsonContent = fs.readFileSync(packageJson, 'utf8'); + const packageJsonObject = JSON.parse(packageJsonContent) as { + dependencies?: Record; + devDependencies?: Record; + }; + + return { + ...packageJsonObject.dependencies, + ...packageJsonObject.devDependencies, + }; + } catch { + return {}; + } +} diff --git a/packages/node/src/integrations/modules.ts b/packages/node/src/integrations/modules.ts index e15aa9dd245b..50f3a3b3aa8d 100644 --- a/packages/node/src/integrations/modules.ts +++ b/packages/node/src/integrations/modules.ts @@ -1,26 +1,24 @@ import { existsSync, readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import type { IntegrationFn } from '@sentry/core'; -import { defineIntegration, logger } from '@sentry/core'; -import { DEBUG_BUILD } from '../debug-build'; +import { defineIntegration } from '@sentry/core'; import { isCjs } from '../utils/commonjs'; -let moduleCache: { [key: string]: string }; +type ModuleInfo = Record; + +let moduleCache: ModuleInfo | undefined; const INTEGRATION_NAME = 'Modules'; -const _modulesIntegration = (() => { - // This integration only works in CJS contexts - if (!isCjs()) { - DEBUG_BUILD && - logger.warn( - 'modulesIntegration only works in CommonJS (CJS) environments. Remove this integration if you are using ESM.', - ); - return { - name: INTEGRATION_NAME, - }; - } +declare const __SENTRY_SERVER_MODULES__: Record; + +/** + * `__SENTRY_SERVER_MODULES__` can be replaced at build time with the modules loaded by the server. + * Right now, we leverage this in Next.js to circumvent the problem that we do not get access to these things at runtime. + */ +const SERVER_MODULES = typeof __SENTRY_SERVER_MODULES__ === 'undefined' ? {} : __SENTRY_SERVER_MODULES__; +const _modulesIntegration = (() => { return { name: INTEGRATION_NAME, processEvent(event) { @@ -36,13 +34,14 @@ const _modulesIntegration = (() => { /** * Add node modules / packages to the event. - * - * Only works in CommonJS (CJS) environments. + * For this, multiple sources are used: + * - They can be injected at build time into the __SENTRY_SERVER_MODULES__ variable (e.g. in Next.js) + * - They are extracted from the dependencies & devDependencies in the package.json file + * - They are extracted from the require.cache (CJS only) */ export const modulesIntegration = defineIntegration(_modulesIntegration); -/** Extract information about paths */ -function getPaths(): string[] { +function getRequireCachePaths(): string[] { try { return require.cache ? Object.keys(require.cache as Record) : []; } catch (e) { @@ -51,17 +50,23 @@ function getPaths(): string[] { } /** Extract information about package.json modules */ -function collectModules(): { - [name: string]: string; -} { +function collectModules(): ModuleInfo { + return { + ...SERVER_MODULES, + ...getModulesFromPackageJson(), + ...(isCjs() ? collectRequireModules() : {}), + }; +} + +/** Extract information about package.json modules from require.cache */ +function collectRequireModules(): ModuleInfo { const mainPaths = require.main?.paths || []; - const paths = getPaths(); - const infos: { - [name: string]: string; - } = {}; - const seen: { - [path: string]: boolean; - } = {}; + const paths = getRequireCachePaths(); + + // We start with the modules from package.json (if possible) + // These may be overwritten by more specific versions from the require.cache + const infos: ModuleInfo = {}; + const seen = new Set(); paths.forEach(path => { let dir = path; @@ -71,7 +76,7 @@ function collectModules(): { const orig = dir; dir = dirname(orig); - if (!dir || orig === dir || seen[orig]) { + if (!dir || orig === dir || seen.has(orig)) { return undefined; } if (mainPaths.indexOf(dir) < 0) { @@ -79,7 +84,7 @@ function collectModules(): { } const pkgfile = join(orig, 'package.json'); - seen[orig] = true; + seen.add(orig); if (!existsSync(pkgfile)) { return updir(); @@ -103,9 +108,34 @@ function collectModules(): { } /** Fetches the list of modules and the versions loaded by the entry file for your node.js app. */ -function _getModules(): { [key: string]: string } { +function _getModules(): ModuleInfo { if (!moduleCache) { moduleCache = collectModules(); } return moduleCache; } + +interface PackageJson { + dependencies?: Record; + devDependencies?: Record; +} + +function getPackageJson(): PackageJson { + try { + const filePath = join(process.cwd(), 'package.json'); + const packageJson = JSON.parse(readFileSync(filePath, 'utf8')) as PackageJson; + + return packageJson; + } catch (e) { + return {}; + } +} + +function getModulesFromPackageJson(): ModuleInfo { + const packageJson = getPackageJson(); + + return { + ...packageJson.dependencies, + ...packageJson.devDependencies, + }; +} diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index 1536242cfdcb..e693d3976fe4 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -40,10 +40,6 @@ import { defaultStackParser, getSentryRelease } from './api'; import { NodeClient } from './client'; import { initOpenTelemetry, maybeInitializeEsmLoader } from './initOtel'; -function getCjsOnlyIntegrations(): Integration[] { - return isCjs() ? [modulesIntegration()] : []; -} - /** * Get default integrations, excluding performance. */ @@ -69,7 +65,7 @@ export function getDefaultIntegrationsWithoutPerformance(): Integration[] { nodeContextIntegration(), childProcessIntegration(), processSessionIntegration(), - ...getCjsOnlyIntegrations(), + modulesIntegration(), ]; } From d35030d9266d99c9f3e24635155310008d3436d9 Mon Sep 17 00:00:00 2001 From: Francesco Gringl-Novy Date: Fri, 13 Jun 2025 16:21:04 +0200 Subject: [PATCH 30/32] feat(node): Automatically enable `vercelAiIntegration` when `ai` module is detected (#16565) This PR improves the handling of the `ai` instrumentation to always be enabled when we detect the `ai` module is installed. For this, we leverage the `modulesIntegration`. This PR should make usage of ai instrumentation in Next.js "automatically" again - BUT users will have to specific ` experimental_telemetry: { isEnabled: true },` at each call manually for the time being. --------- Co-authored-by: Cursor Agent --- .../nextjs-15/app/ai-test/page.tsx | 101 ++++++++++++++++++ .../test-applications/nextjs-15/package.json | 4 +- .../nextjs-15/sentry.server.config.ts | 3 + .../nextjs-15/tests/ai-test.test.ts | 73 +++++++++++++ packages/node/src/integrations/modules.ts | 4 +- .../integrations/tracing/vercelai/index.ts | 23 +++- 6 files changed, 202 insertions(+), 6 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/app/ai-test/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/tests/ai-test.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/ai-test/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/ai-test/page.tsx new file mode 100644 index 000000000000..828e92baf62a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/ai-test/page.tsx @@ -0,0 +1,101 @@ +import { generateText } from 'ai'; +import { MockLanguageModelV1 } from 'ai/test'; +import { z } from 'zod'; +import * as Sentry from '@sentry/nextjs'; + +export const dynamic = 'force-dynamic'; + +async function runAITest() { + // First span - telemetry should be enabled automatically but no input/output recorded when sendDefaultPii: true + const result1 = await generateText({ + model: new MockLanguageModelV1({ + doGenerate: async () => ({ + rawCall: { rawPrompt: null, rawSettings: {} }, + finishReason: 'stop', + usage: { promptTokens: 10, completionTokens: 20 }, + text: 'First span here!', + }), + }), + prompt: 'Where is the first span?', + }); + + // Second span - explicitly enabled telemetry, should record inputs/outputs + const result2 = await generateText({ + experimental_telemetry: { isEnabled: true }, + model: new MockLanguageModelV1({ + doGenerate: async () => ({ + rawCall: { rawPrompt: null, rawSettings: {} }, + finishReason: 'stop', + usage: { promptTokens: 10, completionTokens: 20 }, + text: 'Second span here!', + }), + }), + prompt: 'Where is the second span?', + }); + + // Third span - with tool calls and tool results + const result3 = await generateText({ + model: new MockLanguageModelV1({ + doGenerate: async () => ({ + rawCall: { rawPrompt: null, rawSettings: {} }, + finishReason: 'tool-calls', + usage: { promptTokens: 15, completionTokens: 25 }, + text: 'Tool call completed!', + toolCalls: [ + { + toolCallType: 'function', + toolCallId: 'call-1', + toolName: 'getWeather', + args: '{ "location": "San Francisco" }', + }, + ], + }), + }), + tools: { + getWeather: { + parameters: z.object({ location: z.string() }), + execute: async (args) => { + return `Weather in ${args.location}: Sunny, 72°F`; + }, + }, + }, + prompt: 'What is the weather in San Francisco?', + }); + + // Fourth span - explicitly disabled telemetry, should not be captured + const result4 = await generateText({ + experimental_telemetry: { isEnabled: false }, + model: new MockLanguageModelV1({ + doGenerate: async () => ({ + rawCall: { rawPrompt: null, rawSettings: {} }, + finishReason: 'stop', + usage: { promptTokens: 10, completionTokens: 20 }, + text: 'Third span here!', + }), + }), + prompt: 'Where is the third span?', + }); + + return { + result1: result1.text, + result2: result2.text, + result3: result3.text, + result4: result4.text, + }; +} + +export default async function Page() { + const results = await Sentry.startSpan( + { op: 'function', name: 'ai-test' }, + async () => { + return await runAITest(); + } + ); + + return ( +
+

AI Test Results

+
{JSON.stringify(results, null, 2)}
+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json index a79d34746ee4..416102b15da7 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json @@ -18,10 +18,12 @@ "@types/node": "^18.19.1", "@types/react": "18.0.26", "@types/react-dom": "18.0.9", + "ai": "^3.0.0", "next": "15.3.0-canary.33", "react": "beta", "react-dom": "beta", - "typescript": "~5.0.0" + "typescript": "~5.0.0", + "zod": "^3.22.4" }, "devDependencies": { "@playwright/test": "~1.50.0", diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts index 067d2ead0b8b..947e8bb7f819 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/sentry.server.config.ts @@ -10,4 +10,7 @@ Sentry.init({ // We are doing a lot of events at once in this test bufferSize: 1000, }, + integrations: [ + Sentry.vercelAIIntegration(), + ], }); diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ai-test.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ai-test.test.ts new file mode 100644 index 000000000000..e3598fd15d0e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/ai-test.test.ts @@ -0,0 +1,73 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('should create AI spans with correct attributes', async ({ page }) => { + const aiTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { + return transactionEvent?.transaction === 'ai-test'; + }); + + await page.goto('/ai-test'); + + const aiTransaction = await aiTransactionPromise; + + expect(aiTransaction).toBeDefined(); + expect(aiTransaction.contexts?.trace?.op).toBe('function'); + expect(aiTransaction.transaction).toBe('ai-test'); + + const spans = aiTransaction.spans || []; + + // We expect spans for the first 3 AI calls (4th is disabled) + // Each generateText call should create 2 spans: one for the pipeline and one for doGenerate + // Plus a span for the tool call + // TODO: For now, this is sadly not fully working - the monkey patching of the ai package is not working + // because of this, only spans that are manually opted-in at call time will be captured + // this may be fixed by https://github.com/vercel/ai/pull/6716 in the future + const aiPipelineSpans = spans.filter(span => span.op === 'ai.pipeline.generate_text'); + const aiGenerateSpans = spans.filter(span => span.op === 'gen_ai.generate_text'); + const toolCallSpans = spans.filter(span => span.op === 'gen_ai.execute_tool'); + + expect(aiPipelineSpans.length).toBeGreaterThanOrEqual(1); + expect(aiGenerateSpans.length).toBeGreaterThanOrEqual(1); + expect(toolCallSpans.length).toBeGreaterThanOrEqual(0); + + // First AI call - should have telemetry enabled and record inputs/outputs (sendDefaultPii: true) + /* const firstPipelineSpan = aiPipelineSpans[0]; + expect(firstPipelineSpan?.data?.['ai.model.id']).toBe('mock-model-id'); + expect(firstPipelineSpan?.data?.['ai.model.provider']).toBe('mock-provider'); + expect(firstPipelineSpan?.data?.['ai.prompt']).toContain('Where is the first span?'); + expect(firstPipelineSpan?.data?.['ai.response.text']).toBe('First span here!'); + expect(firstPipelineSpan?.data?.['gen_ai.usage.input_tokens']).toBe(10); + expect(firstPipelineSpan?.data?.['gen_ai.usage.output_tokens']).toBe(20); */ + + // Second AI call - explicitly enabled telemetry + const secondPipelineSpan = aiPipelineSpans[0]; + expect(secondPipelineSpan?.data?.['ai.prompt']).toContain('Where is the second span?'); + expect(secondPipelineSpan?.data?.['ai.response.text']).toContain('Second span here!'); + + // Third AI call - with tool calls + /* const thirdPipelineSpan = aiPipelineSpans[2]; + expect(thirdPipelineSpan?.data?.['ai.response.finishReason']).toBe('tool-calls'); + expect(thirdPipelineSpan?.data?.['gen_ai.usage.input_tokens']).toBe(15); + expect(thirdPipelineSpan?.data?.['gen_ai.usage.output_tokens']).toBe(25); */ + + // Tool call span + /* const toolSpan = toolCallSpans[0]; + expect(toolSpan?.data?.['ai.toolCall.name']).toBe('getWeather'); + expect(toolSpan?.data?.['ai.toolCall.id']).toBe('call-1'); + expect(toolSpan?.data?.['ai.toolCall.args']).toContain('San Francisco'); + expect(toolSpan?.data?.['ai.toolCall.result']).toContain('Sunny, 72°F'); */ + + // Verify the fourth call was not captured (telemetry disabled) + const promptsInSpans = spans + .map(span => span.data?.['ai.prompt']) + .filter((prompt): prompt is string => prompt !== undefined); + const hasDisabledPrompt = promptsInSpans.some(prompt => prompt.includes('Where is the third span?')); + expect(hasDisabledPrompt).toBe(false); + + // Verify results are displayed on the page + const resultsText = await page.locator('#ai-results').textContent(); + expect(resultsText).toContain('First span here!'); + expect(resultsText).toContain('Second span here!'); + expect(resultsText).toContain('Tool call completed!'); + expect(resultsText).toContain('Third span here!'); +}); diff --git a/packages/node/src/integrations/modules.ts b/packages/node/src/integrations/modules.ts index 50f3a3b3aa8d..6adee9e46744 100644 --- a/packages/node/src/integrations/modules.ts +++ b/packages/node/src/integrations/modules.ts @@ -1,7 +1,6 @@ import { existsSync, readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import type { IntegrationFn } from '@sentry/core'; -import { defineIntegration } from '@sentry/core'; import { isCjs } from '../utils/commonjs'; type ModuleInfo = Record; @@ -29,6 +28,7 @@ const _modulesIntegration = (() => { return event; }, + getModules: _getModules, }; }) satisfies IntegrationFn; @@ -39,7 +39,7 @@ const _modulesIntegration = (() => { * - They are extracted from the dependencies & devDependencies in the package.json file * - They are extracted from the require.cache (CJS only) */ -export const modulesIntegration = defineIntegration(_modulesIntegration); +export const modulesIntegration = _modulesIntegration; function getRequireCachePaths(): string[] { try { diff --git a/packages/node/src/integrations/tracing/vercelai/index.ts b/packages/node/src/integrations/tracing/vercelai/index.ts index d2f73e02adc3..6a9d7bdb6d53 100644 --- a/packages/node/src/integrations/tracing/vercelai/index.ts +++ b/packages/node/src/integrations/tracing/vercelai/index.ts @@ -1,9 +1,10 @@ /* eslint-disable @typescript-eslint/no-dynamic-delete */ /* eslint-disable complexity */ -import type { IntegrationFn } from '@sentry/core'; +import type { Client, IntegrationFn } from '@sentry/core'; import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON } from '@sentry/core'; import { generateInstrumentOnce } from '../../../otel/instrument'; import { addOriginToSpan } from '../../../utils/addOriginToSpan'; +import type { modulesIntegration } from '../../modules'; import { AI_MODEL_ID_ATTRIBUTE, AI_MODEL_PROVIDER_ATTRIBUTE, @@ -23,6 +24,15 @@ import type { VercelAiOptions } from './types'; export const instrumentVercelAi = generateInstrumentOnce(INTEGRATION_NAME, () => new SentryVercelAiInstrumentation({})); +/** + * Determines if the integration should be forced based on environment and package availability. + * Returns true if the 'ai' package is available. + */ +function shouldForceIntegration(client: Client): boolean { + const modules = client.getIntegrationByName>('Modules'); + return !!modules?.getModules?.()?.ai; +} + const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { let instrumentation: undefined | SentryVercelAiInstrumentation; @@ -32,7 +42,7 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { setupOnce() { instrumentation = instrumentVercelAi(); }, - setup(client) { + afterAllSetup(client) { function registerProcessors(): void { client.on('spanStart', span => { const { data: attributes, description: name } = spanToJSON(span); @@ -190,7 +200,11 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { }); } - if (options.force) { + // Auto-detect if we should force the integration when running with 'ai' package available + // Note that this can only be detected if the 'Modules' integration is available, and running in CJS mode + const shouldForce = options.force ?? shouldForceIntegration(client); + + if (shouldForce) { registerProcessors(); } else { instrumentation?.callWhenPatched(registerProcessors); @@ -213,6 +227,9 @@ const _vercelAIIntegration = ((options: VercelAiOptions = {}) => { * }); * ``` * + * The integration automatically detects when to force registration in CommonJS environments + * when the 'ai' package is available. You can still manually set the `force` option if needed. + * * By default this integration adds tracing support to all `ai` function calls. If you need to disable * collecting spans for a specific call, you can do so by setting `experimental_telemetry.isEnabled` to * `false` in the first argument of the function call. From 4e7c7eff98b5cd95165ebb4177dd758da0ee58b6 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 13 Jun 2025 11:14:10 -0400 Subject: [PATCH 31/32] feat(browser): Add detail to measure spans and add regression tests (#16557) resolves https://github.com/getsentry/sentry-javascript/issues/16237 In #16348 we had to revert the PR that added `detail` to `measure` spans as attributes. measure API: https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure detail: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMeasure/detail This was [reverted](https://github.com/getsentry/sentry-javascript/issues/16347) because it was causing issues in firefox, specifically this error was being thrown ``` Error: Permission denied to access object at _addMeasureSpans(../../node_modules/@sentry-internal/browser-utils/build/esm/metrics/browserMetrics.js:255:41) at X2e/<(../../node_modules/@sentry-internal/browser-utils/build/esm/metrics/browserMetrics.js:194:9) at addPerformanceEntries(../../node_modules/@sentry-internal/browser-utils/build/esm/metrics/browserMetrics.js:174:48) at idleSpan.beforeSpanEnd(../../node_modules/@sentry/browser/build/npm/esm/tracing/browserTracingIntegration.js:90:9) at span.endapply(../../node_modules/@sentry/browser/node_modules/@sentry/core/build/esm/tracing/idleSpan.js:52:9) at Coe/<(../../node_modules/@sentry/browser/node_modules/@sentry/core/build/esm/tracing/idleSpan.js:196:12) at sentryWrapped(../../node_modules/@sentry/browser/build/npm/esm/helpers.js:38:17) ``` From debugging, this seems to be coming from a `DOMException` being thrown @https://developer.mozilla.org/en-US/docs/Web/API/DOMException This was re-implemented, and then we added tests to validate that this wouldn't break on firefox. --------- Co-authored-by: Cursor Agent --- .../init.js | 58 +++++++++++++++++ .../test.ts | 65 +++++++++++++++++++ .../metrics/pageload-measure-spans/init.js | 1 - .../src/metrics/browserMetrics.ts | 49 +++++++++++++- 4 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js new file mode 100644 index 000000000000..f4df5dbe13e8 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/init.js @@ -0,0 +1,58 @@ +import * as Sentry from '@sentry/browser'; + +// Create measures BEFORE SDK initializes + +// Create a measure with detail +const measure = performance.measure('restricted-test-measure', { + start: performance.now(), + end: performance.now() + 1, + detail: { test: 'initial-value' }, +}); + +// Simulate Firefox's permission denial by overriding the detail getter +// This mimics the actual Firefox behavior where accessing detail throws +Object.defineProperty(measure, 'detail', { + get() { + throw new DOMException('Permission denied to access object', 'SecurityError'); + }, + configurable: false, + enumerable: true, +}); + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.browserTracingIntegration({ + idleTimeout: 9000, + }), + ], + tracesSampleRate: 1, +}); + +// Also create a normal measure to ensure SDK still works +performance.measure('normal-measure', { + start: performance.now(), + end: performance.now() + 50, + detail: 'this-should-work', +}); + +// Create a measure with complex detail object +performance.measure('complex-detail-measure', { + start: performance.now(), + end: performance.now() + 25, + detail: { + nested: { + array: [1, 2, 3], + object: { + key: 'value', + }, + }, + metadata: { + type: 'test', + version: '1.0', + tags: ['complex', 'nested', 'object'], + }, + }, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts new file mode 100644 index 000000000000..a990694b46bf --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans-domexception-details/test.ts @@ -0,0 +1,65 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/core'; +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +// This is a regression test for https://github.com/getsentry/sentry-javascript/issues/16347 + +sentryTest( + 'should handle permission denial gracefully and still create measure spans', + async ({ getLocalTestUrl, page, browserName }) => { + // Skip test on webkit because we can't validate the detail in the browser + if (shouldSkipTracingTest() || browserName === 'webkit') { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + // Find all measure spans + const measureSpans = eventData.spans?.filter(({ op }) => op === 'measure'); + expect(measureSpans?.length).toBe(3); // All three measures should create spans + + // Test 1: Verify the restricted-test-measure span exists but has no detail + const restrictedMeasure = measureSpans?.find(span => span.description === 'restricted-test-measure'); + expect(restrictedMeasure).toBeDefined(); + expect(restrictedMeasure?.data).toMatchObject({ + 'sentry.op': 'measure', + 'sentry.origin': 'auto.resource.browser.metrics', + }); + + // Verify no detail attributes were added due to the permission error + const restrictedDataKeys = Object.keys(restrictedMeasure?.data || {}); + const restrictedDetailKeys = restrictedDataKeys.filter(key => key.includes('detail')); + expect(restrictedDetailKeys).toHaveLength(0); + + // Test 2: Verify the normal measure still captures detail correctly + const normalMeasure = measureSpans?.find(span => span.description === 'normal-measure'); + expect(normalMeasure).toBeDefined(); + expect(normalMeasure?.data).toMatchObject({ + 'sentry.browser.measure.detail': 'this-should-work', + 'sentry.op': 'measure', + 'sentry.origin': 'auto.resource.browser.metrics', + }); + + // Test 3: Verify the complex detail object is captured correctly + const complexMeasure = measureSpans?.find(span => span.description === 'complex-detail-measure'); + expect(complexMeasure).toBeDefined(); + expect(complexMeasure?.data).toMatchObject({ + 'sentry.op': 'measure', + 'sentry.origin': 'auto.resource.browser.metrics', + // The entire nested object is stringified as a single value + 'sentry.browser.measure.detail.nested': JSON.stringify({ + array: [1, 2, 3], + object: { + key: 'value', + }, + }), + 'sentry.browser.measure.detail.metadata': JSON.stringify({ + type: 'test', + version: '1.0', + tags: ['complex', 'nested', 'object'], + }), + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js index db9c448ed19b..f3e6fa567911 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/pageload-measure-spans/init.js @@ -10,7 +10,6 @@ performance.measure('Next.js-before-hydration', { window.Sentry = Sentry; Sentry.init({ - debug: true, dsn: 'https://public@dsn.ingest.sentry.io/1337', integrations: [ Sentry.browserTracingIntegration({ diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 7695802941a6..4c5e78899b29 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -1,10 +1,11 @@ /* eslint-disable max-lines */ -import type { Measurements, Span, SpanAttributes, StartSpanOptions } from '@sentry/core'; +import type { Measurements, Span, SpanAttributes, SpanAttributeValue, StartSpanOptions } from '@sentry/core'; import { browserPerformanceTimeOrigin, getActiveSpan, getComponentName, htmlTreeAsString, + isPrimitive, parseUrl, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, setMeasurement, @@ -483,6 +484,8 @@ export function _addMeasureSpans( attributes['sentry.browser.measure_start_time'] = measureStartTimestamp; } + _addDetailToSpanAttributes(attributes, entry as PerformanceMeasure); + // Measurements from third parties can be off, which would create invalid spans, dropping transactions in the process. if (measureStartTimestamp <= measureEndTimestamp) { startAndEndSpan(span, measureStartTimestamp, measureEndTimestamp, { @@ -493,6 +496,50 @@ export function _addMeasureSpans( } } +function _addDetailToSpanAttributes(attributes: SpanAttributes, performanceMeasure: PerformanceMeasure): void { + try { + // Accessing detail might throw in some browsers (e.g., Firefox) due to security restrictions + const detail = performanceMeasure.detail; + + if (!detail) { + return; + } + + // Process detail based on its type + if (typeof detail === 'object') { + // Handle object details + for (const [key, value] of Object.entries(detail)) { + if (value && isPrimitive(value)) { + attributes[`sentry.browser.measure.detail.${key}`] = value as SpanAttributeValue; + } else if (value !== undefined) { + try { + // This is user defined so we can't guarantee it's serializable + attributes[`sentry.browser.measure.detail.${key}`] = JSON.stringify(value); + } catch { + // Skip values that can't be stringified + } + } + } + return; + } + + if (isPrimitive(detail)) { + // Handle primitive details + attributes['sentry.browser.measure.detail'] = detail as SpanAttributeValue; + return; + } + + try { + attributes['sentry.browser.measure.detail'] = JSON.stringify(detail); + } catch { + // Skip if stringification fails + } + } catch { + // Silently ignore any errors when accessing detail + // This handles the Firefox "Permission denied to access object" error + } +} + /** * Instrument navigation entries * exported only for tests From db2fa33f6e5e20c8c7b98c4a51d22aaf8295ea13 Mon Sep 17 00:00:00 2001 From: Andrew Liu <159852527+aliu39@users.noreply.github.com> Date: Sat, 14 Jun 2025 17:31:40 -0700 Subject: [PATCH 32/32] Remove allowEviction --- packages/core/src/utils/featureFlags.ts | 10 ++----- .../core/test/lib/utils/featureFlags.test.ts | 26 ++++--------------- 2 files changed, 7 insertions(+), 29 deletions(-) diff --git a/packages/core/src/utils/featureFlags.ts b/packages/core/src/utils/featureFlags.ts index 302885bf3792..388613394148 100644 --- a/packages/core/src/utils/featureFlags.ts +++ b/packages/core/src/utils/featureFlags.ts @@ -83,14 +83,12 @@ export function _INTERNAL_insertFlagToScope( * @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 _INTERNAL_insertToFlagBuffer( flags: FeatureFlag[], name: string, value: unknown, maxSize: number, - allowEviction: boolean = true, ): void { if (typeof value !== 'boolean') { return; @@ -110,12 +108,8 @@ export function _INTERNAL_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/core/test/lib/utils/featureFlags.test.ts b/packages/core/test/lib/utils/featureFlags.test.ts index c14a9c4240c8..14258c2caf36 100644 --- a/packages/core/test/lib/utils/featureFlags.test.ts +++ b/packages/core/test/lib/utils/featureFlags.test.ts @@ -1,7 +1,10 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { getCurrentScope } from '../../../src/currentScopes'; -import { type FeatureFlag } from '../../../src/featureFlags'; -import { _INTERNAL_insertFlagToScope, _INTERNAL_insertToFlagBuffer } from '../../../src/utils/featureFlags'; +import { + type FeatureFlag, + _INTERNAL_insertFlagToScope, + _INTERNAL_insertToFlagBuffer, +} from '../../../src/utils/featureFlags'; import { logger } from '../../../src/utils-hoist/logger'; describe('flags', () => { @@ -60,25 +63,6 @@ describe('flags', () => { ]); }); - it('drops new entries when allowEviction is false and buffer is full', () => { - const buffer: FeatureFlag[] = []; - const maxSize = 0; - _INTERNAL_insertToFlagBuffer(buffer, 'feat1', true, maxSize, false); - _INTERNAL_insertToFlagBuffer(buffer, 'feat2', true, maxSize, false); - _INTERNAL_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; - _INTERNAL_insertToFlagBuffer(buffer, 'feat1', false, maxSize, false); - _INTERNAL_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;