Skip to content

Commit eb9bc56

Browse files
authored
feat(core): Add getGlobalScope() method (#9920)
This scope lives in module scope and is applied to _all_ events. Please review this carefully, as it is important that data is correctly applied etc. There should be a decent amount of tests covering all of this, but just to make sure. This was mostly ported/extracted from node-experimental.
1 parent 6173846 commit eb9bc56

File tree

14 files changed

+847
-427
lines changed

14 files changed

+847
-427
lines changed

packages/core/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export {
4242
} from './hub';
4343
export { makeSession, closeSession, updateSession } from './session';
4444
export { SessionFlusher } from './sessionflusher';
45-
export { Scope } from './scope';
45+
export { Scope, getGlobalScope, setGlobalScope } from './scope';
4646
export {
4747
notifyEventProcessors,
4848
// eslint-disable-next-line deprecation/deprecation
@@ -63,7 +63,7 @@ export {
6363
convertIntegrationFnToClass,
6464
} from './integration';
6565
export { FunctionToString, InboundFilters, LinkedErrors } from './integrations';
66-
export { applyScopeDataToEvent } from './utils/applyScopeDataToEvent';
66+
export { applyScopeDataToEvent, mergeScopeData } from './utils/applyScopeDataToEvent';
6767
export { prepareEvent } from './utils/prepareEvent';
6868
export { createCheckInEnvelope } from './checkin';
6969
export { hasTracingEnabled } from './utils/hasTracingEnabled';

packages/core/src/scope.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import type {
2323
Transaction,
2424
User,
2525
} from '@sentry/types';
26-
import { arrayify, dateTimestampInSeconds, isPlainObject, uuid4 } from '@sentry/utils';
26+
import { dateTimestampInSeconds, isPlainObject, uuid4 } from '@sentry/utils';
2727

2828
import { getGlobalEventProcessors, notifyEventProcessors } from './eventProcessors';
2929
import { updateSession } from './session';
@@ -34,6 +34,12 @@ import { applyScopeDataToEvent } from './utils/applyScopeDataToEvent';
3434
*/
3535
const DEFAULT_MAX_BREADCRUMBS = 100;
3636

37+
/**
38+
* The global scope is kept in this module.
39+
* When accessing this via `getGlobalScope()` we'll make sure to set one if none is currently present.
40+
*/
41+
let globalScope: ScopeInterface | undefined;
42+
3743
/**
3844
* Holds additional event information. {@link Scope.applyToEvent} will be
3945
* called by the client before an event will be sent.
@@ -455,9 +461,12 @@ export class Scope implements ScopeInterface {
455461

456462
/**
457463
* @inheritDoc
464+
* @deprecated Use `getScopeData()` instead.
458465
*/
459466
public getAttachments(): Attachment[] {
460-
return this._attachments;
467+
const data = this.getScopeData();
468+
469+
return data.attachments;
461470
}
462471

463472
/**
@@ -570,6 +579,27 @@ export class Scope implements ScopeInterface {
570579
}
571580
}
572581

582+
/**
583+
* Get the global scope.
584+
* This scope is applied to _all_ events.
585+
*/
586+
export function getGlobalScope(): ScopeInterface {
587+
if (!globalScope) {
588+
globalScope = new Scope();
589+
}
590+
591+
return globalScope;
592+
}
593+
594+
/**
595+
* This is mainly needed for tests.
596+
* DO NOT USE this, as this is an internal API and subject to change.
597+
* @hidden
598+
*/
599+
export function setGlobalScope(scope: ScopeInterface | undefined): void {
600+
globalScope = scope;
601+
}
602+
573603
function generatePropagationContext(): PropagationContext {
574604
return {
575605
traceId: uuid4(),

packages/core/src/utils/applyScopeDataToEvent.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,104 @@ export function applyScopeDataToEvent(event: Event, data: ScopeData): void {
2222
applySdkMetadataToEvent(event, sdkProcessingMetadata, propagationContext);
2323
}
2424

25+
/** Merge data of two scopes together. */
26+
export function mergeScopeData(data: ScopeData, mergeData: ScopeData): void {
27+
const {
28+
extra,
29+
tags,
30+
user,
31+
contexts,
32+
level,
33+
sdkProcessingMetadata,
34+
breadcrumbs,
35+
fingerprint,
36+
eventProcessors,
37+
attachments,
38+
propagationContext,
39+
transactionName,
40+
span,
41+
} = mergeData;
42+
43+
mergePropOverwrite(data, 'extra', extra);
44+
mergePropOverwrite(data, 'tags', tags);
45+
mergePropOverwrite(data, 'user', user);
46+
mergePropOverwrite(data, 'contexts', contexts);
47+
mergePropOverwrite(data, 'sdkProcessingMetadata', sdkProcessingMetadata);
48+
49+
if (level) {
50+
data.level = level;
51+
}
52+
53+
if (transactionName) {
54+
data.transactionName = transactionName;
55+
}
56+
57+
if (span) {
58+
data.span = span;
59+
}
60+
61+
if (breadcrumbs.length) {
62+
data.breadcrumbs = [...data.breadcrumbs, ...breadcrumbs];
63+
}
64+
65+
if (fingerprint.length) {
66+
data.fingerprint = [...data.fingerprint, ...fingerprint];
67+
}
68+
69+
if (eventProcessors.length) {
70+
data.eventProcessors = [...data.eventProcessors, ...eventProcessors];
71+
}
72+
73+
if (attachments.length) {
74+
data.attachments = [...data.attachments, ...attachments];
75+
}
76+
77+
data.propagationContext = { ...data.propagationContext, ...propagationContext };
78+
}
79+
80+
/**
81+
* Merge properties, overwriting existing keys.
82+
* Exported only for tests.
83+
*/
84+
export function mergePropOverwrite<
85+
Prop extends 'extra' | 'tags' | 'user' | 'contexts' | 'sdkProcessingMetadata',
86+
Data extends ScopeData | Event,
87+
>(data: Data, prop: Prop, mergeVal: Data[Prop]): void {
88+
if (mergeVal && Object.keys(mergeVal).length) {
89+
data[prop] = { ...data[prop], ...mergeVal };
90+
}
91+
}
92+
93+
/**
94+
* Merge properties, keeping existing keys.
95+
* Exported only for tests.
96+
*/
97+
export function mergePropKeep<
98+
Prop extends 'extra' | 'tags' | 'user' | 'contexts' | 'sdkProcessingMetadata',
99+
Data extends ScopeData | Event,
100+
>(data: Data, prop: Prop, mergeVal: Data[Prop]): void {
101+
if (mergeVal && Object.keys(mergeVal).length) {
102+
data[prop] = { ...mergeVal, ...data[prop] };
103+
}
104+
}
105+
106+
/** Exported only for tests */
107+
export function mergeArray<Prop extends 'breadcrumbs' | 'fingerprint'>(
108+
event: Event,
109+
prop: Prop,
110+
mergeVal: ScopeData[Prop],
111+
): void {
112+
const prevVal = event[prop];
113+
// If we are not merging any new values,
114+
// we only need to proceed if there was an empty array before (as we want to replace it with undefined)
115+
if (!mergeVal.length && (!prevVal || prevVal.length)) {
116+
return;
117+
}
118+
119+
const merged = [...(prevVal || []), ...mergeVal] as ScopeData[Prop];
120+
event[prop] = merged.length ? merged : undefined;
121+
}
122+
25123
function applyDataToEvent(event: Event, data: ScopeData): void {
26124
const { extra, tags, user, contexts, level, transactionName } = data;
27125

packages/core/src/utils/prepareEvent.ts

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import { GLOBAL_OBJ, addExceptionMechanism, dateTimestampInSeconds, normalize, t
1313

1414
import { DEFAULT_ENVIRONMENT } from '../constants';
1515
import { getGlobalEventProcessors, notifyEventProcessors } from '../eventProcessors';
16-
import { Scope } from '../scope';
17-
import { applyScopeDataToEvent } from './applyScopeDataToEvent';
16+
import { Scope, getGlobalScope } from '../scope';
17+
import { applyScopeDataToEvent, mergeScopeData } from './applyScopeDataToEvent';
1818

1919
/**
2020
* This type makes sure that we get either a CaptureContext, OR an EventHint.
@@ -74,36 +74,32 @@ export function prepareEvent(
7474
}
7575

7676
const clientEventProcessors = client && client.getEventProcessors ? client.getEventProcessors() : [];
77-
// TODO (v8): Update this order to be: Global > Client > Scope
78-
const eventProcessors = [
79-
...clientEventProcessors,
80-
// eslint-disable-next-line deprecation/deprecation
81-
...getGlobalEventProcessors(),
82-
];
8377

8478
// This should be the last thing called, since we want that
8579
// {@link Hub.addEventProcessor} gets the finished prepared event.
86-
//
87-
// We need to check for the existence of `finalScope.getAttachments`
88-
// because `getAttachments` can be undefined if users are using an older version
89-
// of `@sentry/core` that does not have the `getAttachments` method.
90-
// See: https://github.com/getsentry/sentry-javascript/issues/5229
80+
// Merge scope data together
81+
const data = getGlobalScope().getScopeData();
82+
9183
if (finalScope) {
92-
// Collect attachments from the hint and scope
93-
if (finalScope.getAttachments) {
94-
const attachments = [...(hint.attachments || []), ...finalScope.getAttachments()];
84+
const finalScopeData = finalScope.getScopeData();
85+
mergeScopeData(data, finalScopeData);
86+
}
9587

96-
if (attachments.length) {
97-
hint.attachments = attachments;
98-
}
99-
}
88+
const attachments = [...(hint.attachments || []), ...data.attachments];
89+
if (attachments.length) {
90+
hint.attachments = attachments;
91+
}
10092

101-
const scopeData = finalScope.getScopeData();
102-
applyScopeDataToEvent(prepared, scopeData);
93+
applyScopeDataToEvent(prepared, data);
10394

95+
// TODO (v8): Update this order to be: Global > Client > Scope
96+
const eventProcessors = [
97+
...clientEventProcessors,
98+
// eslint-disable-next-line deprecation/deprecation
99+
...getGlobalEventProcessors(),
104100
// Run scope event processors _after_ all other processors
105-
eventProcessors.push(...scopeData.eventProcessors);
106-
}
101+
...data.eventProcessors,
102+
];
107103

108104
const result = notifyEventProcessors(eventProcessors, prepared, hint);
109105

packages/core/test/lib/base.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Client, Envelope, Event, Span, Transaction } from '@sentry/types';
22
import { SentryError, SyncPromise, dsnToString, logger } from '@sentry/utils';
33

4-
import { Hub, Scope, makeSession } from '../../src';
4+
import { Hub, Scope, makeSession, setGlobalScope } from '../../src';
55
import * as integrationModule from '../../src/integration';
66
import { TestClient, getDefaultTestClientOptions } from '../mocks/client';
77
import { AdHocIntegration, TestIntegration } from '../mocks/integration';
@@ -54,6 +54,7 @@ describe('BaseClient', () => {
5454
beforeEach(() => {
5555
TestClient.sendEventCalled = undefined;
5656
TestClient.instance = undefined;
57+
setGlobalScope(undefined);
5758
});
5859

5960
afterEach(() => {
@@ -756,7 +757,8 @@ describe('BaseClient', () => {
756757
expect(TestClient.instance!.event!).toEqual(
757758
expect.objectContaining({
758759
breadcrumbs: [normalizedBreadcrumb, normalizedBreadcrumb, normalizedBreadcrumb],
759-
contexts: normalizedObject,
760+
// also has trace context from global scope
761+
contexts: { ...normalizedObject, trace: expect.anything() },
760762
environment: 'production',
761763
event_id: '42',
762764
extra: normalizedObject,
@@ -805,7 +807,8 @@ describe('BaseClient', () => {
805807
expect(TestClient.instance!.event!).toEqual(
806808
expect.objectContaining({
807809
breadcrumbs: [normalizedBreadcrumb, normalizedBreadcrumb, normalizedBreadcrumb],
808-
contexts: normalizedObject,
810+
// also has trace context from global scope
811+
contexts: { ...normalizedObject, trace: expect.anything() },
809812
environment: 'production',
810813
event_id: '42',
811814
extra: normalizedObject,
@@ -859,7 +862,8 @@ describe('BaseClient', () => {
859862
expect(TestClient.instance!.event!).toEqual(
860863
expect.objectContaining({
861864
breadcrumbs: [normalizedBreadcrumb, normalizedBreadcrumb, normalizedBreadcrumb],
862-
contexts: normalizedObject,
865+
// also has trace context from global scope
866+
contexts: { ...normalizedObject, trace: expect.anything() },
863867
environment: 'production',
864868
event_id: '42',
865869
extra: normalizedObject,

0 commit comments

Comments
 (0)