Skip to content

Commit

Permalink
feat(core): Add getGlobalScope() method (#9920)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mydea committed Dec 21, 2023
1 parent 6173846 commit eb9bc56
Show file tree
Hide file tree
Showing 14 changed files with 847 additions and 427 deletions.
4 changes: 2 additions & 2 deletions packages/core/src/index.ts
Expand Up @@ -42,7 +42,7 @@ export {
} from './hub';
export { makeSession, closeSession, updateSession } from './session';
export { SessionFlusher } from './sessionflusher';
export { Scope } from './scope';
export { Scope, getGlobalScope, setGlobalScope } from './scope';
export {
notifyEventProcessors,
// eslint-disable-next-line deprecation/deprecation
Expand All @@ -63,7 +63,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';
Expand Down
34 changes: 32 additions & 2 deletions packages/core/src/scope.ts
Expand Up @@ -23,7 +23,7 @@ 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 { updateSession } from './session';
Expand All @@ -34,6 +34,12 @@ import { applyScopeDataToEvent } from './utils/applyScopeDataToEvent';
*/
const DEFAULT_MAX_BREADCRUMBS = 100;

/**
* The global scope is kept in this module.
* When accessing this via `getGlobalScope()` we'll make sure to set one if none is currently present.
*/
let globalScope: ScopeInterface | undefined;

/**
* Holds additional event information. {@link Scope.applyToEvent} will be
* called by the client before an event will be sent.
Expand Down Expand Up @@ -455,9 +461,12 @@ export class Scope implements ScopeInterface {

/**
* @inheritDoc
* @deprecated Use `getScopeData()` instead.
*/
public getAttachments(): Attachment[] {
return this._attachments;
const data = this.getScopeData();

return data.attachments;
}

/**
Expand Down Expand Up @@ -570,6 +579,27 @@ export class Scope implements ScopeInterface {
}
}

/**
* Get the global scope.
* This scope is applied to _all_ events.
*/
export function getGlobalScope(): ScopeInterface {
if (!globalScope) {
globalScope = new Scope();
}

return globalScope;
}

/**
* This is mainly needed for tests.
* DO NOT USE this, as this is an internal API and subject to change.
* @hidden
*/
export function setGlobalScope(scope: ScopeInterface | undefined): void {
globalScope = scope;
}

function generatePropagationContext(): PropagationContext {
return {
traceId: uuid4(),
Expand Down
98 changes: 98 additions & 0 deletions packages/core/src/utils/applyScopeDataToEvent.ts
Expand Up @@ -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<Prop extends 'breadcrumbs' | 'fingerprint'>(
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;

Expand Down
44 changes: 20 additions & 24 deletions packages/core/src/utils/prepareEvent.ts
Expand Up @@ -13,8 +13,8 @@ import { GLOBAL_OBJ, addExceptionMechanism, dateTimestampInSeconds, normalize, t

import { DEFAULT_ENVIRONMENT } from '../constants';
import { getGlobalEventProcessors, notifyEventProcessors } from '../eventProcessors';
import { Scope } from '../scope';
import { applyScopeDataToEvent } from './applyScopeDataToEvent';
import { Scope, getGlobalScope } from '../scope';
import { applyScopeDataToEvent, mergeScopeData } from './applyScopeDataToEvent';

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

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

// This should be the last thing called, since we want that
// {@link Hub.addEventProcessor} gets the finished prepared event.
//
// We need to check for the existence of `finalScope.getAttachments`
// because `getAttachments` can be undefined if users are using an older version
// of `@sentry/core` that does not have the `getAttachments` method.
// See: https://github.com/getsentry/sentry-javascript/issues/5229
// Merge scope data together
const data = getGlobalScope().getScopeData();

if (finalScope) {
// Collect attachments from the hint and scope
if (finalScope.getAttachments) {
const attachments = [...(hint.attachments || []), ...finalScope.getAttachments()];
const finalScopeData = finalScope.getScopeData();
mergeScopeData(data, finalScopeData);
}

if (attachments.length) {
hint.attachments = attachments;
}
}
const attachments = [...(hint.attachments || []), ...data.attachments];
if (attachments.length) {
hint.attachments = attachments;
}

const scopeData = finalScope.getScopeData();
applyScopeDataToEvent(prepared, scopeData);
applyScopeDataToEvent(prepared, data);

// TODO (v8): Update this order to be: Global > Client > Scope
const eventProcessors = [
...clientEventProcessors,
// eslint-disable-next-line deprecation/deprecation
...getGlobalEventProcessors(),
// Run scope event processors _after_ all other processors
eventProcessors.push(...scopeData.eventProcessors);
}
...data.eventProcessors,
];

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

Expand Down
12 changes: 8 additions & 4 deletions packages/core/test/lib/base.test.ts
@@ -1,7 +1,7 @@
import type { Client, Envelope, Event, Span, Transaction } from '@sentry/types';
import { SentryError, SyncPromise, dsnToString, logger } from '@sentry/utils';

import { Hub, Scope, makeSession } from '../../src';
import { Hub, Scope, makeSession, setGlobalScope } from '../../src';
import * as integrationModule from '../../src/integration';
import { TestClient, getDefaultTestClientOptions } from '../mocks/client';
import { AdHocIntegration, TestIntegration } from '../mocks/integration';
Expand Down Expand Up @@ -54,6 +54,7 @@ describe('BaseClient', () => {
beforeEach(() => {
TestClient.sendEventCalled = undefined;
TestClient.instance = undefined;
setGlobalScope(undefined);
});

afterEach(() => {
Expand Down Expand Up @@ -756,7 +757,8 @@ describe('BaseClient', () => {
expect(TestClient.instance!.event!).toEqual(
expect.objectContaining({
breadcrumbs: [normalizedBreadcrumb, normalizedBreadcrumb, normalizedBreadcrumb],
contexts: normalizedObject,
// also has trace context from global scope
contexts: { ...normalizedObject, trace: expect.anything() },
environment: 'production',
event_id: '42',
extra: normalizedObject,
Expand Down Expand Up @@ -805,7 +807,8 @@ describe('BaseClient', () => {
expect(TestClient.instance!.event!).toEqual(
expect.objectContaining({
breadcrumbs: [normalizedBreadcrumb, normalizedBreadcrumb, normalizedBreadcrumb],
contexts: normalizedObject,
// also has trace context from global scope
contexts: { ...normalizedObject, trace: expect.anything() },
environment: 'production',
event_id: '42',
extra: normalizedObject,
Expand Down Expand Up @@ -859,7 +862,8 @@ describe('BaseClient', () => {
expect(TestClient.instance!.event!).toEqual(
expect.objectContaining({
breadcrumbs: [normalizedBreadcrumb, normalizedBreadcrumb, normalizedBreadcrumb],
contexts: normalizedObject,
// also has trace context from global scope
contexts: { ...normalizedObject, trace: expect.anything() },
environment: 'production',
event_id: '42',
extra: normalizedObject,
Expand Down

0 comments on commit eb9bc56

Please sign in to comment.