From 8184a5472e4a18f8b11873123ee1d940b64317c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Tue, 2 Jun 2020 10:59:23 +0200 Subject: [PATCH] feat: Explicit Scope for captureException and captureMessage (#2627) * feat: Explicit Scope for captureException and captureMessage --- CHANGELOG.md | 17 +- packages/browser/test/package/test-code.js | 12 + packages/core/src/baseclient.ts | 94 +-- packages/core/test/lib/base.test.ts | 86 ++- packages/core/test/mocks/backend.ts | 6 +- packages/hub/src/scope.ts | 62 +- packages/hub/test/hub.test.ts | 26 +- packages/hub/test/scope.test.ts | 694 ++++++++++++--------- packages/minimal/src/index.ts | 14 +- packages/minimal/test/lib/minimal.test.ts | 43 +- packages/types/src/event.ts | 2 + packages/types/src/index.ts | 2 +- packages/types/src/scope.ts | 22 + 13 files changed, 719 insertions(+), 361 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5553bb6e10ba..c1b2cc7ef323 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +- [minimal/core] feat: Allow for explicit scope through 2nd argument to `captureException/captureMessage` (#2627) ## 5.16.0 @@ -12,15 +13,15 @@ - [browser] fix: Call wrapped `RequestAnimationFrame` with correct context (#2570) - [node] fix: Prevent reading the same source file multiple times (#2569) - [integrations] feat: Vue performance monitoring (#2571) -- [apm] fix: Use proper type name for op #2584 -- [core] fix: sent_at for envelope headers to use same clock #2597 -- [apm] fix: Improve bundle size by moving span status to @sentry/apm #2589 -- [apm] feat: No longer discard transactions instead mark them deadline exceeded #2588 -- [apm] feat: Introduce `Sentry.startTransaction` and `Transaction.startChild` #2600 -- [apm] feat: Transactions no longer go through `beforeSend` #2600 +- [apm] fix: Use proper type name for op (#2584) +- [core] fix: sent_at for envelope headers to use same clock (#2597) +- [apm] fix: Improve bundle size by moving span status to @sentry/apm (#2589) +- [apm] feat: No longer discard transactions instead mark them deadline exceeded (#2588) +- [apm] feat: Introduce `Sentry.startTransaction` and `Transaction.startChild` (#2600) +- [apm] feat: Transactions no longer go through `beforeSend` (#2600) - [browser] fix: Emit Sentry Request breadcrumbs from inside the client (#2615) -- [apm] fix: No longer debounce IdleTransaction #2618 -- [apm] feat: Add pageload transaction option + fixes #2623 +- [apm] fix: No longer debounce IdleTransaction (#2618) +- [apm] feat: Add pageload transaction option + fixes (#2623) ## 5.15.5 diff --git a/packages/browser/test/package/test-code.js b/packages/browser/test/package/test-code.js index 8da49b4b1c5b..ebb03ffb9733 100644 --- a/packages/browser/test/package/test-code.js +++ b/packages/browser/test/package/test-code.js @@ -50,7 +50,19 @@ Sentry.addBreadcrumb({ // Capture methods Sentry.captureException(new Error('foo')); +Sentry.captureException(new Error('foo'), { + tags: { + foo: 1, + }, +}); +Sentry.captureException(new Error('foo'), scope => scope); Sentry.captureMessage('bar'); +Sentry.captureMessage('bar', { + tags: { + foo: 1, + }, +}); +Sentry.captureMessage('bar', scope => scope); // Scope behavior Sentry.withScope(scope => { diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 62461411559c..8052e6e91605 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -1,5 +1,5 @@ import { Scope } from '@sentry/hub'; -import { Client, Event, EventHint, Integration, IntegrationClass, Options, SdkInfo, Severity } from '@sentry/types'; +import { Client, Event, EventHint, Integration, IntegrationClass, Options, Severity } from '@sentry/types'; import { Dsn, isPrimitive, @@ -248,54 +248,31 @@ export abstract class BaseClient implement * @returns A new event with more information. */ protected _prepareEvent(event: Event, scope?: Scope, hint?: EventHint): PromiseLike { - const { environment, release, dist, maxValueLength = 250, normalizeDepth = 3 } = this.getOptions(); - - const prepared: Event = { ...event }; - - if (!prepared.timestamp) { - prepared.timestamp = timestampWithMs(); - } - - if (prepared.environment === undefined && environment !== undefined) { - prepared.environment = environment; - } - - if (prepared.release === undefined && release !== undefined) { - prepared.release = release; - } - - if (prepared.dist === undefined && dist !== undefined) { - prepared.dist = dist; - } - - if (prepared.message) { - prepared.message = truncate(prepared.message, maxValueLength); - } - - const exception = prepared.exception && prepared.exception.values && prepared.exception.values[0]; - if (exception && exception.value) { - exception.value = truncate(exception.value, maxValueLength); - } + const { normalizeDepth = 3 } = this.getOptions(); + const prepared: Event = { + ...event, + event_id: event.event_id || (hint && hint.event_id ? hint.event_id : uuid4()), + timestamp: event.timestamp || timestampWithMs(), + }; - const request = prepared.request; - if (request && request.url) { - request.url = truncate(request.url, maxValueLength); - } + this._applyClientOptions(prepared); + this._applyIntegrationsMetadata(prepared); - if (prepared.event_id === undefined) { - prepared.event_id = hint && hint.event_id ? hint.event_id : uuid4(); + // If we have scope given to us, use it as the base for further modifications. + // This allows us to prevent unnecessary copying of data if `captureContext` is not provided. + let finalScope = scope; + if (hint && hint.captureContext) { + finalScope = Scope.clone(finalScope).update(hint.captureContext); } - this._addIntegrations(prepared.sdk); - // We prepare the result here with a resolved Event. let result = SyncPromise.resolve(prepared); // This should be the last thing called, since we want that // {@link Hub.addEventProcessor} gets the finished prepared event. - if (scope) { + if (finalScope) { // In case we have a hub we reassign it. - result = scope.applyToEvent(prepared, hint); + result = finalScope.applyToEvent(prepared, hint); } return result.then(evt => { @@ -345,11 +322,48 @@ export abstract class BaseClient implement }; } + /** + * Enhances event using the client configuration. + * It takes care of all "static" values like environment, release and `dist`, + * as well as truncating overly long values. + * @param event event instance to be enhanced + */ + protected _applyClientOptions(event: Event): void { + const { environment, release, dist, maxValueLength = 250 } = this.getOptions(); + + if (event.environment === undefined && environment !== undefined) { + event.environment = environment; + } + + if (event.release === undefined && release !== undefined) { + event.release = release; + } + + if (event.dist === undefined && dist !== undefined) { + event.dist = dist; + } + + if (event.message) { + event.message = truncate(event.message, maxValueLength); + } + + const exception = event.exception && event.exception.values && event.exception.values[0]; + if (exception && exception.value) { + exception.value = truncate(exception.value, maxValueLength); + } + + const request = event.request; + if (request && request.url) { + request.url = truncate(request.url, maxValueLength); + } + } + /** * This function adds all used integrations to the SDK info in the event. * @param sdkInfo The sdkInfo of the event that will be filled with all integrations. */ - protected _addIntegrations(sdkInfo?: SdkInfo): void { + protected _applyIntegrationsMetadata(event: Event): void { + const sdkInfo = event.sdk; const integrationsArray = Object.keys(this._integrations); if (sdkInfo && integrationsArray.length > 0) { sdkInfo.integrations = integrationsArray; diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index 8638b6ebf8c4..43a89423b623 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -1,5 +1,5 @@ import { Hub, Scope } from '@sentry/hub'; -import { Event } from '@sentry/types'; +import { Event, Severity } from '@sentry/types'; import { SentryError } from '@sentry/utils'; import { TestBackend } from '../mocks/backend'; @@ -163,9 +163,8 @@ describe('BaseClient', () => { }); }); - describe('captures', () => { + describe('captureException', () => { test('captures and sends exceptions', () => { - expect.assertions(1); const client = new TestClient({ dsn: PUBLIC_DSN }); client.captureException(new Error('test exception')); expect(TestBackend.instance!.event).toEqual({ @@ -182,19 +181,69 @@ describe('BaseClient', () => { }); }); + test('allows for providing explicit scope', () => { + const client = new TestClient({ dsn: PUBLIC_DSN }); + const scope = new Scope(); + scope.setExtra('foo', 'wat'); + client.captureException( + new Error('test exception'), + { + captureContext: { + extra: { + bar: 'wat', + }, + }, + }, + scope, + ); + expect(TestBackend.instance!.event).toEqual( + expect.objectContaining({ + extra: { + bar: 'wat', + foo: 'wat', + }, + }), + ); + }); + + test('allows for clearing data from existing scope if explicit one does so in a callback function', () => { + const client = new TestClient({ dsn: PUBLIC_DSN }); + const scope = new Scope(); + scope.setExtra('foo', 'wat'); + client.captureException( + new Error('test exception'), + { + captureContext: s => { + s.clear(); + s.setExtra('bar', 'wat'); + return s; + }, + }, + scope, + ); + expect(TestBackend.instance!.event).toEqual( + expect.objectContaining({ + extra: { + bar: 'wat', + }, + }), + ); + }); + }); + + describe('captureMessage', () => { test('captures and sends messages', () => { - expect.assertions(1); const client = new TestClient({ dsn: PUBLIC_DSN }); client.captureMessage('test message'); expect(TestBackend.instance!.event).toEqual({ event_id: '42', + level: 'info', message: 'test message', timestamp: 2020, }); }); test('should call eventFromException if input to captureMessage is not a primitive', () => { - expect.assertions(2); const client = new TestClient({ dsn: PUBLIC_DSN }); const spy = jest.spyOn(TestBackend.instance!, 'eventFromException'); @@ -209,6 +258,33 @@ describe('BaseClient', () => { client.captureMessage([] as any); expect(spy.mock.calls.length).toEqual(2); }); + + test('allows for providing explicit scope', () => { + const client = new TestClient({ dsn: PUBLIC_DSN }); + const scope = new Scope(); + scope.setExtra('foo', 'wat'); + client.captureMessage( + 'test message', + Severity.Warning, + { + captureContext: { + extra: { + bar: 'wat', + }, + }, + }, + scope, + ); + expect(TestBackend.instance!.event).toEqual( + expect.objectContaining({ + extra: { + bar: 'wat', + foo: 'wat', + }, + level: 'warning', + }), + ); + }); }); describe('captureEvent() / prepareEvent()', () => { diff --git a/packages/core/test/mocks/backend.ts b/packages/core/test/mocks/backend.ts index dfd9efa5da12..ed079bfae607 100644 --- a/packages/core/test/mocks/backend.ts +++ b/packages/core/test/mocks/backend.ts @@ -1,4 +1,4 @@ -import { Event, Options, Transport } from '@sentry/types'; +import { Event, Options, Severity, Transport } from '@sentry/types'; import { SyncPromise } from '@sentry/utils'; import { BaseBackend } from '../../src/basebackend'; @@ -50,8 +50,8 @@ export class TestBackend extends BaseBackend { }); } - public eventFromMessage(message: string): PromiseLike { - return SyncPromise.resolve({ message }); + public eventFromMessage(message: string, level: Severity = Severity.Info): PromiseLike { + return SyncPromise.resolve({ message, level }); } public sendEvent(event: Event): void { diff --git a/packages/hub/src/scope.ts b/packages/hub/src/scope.ts index d64923dbf19e..3fdbd20a65cc 100644 --- a/packages/hub/src/scope.ts +++ b/packages/hub/src/scope.ts @@ -1,14 +1,16 @@ import { Breadcrumb, + CaptureContext, Event, EventHint, EventProcessor, Scope as ScopeInterface, + ScopeContext, Severity, Span, User, } from '@sentry/types'; -import { getGlobalObject, isThenable, SyncPromise, timestampWithMs } from '@sentry/utils'; +import { getGlobalObject, isPlainObject, isThenable, SyncPromise, timestampWithMs } from '@sentry/utils'; /** * Holds additional event information. {@link Scope.applyToEvent} will be @@ -37,7 +39,7 @@ export class Scope implements ScopeInterface { protected _extra: { [key: string]: any } = {}; /** Contexts */ - protected _context: { [key: string]: any } = {}; + protected _contexts: { [key: string]: any } = {}; /** Fingerprint */ protected _fingerprint?: string[]; @@ -193,7 +195,7 @@ export class Scope implements ScopeInterface { * @inheritDoc */ public setContext(key: string, context: { [key: string]: any } | null): this { - this._context = { ...this._context, [key]: context }; + this._contexts = { ...this._contexts, [key]: context }; this._notifyScopeListeners(); return this; } @@ -225,7 +227,7 @@ export class Scope implements ScopeInterface { newScope._breadcrumbs = [...scope._breadcrumbs]; newScope._tags = { ...scope._tags }; newScope._extra = { ...scope._extra }; - newScope._context = { ...scope._context }; + newScope._contexts = { ...scope._contexts }; newScope._user = scope._user; newScope._level = scope._level; newScope._span = scope._span; @@ -236,6 +238,52 @@ export class Scope implements ScopeInterface { return newScope; } + /** + * @inheritDoc + */ + public update(captureContext?: CaptureContext): this { + if (!captureContext) { + return this; + } + + if (typeof captureContext === 'function') { + const updatedScope = (captureContext as ((scope: T) => T))(this); + return updatedScope instanceof Scope ? updatedScope : this; + } + + if (captureContext instanceof Scope) { + this._tags = { ...this._tags, ...captureContext._tags }; + this._extra = { ...this._extra, ...captureContext._extra }; + this._contexts = { ...this._contexts, ...captureContext._contexts }; + if (captureContext._user) { + this._user = captureContext._user; + } + if (captureContext._level) { + this._level = captureContext._level; + } + if (captureContext._fingerprint) { + this._fingerprint = captureContext._fingerprint; + } + } else if (isPlainObject(captureContext)) { + // tslint:disable-next-line:no-parameter-reassignment + captureContext = captureContext as ScopeContext; + this._tags = { ...this._tags, ...captureContext.tags }; + this._extra = { ...this._extra, ...captureContext.extra }; + this._contexts = { ...this._contexts, ...captureContext.contexts }; + if (captureContext.user) { + this._user = captureContext.user; + } + if (captureContext.level) { + this._level = captureContext.level; + } + if (captureContext.fingerprint) { + this._fingerprint = captureContext.fingerprint; + } + } + + return this; + } + /** * @inheritDoc */ @@ -244,7 +292,7 @@ export class Scope implements ScopeInterface { this._tags = {}; this._extra = {}; this._user = {}; - this._context = {}; + this._contexts = {}; this._level = undefined; this._transaction = undefined; this._fingerprint = undefined; @@ -320,8 +368,8 @@ export class Scope implements ScopeInterface { if (this._user && Object.keys(this._user).length) { event.user = { ...this._user, ...event.user }; } - if (this._context && Object.keys(this._context).length) { - event.contexts = { ...this._context, ...event.contexts }; + if (this._contexts && Object.keys(this._contexts).length) { + event.contexts = { ...this._contexts, ...event.contexts }; } if (this._level) { event.level = this._level; diff --git a/packages/hub/test/hub.test.ts b/packages/hub/test/hub.test.ts index f42e0f631428..fd8b33a4ddde 100644 --- a/packages/hub/test/hub.test.ts +++ b/packages/hub/test/hub.test.ts @@ -136,22 +136,20 @@ describe('Hub', () => { }); describe('configureScope', () => { - test('no client, should not invoke configureScope', () => { - expect.assertions(0); - const hub = new Hub(); - hub.configureScope(_ => { - expect(true).toBeFalsy(); - }); - }); - - test('no client, should not invoke configureScope', () => { - expect.assertions(1); + test('should have an access to provide scope', () => { const localScope = new Scope(); localScope.setExtra('a', 'b'); - const hub = new Hub({ a: 'b' } as any, localScope); - hub.configureScope(confScope => { - expect((confScope as any)._extra).toEqual({ a: 'b' }); - }); + const hub = new Hub({} as any, localScope); + const cb = jest.fn(); + hub.configureScope(cb); + expect(cb).toHaveBeenCalledWith(localScope); + }); + + test('should not invoke without client and scope', () => { + const hub = new Hub(); + const cb = jest.fn(); + hub.configureScope(cb); + expect(cb).not.toHaveBeenCalled(); }); }); diff --git a/packages/hub/test/scope.test.ts b/packages/hub/test/scope.test.ts index 3628961d0409..e69856487b09 100644 --- a/packages/hub/test/scope.test.ts +++ b/packages/hub/test/scope.test.ts @@ -10,238 +10,258 @@ describe('Scope', () => { getGlobalObject().__SENTRY__.globalEventProcessors = undefined; }); - describe('fingerprint', () => { - test('set', () => { + describe('attributes modification', () => { + test('setFingerprint', () => { const scope = new Scope(); scope.setFingerprint(['abcd']); expect((scope as any)._fingerprint).toEqual(['abcd']); }); - }); - describe('extra', () => { - test('set key value', () => { + test('setExtra', () => { const scope = new Scope(); scope.setExtra('a', 1); expect((scope as any)._extra).toEqual({ a: 1 }); }); - test('set object', () => { + test('setExtras', () => { const scope = new Scope(); scope.setExtras({ a: 1 }); expect((scope as any)._extra).toEqual({ a: 1 }); }); - test('set undefined', () => { + test('setExtras with undefined overrides the value', () => { const scope = new Scope(); scope.setExtra('a', 1); scope.setExtras({ a: undefined }); expect((scope as any)._extra).toEqual({ a: undefined }); }); - }); - describe('tags', () => { - test('set key value', () => { + test('setTag', () => { const scope = new Scope(); scope.setTag('a', 'b'); expect((scope as any)._tags).toEqual({ a: 'b' }); }); - test('set object', () => { + test('setTags', () => { const scope = new Scope(); scope.setTags({ a: 'b' }); expect((scope as any)._tags).toEqual({ a: 'b' }); }); - }); - describe('user', () => { - test('set', () => { + test('setUser', () => { const scope = new Scope(); scope.setUser({ id: '1' }); expect((scope as any)._user).toEqual({ id: '1' }); }); - test('unset', () => { + + test('setUser with null unsets the user', () => { const scope = new Scope(); scope.setUser({ id: '1' }); scope.setUser(null); expect((scope as any)._user).toEqual({}); }); - }); - describe('level', () => { - test('add', () => { + test('addBreadcrumb', () => { const scope = new Scope(); scope.addBreadcrumb({ message: 'test' }, 100); expect((scope as any)._breadcrumbs[0]).toHaveProperty('message', 'test'); }); - test('set', () => { + + test('setLevel', () => { const scope = new Scope(); scope.setLevel(Severity.Critical); expect((scope as any)._level).toEqual(Severity.Critical); }); - }); - describe('transaction', () => { - test('set', () => { + test('setTransaction', () => { const scope = new Scope(); scope.setTransaction('/abc'); expect((scope as any)._transaction).toEqual('/abc'); }); - test('unset', () => { + + test('setTransaction with no value unsets it', () => { const scope = new Scope(); scope.setTransaction('/abc'); scope.setTransaction(); expect((scope as any)._transaction).toBeUndefined(); }); - }); - describe('context', () => { - test('set', () => { + test('setContext', () => { const scope = new Scope(); scope.setContext('os', { id: '1' }); - expect((scope as any)._context.os).toEqual({ id: '1' }); + expect((scope as any)._contexts.os).toEqual({ id: '1' }); }); - test('unset', () => { + + test('setContext with null unsets it', () => { const scope = new Scope(); scope.setContext('os', { id: '1' }); scope.setContext('os', null); expect((scope as any)._user).toEqual({}); }); - }); - describe('span', () => { - test('set', () => { + test('setSpan', () => { const scope = new Scope(); const span = { fake: 'span' } as any; scope.setSpan(span); expect((scope as any)._span).toEqual(span); }); - test('unset', () => { + + test('setSpan with no value unsets it', () => { const scope = new Scope(); scope.setSpan({ fake: 'span' } as any); scope.setSpan(); expect((scope as any)._span).toEqual(undefined); }); - }); - test('chaining', () => { - const scope = new Scope(); - scope.setLevel(Severity.Critical).setUser({ id: '1' }); - expect((scope as any)._level).toEqual(Severity.Critical); - expect((scope as any)._user).toEqual({ id: '1' }); + test('chaining', () => { + const scope = new Scope(); + scope.setLevel(Severity.Critical).setUser({ id: '1' }); + expect((scope as any)._level).toEqual(Severity.Critical); + expect((scope as any)._user).toEqual({ id: '1' }); + }); }); - test('basic inheritance', () => { - const parentScope = new Scope(); - parentScope.setExtra('a', 1); - const scope = Scope.clone(parentScope); - expect((parentScope as any)._extra).toEqual((scope as any)._extra); - }); + describe('clone', () => { + test('basic inheritance', () => { + const parentScope = new Scope(); + parentScope.setExtra('a', 1); + const scope = Scope.clone(parentScope); + expect((parentScope as any)._extra).toEqual((scope as any)._extra); + }); - test('parent changed inheritance', () => { - const parentScope = new Scope(); - const scope = Scope.clone(parentScope); - parentScope.setExtra('a', 2); - expect((scope as any)._extra).toEqual({}); - expect((parentScope as any)._extra).toEqual({ a: 2 }); - }); + test('parent changed inheritance', () => { + const parentScope = new Scope(); + const scope = Scope.clone(parentScope); + parentScope.setExtra('a', 2); + expect((scope as any)._extra).toEqual({}); + expect((parentScope as any)._extra).toEqual({ a: 2 }); + }); - test('child override inheritance', () => { - const parentScope = new Scope(); - parentScope.setExtra('a', 1); + test('child override inheritance', () => { + const parentScope = new Scope(); + parentScope.setExtra('a', 1); - const scope = Scope.clone(parentScope); - scope.setExtra('a', 2); - expect((parentScope as any)._extra).toEqual({ a: 1 }); - expect((scope as any)._extra).toEqual({ a: 2 }); + const scope = Scope.clone(parentScope); + scope.setExtra('a', 2); + expect((parentScope as any)._extra).toEqual({ a: 1 }); + expect((scope as any)._extra).toEqual({ a: 2 }); + }); }); - test('applyToEvent', () => { - expect.assertions(8); - const scope = new Scope(); - scope.setExtra('a', 2); - scope.setTag('a', 'b'); - scope.setUser({ id: '1' }); - scope.setFingerprint(['abcd']); - scope.setLevel(Severity.Warning); - scope.setTransaction('/abc'); - scope.addBreadcrumb({ message: 'test' }, 100); - scope.setContext('os', { id: '1' }); - 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' } }); + describe('applyToEvent', () => { + test('basic usage', () => { + expect.assertions(8); + const scope = new Scope(); + scope.setExtra('a', 2); + scope.setTag('a', 'b'); + scope.setUser({ id: '1' }); + scope.setFingerprint(['abcd']); + scope.setLevel(Severity.Warning); + scope.setTransaction('/abc'); + scope.addBreadcrumb({ message: 'test' }, 100); + scope.setContext('os', { id: '1' }); + 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' } }); + }); }); - }); - test('applyToEvent merge', () => { - expect.assertions(8); - const scope = new Scope(); - scope.setExtra('a', 2); - scope.setTag('a', 'b'); - scope.setUser({ id: '1' }); - scope.setFingerprint(['abcd']); - scope.addBreadcrumb({ message: 'test' }, 100); - scope.setContext('server', { id: '2' }); - const event: Event = { - breadcrumbs: [{ message: 'test1' }], - contexts: { os: { id: '1' } }, - extra: { b: 3 }, - fingerprint: ['efgh'], - tags: { b: 'c' }, - user: { id: '3' }, - }; - return scope.applyToEvent(event).then(processedEvent => { - expect(processedEvent!.extra).toEqual({ a: 2, b: 3 }); - expect(processedEvent!.tags).toEqual({ a: 'b', b: 'c' }); - expect(processedEvent!.user).toEqual({ id: '3' }); - expect(processedEvent!.fingerprint).toEqual(['efgh', 'abcd']); - expect(processedEvent!.breadcrumbs).toHaveLength(2); - expect(processedEvent!.breadcrumbs![0]).toHaveProperty('message', 'test1'); - expect(processedEvent!.breadcrumbs![1]).toHaveProperty('message', 'test'); - expect(processedEvent!.contexts).toEqual({ - os: { id: '1' }, - server: { id: '2' }, + test('merge with existing event data', () => { + expect.assertions(8); + const scope = new Scope(); + scope.setExtra('a', 2); + scope.setTag('a', 'b'); + scope.setUser({ id: '1' }); + scope.setFingerprint(['abcd']); + scope.addBreadcrumb({ message: 'test' }, 100); + scope.setContext('server', { id: '2' }); + const event: Event = { + breadcrumbs: [{ message: 'test1' }], + contexts: { os: { id: '1' } }, + extra: { b: 3 }, + fingerprint: ['efgh'], + tags: { b: 'c' }, + user: { id: '3' }, + }; + return scope.applyToEvent(event).then(processedEvent => { + expect(processedEvent!.extra).toEqual({ a: 2, b: 3 }); + expect(processedEvent!.tags).toEqual({ a: 'b', b: 'c' }); + expect(processedEvent!.user).toEqual({ id: '3' }); + expect(processedEvent!.fingerprint).toEqual(['efgh', 'abcd']); + expect(processedEvent!.breadcrumbs).toHaveLength(2); + expect(processedEvent!.breadcrumbs![0]).toHaveProperty('message', 'test1'); + expect(processedEvent!.breadcrumbs![1]).toHaveProperty('message', 'test'); + expect(processedEvent!.contexts).toEqual({ + os: { id: '1' }, + server: { id: '2' }, + }); }); }); - }); - test('applyToEvent message fingerprint', async () => { - expect.assertions(1); - const scope = new Scope(); - const event: Event = { - fingerprint: ['bar'], - message: 'foo', - }; - return scope.applyToEvent(event).then(processedEvent => { - expect(processedEvent!.fingerprint).toEqual(['bar']); + test('should make sure that fingerprint is always array', async () => { + const scope = new Scope(); + const event: Event = {}; + + // @ts-ignore + event.fingerprint = 'foo'; + await scope.applyToEvent(event).then(processedEvent => { + expect(processedEvent!.fingerprint).toEqual(['foo']); + }); + + // @ts-ignore + event.fingerprint = 'bar'; + await scope.applyToEvent(event).then(processedEvent => { + expect(processedEvent!.fingerprint).toEqual(['bar']); + }); }); - }); - test('applyToEvent scope level should be stronger', () => { - expect.assertions(1); - const scope = new Scope(); - scope.setLevel(Severity.Warning); - const event: Event = {}; - event.level = Severity.Critical; - return scope.applyToEvent(event).then(processedEvent => { - expect(processedEvent!.level).toEqual('warning'); + test('should merge fingerprint from event and scope', async () => { + const scope = new Scope(); + scope.setFingerprint(['foo']); + const event: Event = { + fingerprint: ['bar'], + }; + + await scope.applyToEvent(event).then(processedEvent => { + expect(processedEvent!.fingerprint).toEqual(['bar', 'foo']); + }); }); - }); - test('applyToEvent scope transaction should be stronger', () => { - expect.assertions(1); - const scope = new Scope(); - scope.setTransaction('/abc'); - const event: Event = {}; - event.transaction = '/cdf'; - return scope.applyToEvent(event).then(processedEvent => { - expect(processedEvent!.transaction).toEqual('/abc'); + test('should remove default empty fingerprint array if theres no data available', async () => { + const scope = new Scope(); + const event: Event = {}; + await scope.applyToEvent(event).then(processedEvent => { + expect(processedEvent!.fingerprint).toEqual(undefined); + }); + }); + + test('scope level should have priority over event level', () => { + expect.assertions(1); + const scope = new Scope(); + scope.setLevel(Severity.Warning); + const event: Event = {}; + event.level = Severity.Critical; + return scope.applyToEvent(event).then(processedEvent => { + expect(processedEvent!.level).toEqual('warning'); + }); + }); + + test('scope transaction should have priority over event transaction', () => { + expect.assertions(1); + const scope = new Scope(); + scope.setTransaction('/abc'); + const event: Event = {}; + event.transaction = '/cdf'; + return scope.applyToEvent(event).then(processedEvent => { + expect(processedEvent!.transaction).toEqual('/abc'); + }); }); }); @@ -265,170 +285,286 @@ describe('Scope', () => { expect((scope as any)._breadcrumbs).toHaveLength(0); }); - test('addEventProcessor', () => { - expect.assertions(3); - const event: Event = { - extra: { b: 3 }, - }; - const localScope = new Scope(); - localScope.setExtra('a', 'b'); - localScope.addEventProcessor((processedEvent: Event) => { - expect(processedEvent.extra).toEqual({ a: 'b', b: 3 }); - return processedEvent; - }); - localScope.addEventProcessor((processedEvent: Event) => { - processedEvent.dist = '1'; - return processedEvent; + describe('update', () => { + let scope: Scope; + + beforeEach(() => { + scope = new Scope(); + scope.setTags({ foo: '1', bar: '2' }); + scope.setExtras({ foo: '1', bar: '2' }); + scope.setContext('foo', { id: '1' }); + scope.setContext('bar', { id: '2' }); + scope.setUser({ id: '1337' }); + scope.setLevel(Severity.Info); + scope.setFingerprint(['foo']); }); - localScope.addEventProcessor((processedEvent: Event) => { - expect(processedEvent.dist).toEqual('1'); - return processedEvent; + + test('given no data, returns the original scope', () => { + const updatedScope = scope.update(); + expect(updatedScope).toEqual(scope); }); - return localScope.applyToEvent(event).then(final => { - expect(final!.dist).toEqual('1'); + test('given neither function, Scope or plain object, returns original scope', () => { + // @ts-ignore + const updatedScope = scope.update('wat'); + expect(updatedScope).toEqual(scope); }); - }); - test('addEventProcessor + global', () => { - expect.assertions(3); - const event: Event = { - extra: { b: 3 }, - }; - const localScope = new Scope(); - localScope.setExtra('a', 'b'); + test('given callback function, pass it the scope and returns original or modified scope', () => { + const cb = jest + .fn() + .mockImplementationOnce(v => v) + .mockImplementationOnce(v => { + v.setTag('foo', 'bar'); + return v; + }); - addGlobalEventProcessor((processedEvent: Event) => { - processedEvent.dist = '1'; - return processedEvent; - }); + let updatedScope = scope.update(cb); + expect(cb).toHaveBeenNthCalledWith(1, scope); + expect(updatedScope).toEqual(scope); - localScope.addEventProcessor((processedEvent: Event) => { - expect(processedEvent.extra).toEqual({ a: 'b', b: 3 }); - return processedEvent; + updatedScope = scope.update(cb); + expect(cb).toHaveBeenNthCalledWith(2, scope); + expect(updatedScope).toEqual(scope); }); - localScope.addEventProcessor((processedEvent: Event) => { - expect(processedEvent.dist).toEqual('1'); - return processedEvent; + test('given callback function, when it doesnt return instanceof Scope, ignore it and return original scope', () => { + const cb = jest.fn().mockImplementationOnce(v => 'wat'); + const updatedScope = scope.update(cb); + expect(cb).toHaveBeenCalledWith(scope); + expect(updatedScope).toEqual(scope); }); - return localScope.applyToEvent(event).then(final => { - expect(final!.dist).toEqual('1'); + test('given another instance of Scope, it should merge two together, with the passed scope having priority', () => { + const localScope = new Scope(); + localScope.setTags({ bar: '3', baz: '4' }); + localScope.setExtras({ bar: '3', baz: '4' }); + localScope.setContext('bar', { id: '3' }); + localScope.setContext('baz', { id: '4' }); + localScope.setUser({ id: '42' }); + localScope.setLevel(Severity.Warning); + localScope.setFingerprint(['bar']); + + const updatedScope = scope.update(localScope) as any; + + expect(updatedScope._tags).toEqual({ + bar: '3', + baz: '4', + foo: '1', + }); + expect(updatedScope._extra).toEqual({ + bar: '3', + baz: '4', + foo: '1', + }); + expect(updatedScope._contexts).toEqual({ + bar: { id: '3' }, + baz: { id: '4' }, + foo: { id: '1' }, + }); + expect(updatedScope._user).toEqual({ id: '42' }); + expect(updatedScope._level).toEqual(Severity.Warning); + expect(updatedScope._fingerprint).toEqual(['bar']); + }); + + test('given a plain object, it should merge two together, with the passed object having priority', () => { + const localAttributes = { + contexts: { bar: { id: '3' }, baz: { id: '4' } }, + extra: { bar: '3', baz: '4' }, + fingerprint: ['bar'], + level: Severity.Warning, + tags: { bar: '3', baz: '4' }, + user: { id: '42' }, + }; + const updatedScope = scope.update(localAttributes) as any; + + expect(updatedScope._tags).toEqual({ + bar: '3', + baz: '4', + foo: '1', + }); + expect(updatedScope._extra).toEqual({ + bar: '3', + baz: '4', + foo: '1', + }); + expect(updatedScope._contexts).toEqual({ + bar: { id: '3' }, + baz: { id: '4' }, + foo: { id: '1' }, + }); + expect(updatedScope._user).toEqual({ id: '42' }); + expect(updatedScope._level).toEqual(Severity.Warning); + expect(updatedScope._fingerprint).toEqual(['bar']); }); }); - test('addEventProcessor async', async () => { - jest.useFakeTimers(); - expect.assertions(6); - const event: Event = { - extra: { b: 3 }, - }; - const localScope = new Scope(); - localScope.setExtra('a', 'b'); - const callCounter = jest.fn(); - localScope.addEventProcessor((processedEvent: Event) => { - callCounter(1); - expect(processedEvent.extra).toEqual({ a: 'b', b: 3 }); - return processedEvent; - }); - localScope.addEventProcessor( - async (processedEvent: Event) => - new Promise(resolve => { - callCounter(2); - setTimeout(() => { - callCounter(3); - processedEvent.dist = '1'; - resolve(processedEvent); - }, 1); - jest.runAllTimers(); - }), - ); - localScope.addEventProcessor((processedEvent: Event) => { - callCounter(4); - return processedEvent; - }); - - return localScope.applyToEvent(event).then(processedEvent => { - expect(callCounter.mock.calls[0][0]).toBe(1); - expect(callCounter.mock.calls[1][0]).toBe(2); - expect(callCounter.mock.calls[2][0]).toBe(3); - expect(callCounter.mock.calls[3][0]).toBe(4); - expect(processedEvent!.dist).toEqual('1'); + describe('addEventProcessor', () => { + test('should allow for basic event manipulation', () => { + expect.assertions(3); + const event: Event = { + extra: { b: 3 }, + }; + const localScope = new Scope(); + localScope.setExtra('a', 'b'); + localScope.addEventProcessor((processedEvent: Event) => { + expect(processedEvent.extra).toEqual({ a: 'b', b: 3 }); + return processedEvent; + }); + localScope.addEventProcessor((processedEvent: Event) => { + processedEvent.dist = '1'; + return processedEvent; + }); + localScope.addEventProcessor((processedEvent: Event) => { + expect(processedEvent.dist).toEqual('1'); + return processedEvent; + }); + + return localScope.applyToEvent(event).then(final => { + expect(final!.dist).toEqual('1'); + }); }); - }); - test('addEventProcessor async with reject', async () => { - jest.useFakeTimers(); - expect.assertions(2); - const event: Event = { - extra: { b: 3 }, - }; - const localScope = new Scope(); - localScope.setExtra('a', 'b'); - const callCounter = jest.fn(); - localScope.addEventProcessor((processedEvent: Event) => { - callCounter(1); - expect(processedEvent.extra).toEqual({ a: 'b', b: 3 }); - return processedEvent; - }); - localScope.addEventProcessor( - async (_processedEvent: Event) => - new Promise((_, reject) => { - setTimeout(() => { - reject('bla'); - }, 1); - jest.runAllTimers(); - }), - ); - localScope.addEventProcessor((processedEvent: Event) => { - callCounter(4); - return processedEvent; - }); - - return localScope.applyToEvent(event).then(null, reason => { - expect(reason).toEqual('bla'); + test('should work alongside global event processors', () => { + expect.assertions(3); + const event: Event = { + extra: { b: 3 }, + }; + const localScope = new Scope(); + localScope.setExtra('a', 'b'); + + addGlobalEventProcessor((processedEvent: Event) => { + processedEvent.dist = '1'; + return processedEvent; + }); + + localScope.addEventProcessor((processedEvent: Event) => { + expect(processedEvent.extra).toEqual({ a: 'b', b: 3 }); + return processedEvent; + }); + + localScope.addEventProcessor((processedEvent: Event) => { + expect(processedEvent.dist).toEqual('1'); + return processedEvent; + }); + + return localScope.applyToEvent(event).then(final => { + expect(final!.dist).toEqual('1'); + }); }); - }); - test('addEventProcessor return null', () => { - expect.assertions(1); - const event: Event = { - extra: { b: 3 }, - }; - const localScope = new Scope(); - localScope.setExtra('a', 'b'); - localScope.addEventProcessor(async (_: Event) => null); - return localScope.applyToEvent(event).then(processedEvent => { - expect(processedEvent).toBeNull(); + test('should allow for async callbacks', async () => { + jest.useFakeTimers(); + expect.assertions(6); + const event: Event = { + extra: { b: 3 }, + }; + const localScope = new Scope(); + localScope.setExtra('a', 'b'); + const callCounter = jest.fn(); + localScope.addEventProcessor((processedEvent: Event) => { + callCounter(1); + expect(processedEvent.extra).toEqual({ a: 'b', b: 3 }); + return processedEvent; + }); + localScope.addEventProcessor( + async (processedEvent: Event) => + new Promise(resolve => { + callCounter(2); + setTimeout(() => { + callCounter(3); + processedEvent.dist = '1'; + resolve(processedEvent); + }, 1); + jest.runAllTimers(); + }), + ); + localScope.addEventProcessor((processedEvent: Event) => { + callCounter(4); + return processedEvent; + }); + + return localScope.applyToEvent(event).then(processedEvent => { + expect(callCounter.mock.calls[0][0]).toBe(1); + expect(callCounter.mock.calls[1][0]).toBe(2); + expect(callCounter.mock.calls[2][0]).toBe(3); + expect(callCounter.mock.calls[3][0]).toBe(4); + expect(processedEvent!.dist).toEqual('1'); + }); }); - }); - test('addEventProcessor pass along hint', () => { - expect.assertions(3); - const event: Event = { - extra: { b: 3 }, - }; - const localScope = new Scope(); - localScope.setExtra('a', 'b'); - localScope.addEventProcessor(async (internalEvent: Event, hint?: EventHint) => { - expect(hint).toBeTruthy(); - expect(hint!.syntheticException).toBeTruthy(); - return internalEvent; - }); - return localScope.applyToEvent(event, { syntheticException: new Error('what') }).then(processedEvent => { - expect(processedEvent).toEqual(event); + test('should correctly handle async rejections', async () => { + jest.useFakeTimers(); + expect.assertions(2); + const event: Event = { + extra: { b: 3 }, + }; + const localScope = new Scope(); + localScope.setExtra('a', 'b'); + const callCounter = jest.fn(); + localScope.addEventProcessor((processedEvent: Event) => { + callCounter(1); + expect(processedEvent.extra).toEqual({ a: 'b', b: 3 }); + return processedEvent; + }); + localScope.addEventProcessor( + async (_processedEvent: Event) => + new Promise((_, reject) => { + setTimeout(() => { + reject('bla'); + }, 1); + jest.runAllTimers(); + }), + ); + localScope.addEventProcessor((processedEvent: Event) => { + callCounter(4); + return processedEvent; + }); + + return localScope.applyToEvent(event).then(null, reason => { + expect(reason).toEqual('bla'); + }); }); - }); - test('listeners', () => { - jest.useFakeTimers(); - const scope = new Scope(); - const listener = jest.fn(); - scope.addScopeListener(listener); - scope.setExtra('a', 2); - jest.runAllTimers(); - expect(listener).toHaveBeenCalled(); - expect(listener.mock.calls[0][0]._extra).toEqual({ a: 2 }); + test('should drop an event when any of processors return null', () => { + expect.assertions(1); + const event: Event = { + extra: { b: 3 }, + }; + const localScope = new Scope(); + localScope.setExtra('a', 'b'); + localScope.addEventProcessor(async (_: Event) => null); + return localScope.applyToEvent(event).then(processedEvent => { + expect(processedEvent).toBeNull(); + }); + }); + + test('should have an access to the EventHint', () => { + expect.assertions(3); + const event: Event = { + extra: { b: 3 }, + }; + const localScope = new Scope(); + localScope.setExtra('a', 'b'); + localScope.addEventProcessor(async (internalEvent: Event, hint?: EventHint) => { + expect(hint).toBeTruthy(); + expect(hint!.syntheticException).toBeTruthy(); + return internalEvent; + }); + return localScope.applyToEvent(event, { syntheticException: new Error('what') }).then(processedEvent => { + expect(processedEvent).toEqual(event); + }); + }); + + test('should notify all the listeners about the changes', () => { + jest.useFakeTimers(); + const scope = new Scope(); + const listener = jest.fn(); + scope.addScopeListener(listener); + scope.setExtra('a', 2); + jest.runAllTimers(); + expect(listener).toHaveBeenCalled(); + expect(listener.mock.calls[0][0]._extra).toEqual({ a: 2 }); + }); }); }); diff --git a/packages/minimal/src/index.ts b/packages/minimal/src/index.ts index 6372d46d16d3..76725f5dfcb6 100644 --- a/packages/minimal/src/index.ts +++ b/packages/minimal/src/index.ts @@ -1,5 +1,5 @@ import { getCurrentHub, Hub, Scope } from '@sentry/hub'; -import { Breadcrumb, Event, Severity, Transaction, TransactionContext, User } from '@sentry/types'; +import { Breadcrumb, CaptureContext, Event, Severity, Transaction, TransactionContext, User } from '@sentry/types'; /** * This calls a function on the current hub. @@ -21,7 +21,7 @@ function callOnHub(method: string, ...args: any[]): T { * @param exception An exception-like object. * @returns The generated eventId. */ -export function captureException(exception: any): string { +export function captureException(exception: any, captureContext?: CaptureContext): string { let syntheticException: Error; try { throw new Error('Sentry syntheticException'); @@ -29,6 +29,7 @@ export function captureException(exception: any): string { syntheticException = exception as Error; } return callOnHub('captureException', exception, { + captureContext, originalException: exception, syntheticException, }); @@ -41,16 +42,23 @@ export function captureException(exception: any): string { * @param level Define the level of the message. * @returns The generated eventId. */ -export function captureMessage(message: string, level?: Severity): string { +export function captureMessage(message: string, captureContext?: CaptureContext | Severity): string { let syntheticException: Error; try { throw new Error(message); } catch (exception) { syntheticException = exception as Error; } + + // This is necessary to provide explicit scopes upgrade, without changing the original + // arrity of the `captureMessage(message, level)` method. + const level = typeof captureContext === 'string' ? captureContext : undefined; + const context = typeof captureContext !== 'string' ? { captureContext } : undefined; + return callOnHub('captureMessage', message, level, { originalException: message, syntheticException, + ...context, }); } diff --git a/packages/minimal/test/lib/minimal.test.ts b/packages/minimal/test/lib/minimal.test.ts index f429477fd8a1..bcadc4ba93d3 100644 --- a/packages/minimal/test/lib/minimal.test.ts +++ b/packages/minimal/test/lib/minimal.test.ts @@ -51,6 +51,20 @@ describe('Minimal', () => { }); }); + test('Exception with explicit scope', () => { + const client: any = { + captureException: jest.fn(async () => Promise.resolve()), + }; + getCurrentHub().withScope(() => { + getCurrentHub().bindClient(client); + const e = new Error('test exception'); + const captureContext = { extra: { foo: 'wat' } }; + captureException(e, captureContext); + expect(client.captureException.mock.calls[0][0]).toBe(e); + expect(client.captureException.mock.calls[0][1].captureContext).toBe(captureContext); + }); + }); + test('Message', () => { const client: any = { captureMessage: jest.fn(async () => Promise.resolve()) }; getCurrentHub().withScope(() => { @@ -61,6 +75,33 @@ describe('Minimal', () => { }); }); + test('Message with explicit scope', () => { + const client: any = { captureMessage: jest.fn(async () => Promise.resolve()) }; + getCurrentHub().withScope(() => { + getCurrentHub().bindClient(client); + const message = 'yo'; + const captureContext = { extra: { foo: 'wat' } }; + captureMessage(message, captureContext); + expect(client.captureMessage.mock.calls[0][0]).toBe(message); + // Skip the level if explicit content is provided + expect(client.captureMessage.mock.calls[0][1]).toBe(undefined); + expect(client.captureMessage.mock.calls[0][2].captureContext).toBe(captureContext); + }); + }); + + // NOTE: We left custom level as 2nd argument to not break the API. Should be removed and unified in v6. + test('Message with custom level', () => { + const client: any = { captureMessage: jest.fn(async () => Promise.resolve()) }; + getCurrentHub().withScope(() => { + getCurrentHub().bindClient(client); + const message = 'yo'; + const level = Severity.Warning; + captureMessage(message, level); + expect(client.captureMessage.mock.calls[0][0]).toBe(message); + expect(client.captureMessage.mock.calls[0][1]).toBe(Severity.Warning); + }); + }); + test('Event', () => { const client: any = { captureEvent: jest.fn(async () => Promise.resolve()) }; getCurrentHub().withScope(() => { @@ -258,6 +299,6 @@ describe('Minimal', () => { test('setContext', () => { init({}); setContext('test', { id: 'b' }); - expect(global.__SENTRY__.hub._stack[0].scope._context).toEqual({ test: { id: 'b' } }); + expect(global.__SENTRY__.hub._stack[0].scope._contexts).toEqual({ test: { id: 'b' } }); }); }); diff --git a/packages/types/src/event.ts b/packages/types/src/event.ts index 7a67948436f1..b96d10730288 100644 --- a/packages/types/src/event.ts +++ b/packages/types/src/event.ts @@ -1,6 +1,7 @@ import { Breadcrumb } from './breadcrumb'; import { Exception } from './exception'; import { Request } from './request'; +import { CaptureContext } from './scope'; import { SdkInfo } from './sdkinfo'; import { Severity } from './severity'; import { Span } from './span'; @@ -46,6 +47,7 @@ export type EventType = 'transaction'; /** JSDoc */ export interface EventHint { event_id?: string; + captureContext?: CaptureContext; syntheticException?: Error | null; originalException?: Error | string | null; data?: any; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 857d304d42c4..628b49d6b8d6 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -14,7 +14,7 @@ export { Package } from './package'; export { Request } from './request'; export { Response } from './response'; export { Runtime } from './runtime'; -export { Scope } from './scope'; +export { CaptureContext, Scope, ScopeContext } from './scope'; export { SdkInfo } from './sdkinfo'; export { Severity } from './severity'; export { Span, SpanContext } from './span'; diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index aba75fb40230..5af0c689f33d 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -4,6 +4,19 @@ import { Severity } from './severity'; import { Span } from './span'; import { User } from './user'; +/** JSDocs */ +export type CaptureContext = Scope | Partial | ((scope: Scope) => Scope); + +/** JSDocs */ +export interface ScopeContext { + user: User; + level: Severity; + extra: { [key: string]: any }; + contexts: { [key: string]: any }; + tags: { [key: string]: string }; + fingerprint: string[]; +} + /** * Holds additional event information. {@link Scope.applyToEvent} will be * called by the client before an event will be sent. @@ -76,6 +89,15 @@ export interface Scope { */ setSpan(span?: Span): this; + /** + * Updates the scope with provided data. Can work in three variations: + * - plain object containing updatable attributes + * - Scope instance that'll extract the attributes from + * - callback function that'll receive the current scope as an argument and allow for modifications + * @param captureContext scope modifier to be used + */ + update(captureContext?: CaptureContext): this; + /** Clears the current scope and resets its properties. */ clear(): this;