diff --git a/packages/core/src/globals.ts b/packages/core/src/globals.ts new file mode 100644 index 000000000000..e889b4fa0709 --- /dev/null +++ b/packages/core/src/globals.ts @@ -0,0 +1,22 @@ +import type { Scope } from '@sentry/types'; + +interface GlobalData { + globalScope?: Scope; +} + +const GLOBAL_DATA: GlobalData = {}; + +/** + * Get the global data. + */ +export function getGlobalData(): GlobalData { + return GLOBAL_DATA; +} + +/** + * Reset all global data. + * Mostly useful for tests. + */ +export function clearGlobalData(): void { + delete GLOBAL_DATA.globalScope; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 08f18f38ade6..400aac801ebb 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -42,7 +42,8 @@ export { } from './hub'; export { makeSession, closeSession, updateSession } from './session'; export { SessionFlusher } from './sessionflusher'; -export { Scope } from './scope'; +export { Scope, getGlobalScope } from './scope'; +export { clearGlobalData, getGlobalData } from './globals'; export { notifyEventProcessors, // eslint-disable-next-line deprecation/deprecation @@ -63,7 +64,7 @@ export { convertIntegrationFnToClass, } from './integration'; export { FunctionToString, InboundFilters, LinkedErrors } from './integrations'; -export { applyScopeDataToEvent } from './utils/applyScopeDataToEvent'; +export { applyScopeDataToEvent, mergeScopeData } from './utils/applyScopeDataToEvent'; export { prepareEvent } from './utils/prepareEvent'; export { createCheckInEnvelope } from './checkin'; export { hasTracingEnabled } from './utils/hasTracingEnabled'; diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 599b5c0f8d57..7919e67e2b18 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -23,11 +23,12 @@ import type { Transaction, User, } from '@sentry/types'; -import { arrayify, dateTimestampInSeconds, isPlainObject, uuid4 } from '@sentry/utils'; +import { dateTimestampInSeconds, isPlainObject, uuid4 } from '@sentry/utils'; import { getGlobalEventProcessors, notifyEventProcessors } from './eventProcessors'; +import { getGlobalData } from './globals'; import { updateSession } from './session'; -import { applyScopeDataToEvent } from './utils/applyScopeDataToEvent'; +import { applyScopeDataToEvent, mergeScopeData } from './utils/applyScopeDataToEvent'; /** * Default value for maximum number of breadcrumbs added to an event. @@ -457,7 +458,9 @@ export class Scope implements ScopeInterface { * @inheritDoc */ public getAttachments(): Attachment[] { - return this._attachments; + const data = this.getScopeData(); + + return data.attachments; } /** @@ -469,7 +472,7 @@ export class Scope implements ScopeInterface { } /** @inheritDoc */ - public getScopeData(): ScopeData { + public getPerScopeData(): ScopeData { const { _breadcrumbs, _attachments, @@ -503,6 +506,16 @@ export class Scope implements ScopeInterface { }; } + /** @inheritdoc */ + public getScopeData(): ScopeData { + const data = getGlobalScope().getPerScopeData(); + const scopeData = this.getPerScopeData(); + + mergeScopeData(data, scopeData); + + return data; + } + /** * Applies data from the scope to the event and runs all event processors on it. * @@ -570,6 +583,19 @@ export class Scope implements ScopeInterface { } } +/** + * Get the global scope. + * This scope is applied to _all_ events. + */ +export function getGlobalScope(): ScopeInterface { + const globalData = getGlobalData(); + if (!globalData.globalScope) { + globalData.globalScope = new Scope(); + } + + return globalData.globalScope; +} + function generatePropagationContext(): PropagationContext { return { traceId: uuid4(), diff --git a/packages/core/src/utils/applyScopeDataToEvent.ts b/packages/core/src/utils/applyScopeDataToEvent.ts index cc63e7c26cb6..1dab87aea866 100644 --- a/packages/core/src/utils/applyScopeDataToEvent.ts +++ b/packages/core/src/utils/applyScopeDataToEvent.ts @@ -22,6 +22,104 @@ export function applyScopeDataToEvent(event: Event, data: ScopeData): void { applySdkMetadataToEvent(event, sdkProcessingMetadata, propagationContext); } +/** Merge data of two scopes together. */ +export function mergeScopeData(data: ScopeData, mergeData: ScopeData): void { + const { + extra, + tags, + user, + contexts, + level, + sdkProcessingMetadata, + breadcrumbs, + fingerprint, + eventProcessors, + attachments, + propagationContext, + transactionName, + span, + } = mergeData; + + mergePropOverwrite(data, 'extra', extra); + mergePropOverwrite(data, 'tags', tags); + mergePropOverwrite(data, 'user', user); + mergePropOverwrite(data, 'contexts', contexts); + mergePropOverwrite(data, 'sdkProcessingMetadata', sdkProcessingMetadata); + + if (level) { + data.level = level; + } + + if (transactionName) { + data.transactionName = transactionName; + } + + if (span) { + data.span = span; + } + + if (breadcrumbs.length) { + data.breadcrumbs = [...data.breadcrumbs, ...breadcrumbs]; + } + + if (fingerprint.length) { + data.fingerprint = [...data.fingerprint, ...fingerprint]; + } + + if (eventProcessors.length) { + data.eventProcessors = [...data.eventProcessors, ...eventProcessors]; + } + + if (attachments.length) { + data.attachments = [...data.attachments, ...attachments]; + } + + data.propagationContext = { ...data.propagationContext, ...propagationContext }; +} + +/** + * Merge properties, overwriting existing keys. + * Exported only for tests. + */ +export function mergePropOverwrite< + Prop extends 'extra' | 'tags' | 'user' | 'contexts' | 'sdkProcessingMetadata', + Data extends ScopeData | Event, +>(data: Data, prop: Prop, mergeVal: Data[Prop]): void { + if (mergeVal && Object.keys(mergeVal).length) { + data[prop] = { ...data[prop], ...mergeVal }; + } +} + +/** + * Merge properties, keeping existing keys. + * Exported only for tests. + */ +export function mergePropKeep< + Prop extends 'extra' | 'tags' | 'user' | 'contexts' | 'sdkProcessingMetadata', + Data extends ScopeData | Event, +>(data: Data, prop: Prop, mergeVal: Data[Prop]): void { + if (mergeVal && Object.keys(mergeVal).length) { + data[prop] = { ...mergeVal, ...data[prop] }; + } +} + +/** Exported only for tests */ +export function mergeArray( + event: Event, + prop: Prop, + mergeVal: ScopeData[Prop], +): void { + const prevVal = event[prop]; + // If we are not merging any new values, + // we only need to proceed if there was an empty array before (as we want to replace it with undefined) + if (!mergeVal.length && (!prevVal || prevVal.length)) { + return; + } + + const merged = [...(prevVal || []), ...mergeVal] as ScopeData[Prop]; + event[prop] = merged.length ? merged : undefined; +} + function applyDataToEvent(event: Event, data: ScopeData): void { const { extra, tags, user, contexts, level, transactionName } = data; diff --git a/packages/core/test/lib/scope.test.ts b/packages/core/test/lib/scope.test.ts new file mode 100644 index 000000000000..860e6caf7980 --- /dev/null +++ b/packages/core/test/lib/scope.test.ts @@ -0,0 +1,204 @@ +import { applyScopeDataToEvent } from '@sentry/core'; +import type { Attachment, Breadcrumb, EventProcessor } from '@sentry/types'; +import { clearGlobalData } from '../../src/globals'; +import { Scope, getGlobalScope } from '../../src/scope'; + +describe('Unit | Scope', () => { + beforeEach(() => { + clearGlobalData(); + }); + + it('allows to create & update a scope', () => { + const scope = new Scope(); + + expect(scope.getScopeData()).toEqual({ + breadcrumbs: [], + attachments: [], + contexts: {}, + tags: {}, + extra: {}, + user: {}, + level: undefined, + fingerprint: [], + eventProcessors: [], + propagationContext: { + traceId: expect.any(String), + spanId: expect.any(String), + }, + sdkProcessingMetadata: {}, + }); + + scope.update({ + tags: { foo: 'bar' }, + extra: { foo2: 'bar2' }, + }); + + expect(scope.getScopeData()).toEqual({ + breadcrumbs: [], + attachments: [], + contexts: {}, + tags: { + foo: 'bar', + }, + extra: { + foo2: 'bar2', + }, + user: {}, + level: undefined, + fingerprint: [], + eventProcessors: [], + propagationContext: { + traceId: expect.any(String), + spanId: expect.any(String), + }, + sdkProcessingMetadata: {}, + }); + }); + + it('allows to clone a scope', () => { + const scope = new Scope(); + + scope.update({ + tags: { foo: 'bar' }, + extra: { foo2: 'bar2' }, + }); + + const newScope = scope.clone(); + expect(newScope).toBeInstanceOf(Scope); + expect(newScope).not.toBe(scope); + + expect(newScope.getScopeData()).toEqual({ + breadcrumbs: [], + attachments: [], + contexts: {}, + tags: { + foo: 'bar', + }, + extra: { + foo2: 'bar2', + }, + user: {}, + level: undefined, + fingerprint: [], + eventProcessors: [], + propagationContext: { + traceId: expect.any(String), + spanId: expect.any(String), + }, + sdkProcessingMetadata: {}, + }); + }); + + describe('global scope', () => { + beforeEach(() => { + clearGlobalData(); + }); + + it('works', () => { + const globalScope = getGlobalScope(); + expect(globalScope).toBeDefined(); + expect(globalScope).toBeInstanceOf(Scope); + + // Repeatedly returns the same instance + expect(getGlobalScope()).toBe(globalScope); + + globalScope.setTag('tag1', 'val1'); + globalScope.setTag('tag2', 'val2'); + + expect(globalScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + }); + }); + + describe('applyScopeDataToEvent', () => { + it('works without any data', async () => { + const scope = new Scope(); + + const event = { message: 'foo' }; + applyScopeDataToEvent(event, scope.getScopeData()); + + expect(event).toEqual({ + message: 'foo', + sdkProcessingMetadata: { + propagationContext: { + spanId: expect.any(String), + traceId: expect.any(String), + }, + }, + }); + }); + + it('merges scope data', async () => { + const breadcrumb1 = { message: '1', timestamp: 111 } as Breadcrumb; + const breadcrumb2 = { message: '2', timestamp: 222 } as Breadcrumb; + const breadcrumb3 = { message: '4', timestamp: 333 } as Breadcrumb; + + const eventProcessor1 = jest.fn((a: unknown) => a) as EventProcessor; + const eventProcessor2 = jest.fn((b: unknown) => b) as EventProcessor; + + const scope = new Scope(); + scope.update({ + user: { id: '1', email: 'test@example.com' }, + tags: { tag1: 'aa', tag2: 'aa' }, + extra: { extra1: 'aa', extra2: 'aa' }, + contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, + propagationContext: { spanId: '1', traceId: '1' }, + fingerprint: ['aa'], + }); + scope.addBreadcrumb(breadcrumb1); + scope.addEventProcessor(eventProcessor1); + scope.setSDKProcessingMetadata({ aa: 'aa' }); + + const globalScope = getGlobalScope(); + + globalScope.addBreadcrumb(breadcrumb2); + globalScope.addEventProcessor(eventProcessor2); + globalScope.setSDKProcessingMetadata({ bb: 'bb' }); + + const event = { message: 'foo', breadcrumbs: [breadcrumb3], fingerprint: ['dd'] }; + + applyScopeDataToEvent(event, scope.getScopeData()); + + expect(event).toEqual({ + message: 'foo', + user: { id: '1', email: 'test@example.com' }, + tags: { tag1: 'aa', tag2: 'aa' }, + extra: { extra1: 'aa', extra2: 'aa' }, + contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, + fingerprint: ['dd', 'aa'], + breadcrumbs: [breadcrumb3, breadcrumb2, breadcrumb1], + sdkProcessingMetadata: { + aa: 'aa', + bb: 'bb', + propagationContext: { + spanId: '1', + traceId: '1', + }, + }, + }); + }); + }); + + describe('getAttachments', () => { + it('works without any data', async () => { + const scope = new Scope(); + + const actual = scope.getAttachments(); + expect(actual).toEqual([]); + }); + + it('merges attachments data', async () => { + const attachment1 = { filename: '1' } as Attachment; + const attachment2 = { filename: '2' } as Attachment; + + const scope = new Scope(); + scope.addAttachment(attachment1); + + const globalScope = getGlobalScope(); + + globalScope.addAttachment(attachment2); + + const actual = scope.getAttachments(); + expect(actual).toEqual([attachment2, attachment1]); + }); + }); +}); diff --git a/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts b/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts new file mode 100644 index 000000000000..f4fa38ee8b8b --- /dev/null +++ b/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts @@ -0,0 +1,201 @@ +import type { Attachment, Breadcrumb, EventProcessor, ScopeData } from '@sentry/types'; +import { + mergeArray, + mergePropKeep, + mergePropOverwrite, + mergeScopeData, +} from '../../../src/utils/applyScopeDataToEvent'; + +describe('mergeArray', () => { + it.each([ + [[], [], undefined], + [undefined, [], undefined], + [['a'], [], ['a']], + [['a'], ['b', 'c'], ['a', 'b', 'c']], + [[], ['b', 'c'], ['b', 'c']], + [undefined, ['b', 'c'], ['b', 'c']], + ])('works with %s and %s', (a, b, expected) => { + const data = { fingerprint: a }; + mergeArray(data, 'fingerprint', b); + expect(data.fingerprint).toEqual(expected); + }); + + it('does not mutate the original array if no changes are made', () => { + const fingerprint = ['a']; + const data = { fingerprint }; + mergeArray(data, 'fingerprint', []); + expect(data.fingerprint).toBe(fingerprint); + }); +}); + +describe('mergePropKeep', () => { + it.each([ + [{}, {}, {}], + [{ a: 'aa' }, {}, { a: 'aa' }], + [{ a: 'aa' }, { b: 'bb' }, { a: 'aa', b: 'bb' }], + // Does not overwrite existing keys + [{ a: 'aa' }, { b: 'bb', a: 'cc' }, { a: 'aa', b: 'bb' }], + ])('works with %s and %s', (a, b, expected) => { + const data = { tags: a } as unknown as ScopeData; + mergePropKeep(data, 'tags', b); + expect(data.tags).toEqual(expected); + }); + + it('does not deep merge', () => { + const data = { + contexts: { + app: { app_version: 'v1' }, + culture: { display_name: 'name1' }, + }, + } as unknown as ScopeData; + mergePropKeep(data, 'contexts', { + os: { name: 'os1' }, + app: { app_name: 'name1' }, + }); + expect(data.contexts).toEqual({ + os: { name: 'os1' }, + culture: { display_name: 'name1' }, + app: { app_version: 'v1' }, + }); + }); + + it('does not mutate the original object if no changes are made', () => { + const tags = { a: 'aa' }; + const data = { tags } as unknown as ScopeData; + mergePropKeep(data, 'tags', {}); + expect(data.tags).toBe(tags); + }); +}); + +describe('mergePropOverwrite', () => { + it.each([ + [{}, {}, {}], + [{ a: 'aa' }, {}, { a: 'aa' }], + [{ a: 'aa' }, { b: 'bb' }, { a: 'aa', b: 'bb' }], + // overwrites existing keys + [{ a: 'aa' }, { b: 'bb', a: 'cc' }, { a: 'cc', b: 'bb' }], + ])('works with %s and %s', (a, b, expected) => { + const data = { tags: a } as unknown as ScopeData; + mergePropOverwrite(data, 'tags', b); + expect(data.tags).toEqual(expected); + }); + + it('does not deep merge', () => { + const data = { + contexts: { + app: { app_version: 'v1' }, + culture: { display_name: 'name1' }, + }, + } as unknown as ScopeData; + mergePropOverwrite(data, 'contexts', { + os: { name: 'os1' }, + app: { app_name: 'name1' }, + }); + expect(data.contexts).toEqual({ + os: { name: 'os1' }, + culture: { display_name: 'name1' }, + app: { app_name: 'name1' }, + }); + }); + + it('does not mutate the original object if no changes are made', () => { + const tags = { a: 'aa' }; + const data = { tags } as unknown as ScopeData; + mergePropOverwrite(data, 'tags', {}); + expect(data.tags).toBe(tags); + }); +}); + +describe('mergeScopeData', () => { + it('works with empty data', () => { + const data1: ScopeData = { + eventProcessors: [], + breadcrumbs: [], + user: {}, + tags: {}, + extra: {}, + contexts: {}, + attachments: [], + propagationContext: { spanId: '1', traceId: '1' }, + sdkProcessingMetadata: {}, + fingerprint: [], + }; + const data2: ScopeData = { + eventProcessors: [], + breadcrumbs: [], + user: {}, + tags: {}, + extra: {}, + contexts: {}, + attachments: [], + propagationContext: { spanId: '1', traceId: '1' }, + sdkProcessingMetadata: {}, + fingerprint: [], + }; + mergeScopeData(data1, data2); + expect(data1).toEqual({ + eventProcessors: [], + breadcrumbs: [], + user: {}, + tags: {}, + extra: {}, + contexts: {}, + attachments: [], + propagationContext: { spanId: '1', traceId: '1' }, + sdkProcessingMetadata: {}, + fingerprint: [], + }); + }); + + it('merges data correctly', () => { + const attachment1 = { filename: '1' } as Attachment; + const attachment2 = { filename: '2' } as Attachment; + const attachment3 = { filename: '3' } as Attachment; + + const breadcrumb1 = { message: '1' } as Breadcrumb; + const breadcrumb2 = { message: '2' } as Breadcrumb; + const breadcrumb3 = { message: '3' } as Breadcrumb; + + const eventProcessor1 = ((a: unknown) => null) as EventProcessor; + const eventProcessor2 = ((b: unknown) => null) as EventProcessor; + const eventProcessor3 = ((c: unknown) => null) as EventProcessor; + + const data1: ScopeData = { + eventProcessors: [eventProcessor1], + breadcrumbs: [breadcrumb1], + user: { id: '1', email: 'test@example.com' }, + tags: { tag1: 'aa', tag2: 'aa' }, + extra: { extra1: 'aa', extra2: 'aa' }, + contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, + attachments: [attachment1], + propagationContext: { spanId: '1', traceId: '1' }, + sdkProcessingMetadata: { aa: 'aa', bb: 'aa' }, + fingerprint: ['aa', 'bb'], + }; + const data2: ScopeData = { + eventProcessors: [eventProcessor2, eventProcessor3], + breadcrumbs: [breadcrumb2, breadcrumb3], + user: { id: '2', name: 'foo' }, + tags: { tag2: 'bb', tag3: 'bb' }, + extra: { extra2: 'bb', extra3: 'bb' }, + contexts: { os: { name: 'os2' } }, + attachments: [attachment2, attachment3], + propagationContext: { spanId: '2', traceId: '2' }, + sdkProcessingMetadata: { bb: 'bb', cc: 'bb' }, + fingerprint: ['cc'], + }; + mergeScopeData(data1, data2); + expect(data1).toEqual({ + eventProcessors: [eventProcessor1, eventProcessor2, eventProcessor3], + breadcrumbs: [breadcrumb1, breadcrumb2, breadcrumb3], + user: { id: '2', name: 'foo', email: 'test@example.com' }, + tags: { tag1: 'aa', tag2: 'bb', tag3: 'bb' }, + extra: { extra1: 'aa', extra2: 'bb', extra3: 'bb' }, + contexts: { os: { name: 'os2' }, culture: { display_name: 'name1' } }, + attachments: [attachment1, attachment2, attachment3], + propagationContext: { spanId: '2', traceId: '2' }, + sdkProcessingMetadata: { aa: 'aa', bb: 'bb', cc: 'bb' }, + fingerprint: ['aa', 'bb', 'cc'], + }); + }); +}); diff --git a/packages/hub/test/scope.test.ts b/packages/hub/test/scope.test.ts index a039a839dcc6..a2be18b0526d 100644 --- a/packages/hub/test/scope.test.ts +++ b/packages/hub/test/scope.test.ts @@ -237,8 +237,6 @@ describe('Scope', () => { describe('applyToEvent', () => { test('basic usage', async () => { - expect.assertions(9); - const scope = new Scope(); scope.setExtra('a', 2); scope.setTag('a', 'b'); @@ -251,20 +249,20 @@ describe('Scope', () => { scope.setSDKProcessingMetadata({ dogs: 'are great!' }); const event: Event = {}; - return scope.applyToEvent(event).then(processedEvent => { - expect(processedEvent!.extra).toEqual({ a: 2 }); - expect(processedEvent!.tags).toEqual({ a: 'b' }); - expect(processedEvent!.user).toEqual({ id: '1' }); - expect(processedEvent!.fingerprint).toEqual(['abcd']); - expect(processedEvent!.level).toEqual('warning'); - expect(processedEvent!.transaction).toEqual('/abc'); - expect(processedEvent!.breadcrumbs![0]).toHaveProperty('message', 'test'); - expect(processedEvent!.contexts).toEqual({ os: { id: '1' } }); - expect(processedEvent!.sdkProcessingMetadata).toEqual({ - dogs: 'are great!', - // @ts-expect-error accessing private property for test - propagationContext: scope._propagationContext, - }); + const processedEvent = await scope.applyToEvent(event); + + expect(processedEvent!.extra).toEqual({ a: 2 }); + expect(processedEvent!.tags).toEqual({ a: 'b' }); + expect(processedEvent!.user).toEqual({ id: '1' }); + expect(processedEvent!.fingerprint).toEqual(['abcd']); + expect(processedEvent!.level).toEqual('warning'); + expect(processedEvent!.transaction).toEqual('/abc'); + expect(processedEvent!.breadcrumbs![0]).toHaveProperty('message', 'test'); + expect(processedEvent!.contexts).toEqual({ os: { id: '1' } }); + expect(processedEvent!.sdkProcessingMetadata).toEqual({ + dogs: 'are great!', + // @ts-expect-error accessing private property for test + propagationContext: scope._propagationContext, }); }); @@ -360,7 +358,6 @@ describe('Scope', () => { }); test('adds trace context', async () => { - expect.assertions(1); const scope = new Scope(); const span = { fake: 'span', @@ -368,9 +365,8 @@ describe('Scope', () => { } as any; scope.setSpan(span); const event: Event = {}; - return scope.applyToEvent(event).then(processedEvent => { - expect((processedEvent!.contexts!.trace as any).a).toEqual('b'); - }); + const processedEvent = await scope.applyToEvent(event); + expect((processedEvent!.contexts!.trace as any).a).toEqual('b'); }); test('existing trace context in event should take precedence', async () => { diff --git a/packages/node-experimental/src/sdk/scope.ts b/packages/node-experimental/src/sdk/scope.ts index 5bf220708a6a..cb1247ed5b31 100644 --- a/packages/node-experimental/src/sdk/scope.ts +++ b/packages/node-experimental/src/sdk/scope.ts @@ -1,5 +1,6 @@ +import { getGlobalData, mergeScopeData } from '@sentry/core'; import { OpenTelemetryScope } from '@sentry/opentelemetry'; -import type { Attachment, Breadcrumb, Client, Event, EventHint, Severity, SeverityLevel } from '@sentry/types'; +import type { Breadcrumb, Client, Event, EventHint, Severity, SeverityLevel } from '@sentry/types'; import { uuid4 } from '@sentry/utils'; import { getGlobalCarrier } from './globals'; @@ -18,15 +19,25 @@ export function setCurrentScope(scope: Scope): void { getScopes().scope = scope; } -/** Get the global scope. */ +/** + * Get the global scope. + * We overwrite this from the core implementation to make sure we get the correct Scope class. + */ export function getGlobalScope(): Scope { - const carrier = getGlobalCarrier(); + const globalData = getGlobalData(); + + if (!globalData.globalScope) { + globalData.globalScope = new Scope(); + } - if (!carrier.globalScope) { - carrier.globalScope = new Scope(); + // If we have a default Scope here by chance, make sure to "upgrade" it to our custom Scope + if (!(globalData.globalScope instanceof Scope)) { + const oldScope = globalData.globalScope; + globalData.globalScope = new Scope(); + globalData.globalScope.update(oldScope); } - return carrier.globalScope as Scope; + return globalData.globalScope as Scope; } /** Get the currently active isolation scope. */ @@ -104,13 +115,6 @@ export class Scope extends OpenTelemetryScope implements ScopeInterface { return this._client; } - /** @inheritdoc */ - public getAttachments(): Attachment[] { - const data = this.getScopeData(); - - return data.attachments; - } - /** Capture an exception for this scope. */ public captureException(exception: unknown, hint?: EventHint): string { const eventId = hint && hint.event_id ? hint.event_id : uuid4(); @@ -183,37 +187,6 @@ export class Scope extends OpenTelemetryScope implements ScopeInterface { return this._addBreadcrumb(breadcrumb, maxBreadcrumbs); } - /** Get all relevant data for this scope. */ - public getPerScopeData(): ScopeData { - const { - _breadcrumbs, - _attachments, - _contexts, - _tags, - _extra, - _user, - _level, - _fingerprint, - _eventProcessors, - _propagationContext, - _sdkProcessingMetadata, - } = this; - - return { - breadcrumbs: _breadcrumbs, - attachments: _attachments, - contexts: _contexts, - tags: _tags, - extra: _extra, - user: _user, - level: _level, - fingerprint: _fingerprint || [], - eventProcessors: _eventProcessors, - propagationContext: _propagationContext, - sdkProcessingMetadata: _sdkProcessingMetadata, - }; - } - /** @inheritdoc */ public getScopeData(): ScopeData { const data = getGlobalScope().getPerScopeData(); @@ -221,8 +194,8 @@ export class Scope extends OpenTelemetryScope implements ScopeInterface { const scopeData = this.getPerScopeData(); // Merge data together, in order - mergeData(data, isolationScopeData); - mergeData(data, scopeData); + mergeScopeData(data, isolationScopeData); + mergeScopeData(data, scopeData); return data; } @@ -233,94 +206,6 @@ export class Scope extends OpenTelemetryScope implements ScopeInterface { } } -/** Exported only for tests */ -export function mergeData(data: ScopeData, mergeData: ScopeData): void { - const { - extra, - tags, - user, - contexts, - level, - sdkProcessingMetadata, - breadcrumbs, - fingerprint, - eventProcessors, - attachments, - propagationContext, - } = mergeData; - - mergePropOverwrite(data, 'extra', extra); - mergePropOverwrite(data, 'tags', tags); - mergePropOverwrite(data, 'user', user); - mergePropOverwrite(data, 'contexts', contexts); - mergePropOverwrite(data, 'sdkProcessingMetadata', sdkProcessingMetadata); - - if (level) { - data.level = level; - } - - if (breadcrumbs.length) { - data.breadcrumbs = [...data.breadcrumbs, ...breadcrumbs]; - } - - if (fingerprint.length) { - data.fingerprint = [...data.fingerprint, ...fingerprint]; - } - - if (eventProcessors.length) { - data.eventProcessors = [...data.eventProcessors, ...eventProcessors]; - } - - if (attachments.length) { - data.attachments = [...data.attachments, ...attachments]; - } - - data.propagationContext = { ...data.propagationContext, ...propagationContext }; -} - -/** - * Merge properties, overwriting existing keys. - * Exported only for tests. - */ -export function mergePropOverwrite< - Prop extends 'extra' | 'tags' | 'user' | 'contexts' | 'sdkProcessingMetadata', - Data extends ScopeData | Event, ->(data: Data, prop: Prop, mergeVal: Data[Prop]): void { - if (mergeVal && Object.keys(mergeVal).length) { - data[prop] = { ...data[prop], ...mergeVal }; - } -} - -/** - * Merge properties, keeping existing keys. - * Exported only for tests. - */ -export function mergePropKeep< - Prop extends 'extra' | 'tags' | 'user' | 'contexts' | 'sdkProcessingMetadata', - Data extends ScopeData | Event, ->(data: Data, prop: Prop, mergeVal: Data[Prop]): void { - if (mergeVal && Object.keys(mergeVal).length) { - data[prop] = { ...mergeVal, ...data[prop] }; - } -} - -/** Exported only for tests */ -export function mergeArray( - event: Event, - prop: Prop, - mergeVal: ScopeData[Prop], -): void { - const prevVal = event[prop]; - // If we are not merging any new values, - // we only need to proceed if there was an empty array before (as we want to replace it with undefined) - if (!mergeVal.length && (!prevVal || prevVal.length)) { - return; - } - - const merged = [...(prevVal || []), ...mergeVal] as ScopeData[Prop]; - event[prop] = merged.length ? merged : undefined; -} - function getScopes(): CurrentScopes { const carrier = getGlobalCarrier(); diff --git a/packages/node-experimental/src/sdk/types.ts b/packages/node-experimental/src/sdk/types.ts index 773c404d65ce..140056e583ca 100644 --- a/packages/node-experimental/src/sdk/types.ts +++ b/packages/node-experimental/src/sdk/types.ts @@ -74,7 +74,6 @@ export interface AsyncContextStrategy { } export interface SentryCarrier { - globalScope?: Scope; scopes?: CurrentScopes; acs?: AsyncContextStrategy; diff --git a/packages/node-experimental/test/integration/scope.test.ts b/packages/node-experimental/test/integration/scope.test.ts index 57be6126bcae..d5f1d3156a11 100644 --- a/packages/node-experimental/test/integration/scope.test.ts +++ b/packages/node-experimental/test/integration/scope.test.ts @@ -1,3 +1,4 @@ +import { clearGlobalData } from '@sentry/core'; import { getCurrentHub, getSpanScope } from '@sentry/opentelemetry'; import * as Sentry from '../../src/'; @@ -230,7 +231,7 @@ describe('Integration | Scope', () => { describe('global scope', () => { beforeEach(() => { - resetGlobals(); + clearGlobalData(); }); it('works before calling init', () => { @@ -245,14 +246,14 @@ describe('Integration | Scope', () => { globalScope.setTag('tag1', 'val1'); globalScope.setTag('tag2', 'val2'); - expect(globalScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + expect(globalScope.getPerScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); // Now when we call init, the global scope remains intact Sentry.init({ dsn: 'https://username@domain/123', defaultIntegrations: false }); expect(globalScope.getClient()).toBeDefined(); expect(Sentry.getGlobalScope()).toBe(globalScope); - expect(globalScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + expect(globalScope.getPerScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); }); it('is applied to events', async () => { @@ -303,7 +304,7 @@ describe('Integration | Scope', () => { isolationScope.setTag('tag1', 'val1'); isolationScope.setTag('tag2', 'val2'); - expect(isolationScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + expect(isolationScope.getPerScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); // Now when we call init, the isolation scope remains intact Sentry.init({ dsn: 'https://username@domain/123', defaultIntegrations: false }); @@ -311,7 +312,7 @@ describe('Integration | Scope', () => { // client is only attached to global scope by default expect(isolationScope.getClient()).toBeUndefined(); expect(Sentry.getIsolationScope()).toBe(isolationScope); - expect(isolationScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + expect(isolationScope.getPerScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); }); it('is applied to events', async () => { @@ -367,13 +368,13 @@ describe('Integration | Scope', () => { expect(newIsolationScope).not.toBe(initialIsolationScope); // Data is forked off original isolation scope - expect(newIsolationScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + expect(newIsolationScope.getPerScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); newIsolationScope.setTag('tag3', 'val3'); Sentry.captureException(error); }); - expect(initialIsolationScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + expect(initialIsolationScope.getPerScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); await client.flush(); diff --git a/packages/node-experimental/test/sdk/scope.test.ts b/packages/node-experimental/test/sdk/scope.test.ts index 9fb5c02bea69..d1cc12b7463b 100644 --- a/packages/node-experimental/test/sdk/scope.test.ts +++ b/packages/node-experimental/test/sdk/scope.test.ts @@ -1,8 +1,7 @@ import { applyScopeDataToEvent } from '@sentry/core'; import type { Attachment, Breadcrumb, Client, EventProcessor } from '@sentry/types'; import { Scope, getIsolationScope } from '../../src'; -import { getGlobalScope, mergeArray, mergeData, mergePropKeep, mergePropOverwrite } from '../../src/sdk/scope'; -import type { ScopeData } from '../../src/sdk/types'; +import { getGlobalScope } from '../../src/sdk/scope'; import { mockSdkInit, resetGlobals } from '../helpers/mockSdkInit'; describe('Unit | Scope', () => { @@ -109,200 +108,6 @@ describe('Unit | Scope', () => { expect(scope['_getIsolationScope']()).toBe(customIsolationScope); }); - describe('mergeArray', () => { - it.each([ - [[], [], undefined], - [undefined, [], undefined], - [['a'], [], ['a']], - [['a'], ['b', 'c'], ['a', 'b', 'c']], - [[], ['b', 'c'], ['b', 'c']], - [undefined, ['b', 'c'], ['b', 'c']], - ])('works with %s and %s', (a, b, expected) => { - const data = { fingerprint: a }; - mergeArray(data, 'fingerprint', b); - expect(data.fingerprint).toEqual(expected); - }); - - it('does not mutate the original array if no changes are made', () => { - const fingerprint = ['a']; - const data = { fingerprint }; - mergeArray(data, 'fingerprint', []); - expect(data.fingerprint).toBe(fingerprint); - }); - }); - - describe('mergePropKeep', () => { - it.each([ - [{}, {}, {}], - [{ a: 'aa' }, {}, { a: 'aa' }], - [{ a: 'aa' }, { b: 'bb' }, { a: 'aa', b: 'bb' }], - // Does not overwrite existing keys - [{ a: 'aa' }, { b: 'bb', a: 'cc' }, { a: 'aa', b: 'bb' }], - ])('works with %s and %s', (a, b, expected) => { - const data = { tags: a } as unknown as ScopeData; - mergePropKeep(data, 'tags', b); - expect(data.tags).toEqual(expected); - }); - - it('does not deep merge', () => { - const data = { - contexts: { - app: { app_version: 'v1' }, - culture: { display_name: 'name1' }, - }, - } as unknown as ScopeData; - mergePropKeep(data, 'contexts', { - os: { name: 'os1' }, - app: { app_name: 'name1' }, - }); - expect(data.contexts).toEqual({ - os: { name: 'os1' }, - culture: { display_name: 'name1' }, - app: { app_version: 'v1' }, - }); - }); - - it('does not mutate the original object if no changes are made', () => { - const tags = { a: 'aa' }; - const data = { tags } as unknown as ScopeData; - mergePropKeep(data, 'tags', {}); - expect(data.tags).toBe(tags); - }); - }); - - describe('mergePropOverwrite', () => { - it.each([ - [{}, {}, {}], - [{ a: 'aa' }, {}, { a: 'aa' }], - [{ a: 'aa' }, { b: 'bb' }, { a: 'aa', b: 'bb' }], - // overwrites existing keys - [{ a: 'aa' }, { b: 'bb', a: 'cc' }, { a: 'cc', b: 'bb' }], - ])('works with %s and %s', (a, b, expected) => { - const data = { tags: a } as unknown as ScopeData; - mergePropOverwrite(data, 'tags', b); - expect(data.tags).toEqual(expected); - }); - - it('does not deep merge', () => { - const data = { - contexts: { - app: { app_version: 'v1' }, - culture: { display_name: 'name1' }, - }, - } as unknown as ScopeData; - mergePropOverwrite(data, 'contexts', { - os: { name: 'os1' }, - app: { app_name: 'name1' }, - }); - expect(data.contexts).toEqual({ - os: { name: 'os1' }, - culture: { display_name: 'name1' }, - app: { app_name: 'name1' }, - }); - }); - - it('does not mutate the original object if no changes are made', () => { - const tags = { a: 'aa' }; - const data = { tags } as unknown as ScopeData; - mergePropOverwrite(data, 'tags', {}); - expect(data.tags).toBe(tags); - }); - }); - - describe('mergeData', () => { - it('works with empty data', () => { - const data1: ScopeData = { - eventProcessors: [], - breadcrumbs: [], - user: {}, - tags: {}, - extra: {}, - contexts: {}, - attachments: [], - propagationContext: { spanId: '1', traceId: '1' }, - sdkProcessingMetadata: {}, - fingerprint: [], - }; - const data2: ScopeData = { - eventProcessors: [], - breadcrumbs: [], - user: {}, - tags: {}, - extra: {}, - contexts: {}, - attachments: [], - propagationContext: { spanId: '1', traceId: '1' }, - sdkProcessingMetadata: {}, - fingerprint: [], - }; - mergeData(data1, data2); - expect(data1).toEqual({ - eventProcessors: [], - breadcrumbs: [], - user: {}, - tags: {}, - extra: {}, - contexts: {}, - attachments: [], - propagationContext: { spanId: '1', traceId: '1' }, - sdkProcessingMetadata: {}, - fingerprint: [], - }); - }); - - it('merges data correctly', () => { - const attachment1 = { filename: '1' } as Attachment; - const attachment2 = { filename: '2' } as Attachment; - const attachment3 = { filename: '3' } as Attachment; - - const breadcrumb1 = { message: '1' } as Breadcrumb; - const breadcrumb2 = { message: '2' } as Breadcrumb; - const breadcrumb3 = { message: '3' } as Breadcrumb; - - const eventProcessor1 = ((a: unknown) => null) as EventProcessor; - const eventProcessor2 = ((b: unknown) => null) as EventProcessor; - const eventProcessor3 = ((c: unknown) => null) as EventProcessor; - - const data1: ScopeData = { - eventProcessors: [eventProcessor1], - breadcrumbs: [breadcrumb1], - user: { id: '1', email: 'test@example.com' }, - tags: { tag1: 'aa', tag2: 'aa' }, - extra: { extra1: 'aa', extra2: 'aa' }, - contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, - attachments: [attachment1], - propagationContext: { spanId: '1', traceId: '1' }, - sdkProcessingMetadata: { aa: 'aa', bb: 'aa' }, - fingerprint: ['aa', 'bb'], - }; - const data2: ScopeData = { - eventProcessors: [eventProcessor2, eventProcessor3], - breadcrumbs: [breadcrumb2, breadcrumb3], - user: { id: '2', name: 'foo' }, - tags: { tag2: 'bb', tag3: 'bb' }, - extra: { extra2: 'bb', extra3: 'bb' }, - contexts: { os: { name: 'os2' } }, - attachments: [attachment2, attachment3], - propagationContext: { spanId: '2', traceId: '2' }, - sdkProcessingMetadata: { bb: 'bb', cc: 'bb' }, - fingerprint: ['cc'], - }; - mergeData(data1, data2); - expect(data1).toEqual({ - eventProcessors: [eventProcessor1, eventProcessor2, eventProcessor3], - breadcrumbs: [breadcrumb1, breadcrumb2, breadcrumb3], - user: { id: '2', name: 'foo', email: 'test@example.com' }, - tags: { tag1: 'aa', tag2: 'bb', tag3: 'bb' }, - extra: { extra1: 'aa', extra2: 'bb', extra3: 'bb' }, - contexts: { os: { name: 'os2' }, culture: { display_name: 'name1' } }, - attachments: [attachment1, attachment2, attachment3], - propagationContext: { spanId: '2', traceId: '2' }, - sdkProcessingMetadata: { aa: 'aa', bb: 'bb', cc: 'bb' }, - fingerprint: ['aa', 'bb', 'cc'], - }); - }); - }); - describe('applyToEvent', () => { it('works without any data', async () => { mockSdkInit(); diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index 50ef4da5987f..8cee38ca50ab 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -53,6 +53,9 @@ export interface Scope { /** Get the data of this scope, which is applied to an event during processing. */ getScopeData(): ScopeData; + /** Get the data of this scope only, ignoring all other related scopes. */ + getPerScopeData(): ScopeData; + /** * Updates user context information for future events. *