diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index ff89c0d593a9..65e5821a3d1f 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -21,7 +21,13 @@ import type { SeverityLevel, User, } from '@sentry/types'; -import { dateTimestampInSeconds, generatePropagationContext, isPlainObject, logger, uuid4 } from '@sentry/utils'; +import { + dateTimestampInSeconds, + generatePropagationContext, + isPlainObject, + logger, + uuid4, +} from '@sentry/utils'; import { updateSession } from './session'; import { _getSpanForScope, _setSpanForScope } from './utils/spanOnScope'; @@ -122,6 +128,13 @@ class ScopeClass implements ScopeInterface { newScope._tags = { ...this._tags }; newScope._extra = { ...this._extra }; newScope._contexts = { ...this._contexts }; + if (this._contexts.flags) { + // The flags context needs a deep copy. + newScope._contexts.flags = { + values: [...this._contexts.flags.values] + } + } + newScope._user = this._user; newScope._level = this._level; newScope._session = this._session; diff --git a/packages/types/src/context.ts b/packages/types/src/context.ts index 10fc61420e25..3e757928923a 100644 --- a/packages/types/src/context.ts +++ b/packages/types/src/context.ts @@ -1,5 +1,6 @@ import type { Primitive } from './misc'; import type { SpanOrigin } from './span'; +import type { FeatureFlag } from './flags' export type Context = Record; @@ -13,6 +14,7 @@ export interface Contexts extends Record { cloud_resource?: CloudResourceContext; state?: StateContext; profile?: ProfileContext; + flags?: FeatureFlagContext; } export interface StateContext extends Record { @@ -124,3 +126,8 @@ export interface MissingInstrumentationContext extends Record { package: string; ['javascript.is_cjs']?: boolean; } + +export interface FeatureFlagContext extends Record { + // This should only be modified by @sentry/util methods (insertToFlagBuffer). + readonly values: FeatureFlag[]; +} diff --git a/packages/types/src/flags.ts b/packages/types/src/flags.ts new file mode 100644 index 000000000000..c117fbd0d686 --- /dev/null +++ b/packages/types/src/flags.ts @@ -0,0 +1,2 @@ +// Key names match the type used by Sentry frontend. +export type FeatureFlag = { readonly flag: string; readonly result: boolean }; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index b100c1e9c26a..bf187a15edb5 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -56,6 +56,7 @@ export type { Event, EventHint, EventType, ErrorEvent, TransactionEvent } from ' export type { EventProcessor } from './eventprocessor'; export type { Exception } from './exception'; export type { Extra, Extras } from './extra'; +export type { FeatureFlag } from './flags'; // eslint-disable-next-line deprecation/deprecation export type { Hub } from './hub'; export type { Integration, IntegrationClass, IntegrationFn } from './integration'; diff --git a/packages/utils/src/flags.ts b/packages/utils/src/flags.ts new file mode 100644 index 000000000000..2cc02b989577 --- /dev/null +++ b/packages/utils/src/flags.ts @@ -0,0 +1,38 @@ +import type { FeatureFlag } from '@sentry/types'; + +/** + * 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. + */ + +export const FLAG_BUFFER_SIZE = 100; + +/** + * Insert into a FeatureFlag array while maintaining ordered LRU properties. + * After inserting: + * - The flag is guaranteed to be at the end of `flags`. + * - No other flags with the same name exist in `flags`. + * - The length of `flags` does not exceed FLAG_BUFFER_SIZE. If needed, the + * oldest inserted flag is evicted. + */ +export function insertToFlagBuffer(flags: FeatureFlag[], name: string, value: boolean): void { + // Check if the flag is already in the buffer + 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 === FLAG_BUFFER_SIZE) { + // If at capacity, pop the earliest flag - O(n) + flags.shift(); + } + + // Push the flag to the end - O(1) + flags.push({ + flag: name, + result: value, + }); +} diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 4a2d68ca0d8b..1f8a86960a03 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -40,3 +40,4 @@ export * from './buildPolyfills'; export * from './propagationContext'; export * from './vercelWaitUntil'; export * from './version'; +export * from './flags';