diff --git a/CHANGELOG.md b/CHANGELOG.md index 69269fbc5a38..1e7aa0557525 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ since we removed some methods from the public API and removed some classes from - **breaking** [core] ref: Move `extraErrorData` integration to `@sentry/integrations` package - [core] feat: Add `maxValueLength` option to adjust max string length for values, default is 250. - **breaking** [all] ref: Expose `module` in `package.json` as entry point for esm builds. +- [hub] feat: Introduce `setExtra`, `setTags`, `clearBreadcrumbs` additionally some `set` on the Scope now accept no + argument which makes it possible to unset the value. ## 4.6.4 diff --git a/packages/hub/src/scope.ts b/packages/hub/src/scope.ts index 9b32e8c10a1c..b6e925d34ddd 100644 --- a/packages/hub/src/scope.ts +++ b/packages/hub/src/scope.ts @@ -1,5 +1,5 @@ import { Breadcrumb, Event, EventHint, EventProcessor, Scope as ScopeInterface, Severity, User } from '@sentry/types'; -import { isThenable } from '@sentry/utils/is'; +import { isPlainObject, isThenable } from '@sentry/utils/is'; import { getGlobalObject } from '@sentry/utils/misc'; import { normalize } from '@sentry/utils/object'; import { SyncPromise } from '@sentry/utils/syncpromise'; @@ -36,7 +36,10 @@ export class Scope implements ScopeInterface { /** Severity */ protected level?: Severity; - /** Add internal on change listener. */ + /** + * Add internal on change listener. Used for sub SDKs that need to store the scope. + * @hidden + */ public addScopeListener(callback: (scope: Scope) => void): void { this.scopeListeners.push(callback); } @@ -44,11 +47,26 @@ export class Scope implements ScopeInterface { /** * @inheritdoc */ - public addEventProcessor(callback: EventProcessor): Scope { + public addEventProcessor(callback: EventProcessor): this { this.eventProcessors.push(callback); return this; } + /** + * This will be called on every set call. + */ + protected notifyScopeListeners(): void { + if (!this.notifyingListeners) { + this.notifyingListeners = true; + setTimeout(() => { + this.scopeListeners.forEach(callback => { + callback(this); + }); + this.notifyingListeners = false; + }); + } + } + /** * This will be called after {@link applyToEvent} is finished. */ @@ -81,40 +99,75 @@ export class Scope implements ScopeInterface { /** * @inheritdoc */ - public setUser(user: User): Scope { - this.user = normalize(user); + public setUser(user?: User): this { + this.user = user ? normalize(user) : {}; + this.notifyScopeListeners(); return this; } /** * @inheritdoc */ - public setTag(key: string, value: string): Scope { + public setTags(tags?: { [key: string]: string }): this { + this.tags = + tags && isPlainObject(tags) + ? { + ...this.tags, + ...normalize(tags), + } + : {}; + this.notifyScopeListeners(); + return this; + } + + /** + * @inheritdoc + */ + public setTag(key: string, value: string): this { this.tags = { ...this.tags, [key]: normalize(value) }; + this.notifyScopeListeners(); return this; } /** * @inheritdoc */ - public setExtra(key: string, extra: any): Scope { + public setExtras(extra?: { [key: string]: any }): this { + this.extra = + extra && isPlainObject(extra) + ? { + ...this.extra, + ...normalize(extra), + } + : {}; + this.notifyScopeListeners(); + return this; + } + + /** + * @inheritdoc + */ + public setExtra(key: string, extra: any): this { this.extra = { ...this.extra, [key]: normalize(extra) }; + this.notifyScopeListeners(); return this; } /** * @inheritdoc */ - public setFingerprint(fingerprint: string[]): Scope { - this.fingerprint = normalize(fingerprint); + public setFingerprint(fingerprint?: string[]): this { + this.fingerprint = fingerprint ? normalize(fingerprint) : undefined; + this.notifyScopeListeners(); return this; } /** * @inheritdoc */ - public setLevel(level: Severity): Scope { - this.level = normalize(level); + public setLevel(level?: Severity): this { + this.level = level ? normalize(level) : undefined; + this.notifyScopeListeners(); return this; } @@ -139,23 +192,36 @@ export class Scope implements ScopeInterface { /** * @inheritdoc */ - public clear(): void { + public clear(): this { this.breadcrumbs = []; this.tags = {}; this.extra = {}; this.user = {}; this.level = undefined; this.fingerprint = undefined; + this.notifyScopeListeners(); + return this; } /** * @inheritdoc */ - public addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): void { + public addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this { this.breadcrumbs = maxBreadcrumbs !== undefined && maxBreadcrumbs >= 0 ? [...this.breadcrumbs, normalize(breadcrumb)].slice(-maxBreadcrumbs) : [...this.breadcrumbs, normalize(breadcrumb)]; + this.notifyScopeListeners(); + return this; + } + + /** + * @inheritdoc + */ + public clearBreadcrumbs(): this { + this.breadcrumbs = []; + this.notifyScopeListeners(); + return this; } /** diff --git a/packages/hub/test/scope.test.ts b/packages/hub/test/scope.test.ts index ceb18df98695..9d4db7bc4c26 100644 --- a/packages/hub/test/scope.test.ts +++ b/packages/hub/test/scope.test.ts @@ -7,40 +7,105 @@ describe('Scope', () => { jest.useRealTimers(); }); - test('fingerprint', () => { - const scope = new Scope(); - scope.setFingerprint(['abcd']); - expect((scope as any).fingerprint).toEqual(['abcd']); + describe('fingerprint', () => { + test('set', () => { + const scope = new Scope(); + scope.setFingerprint(['abcd']); + expect((scope as any).fingerprint).toEqual(['abcd']); + }); + + test('unset', () => { + const scope = new Scope(); + scope.setFingerprint(['abcd']); + scope.setFingerprint(); + expect((scope as any).fingerprint).toEqual(undefined); + }); }); - test('extra', () => { - const scope = new Scope(); - scope.setExtra('a', 1); - expect((scope as any).extra).toEqual({ a: 1 }); + describe('extra', () => { + test('set key value', () => { + const scope = new Scope(); + scope.setExtra('a', 1); + expect((scope as any).extra).toEqual({ a: 1 }); + }); + + test('set object', () => { + const scope = new Scope(); + scope.setExtras({ a: 1 }); + expect((scope as any).extra).toEqual({ a: 1 }); + }); + + test('set undefined', () => { + const scope = new Scope(); + scope.setExtra('a', 1); + scope.setExtras(); + expect((scope as any).extra).toEqual({}); + }); }); - test('tags', () => { - const scope = new Scope(); - scope.setTag('a', 'b'); - expect((scope as any).tags).toEqual({ a: 'b' }); + describe('tags', () => { + test('set key value', () => { + const scope = new Scope(); + scope.setTag('a', 'b'); + expect((scope as any).tags).toEqual({ a: 'b' }); + }); + + test('set object', () => { + const scope = new Scope(); + scope.setTags({ a: 'b' }); + expect((scope as any).tags).toEqual({ a: 'b' }); + }); + + test('set undefined', () => { + const scope = new Scope(); + scope.setTags({ a: 'b' }); + scope.setTags(); + expect((scope as any).tags).toEqual({}); + }); }); - test('user', () => { - const scope = new Scope(); - scope.setUser({ id: '1' }); - expect((scope as any).user).toEqual({ id: '1' }); + describe('user', () => { + test('set', () => { + const scope = new Scope(); + scope.setUser({ id: '1' }); + expect((scope as any).user).toEqual({ id: '1' }); + }); + + test('unset', () => { + const scope = new Scope(); + scope.setUser({ id: '1' }); + scope.setUser(); + expect((scope as any).user).toEqual({}); + }); }); - test('breadcrumbs', () => { - const scope = new Scope(); - scope.addBreadcrumb({ message: 'test' }, 100); - expect((scope as any).breadcrumbs).toEqual([{ message: 'test' }]); + describe('level', () => { + test('add', () => { + const scope = new Scope(); + scope.addBreadcrumb({ message: 'test' }, 100); + expect((scope as any).breadcrumbs).toEqual([{ message: 'test' }]); + }); + + test('clear', () => { + const scope = new Scope(); + scope.addBreadcrumb({ message: 'test' }, 100); + scope.clearBreadcrumbs(); + expect((scope as any).breadcrumbs).toEqual([]); + }); }); - test('level', () => { - const scope = new Scope(); - scope.setLevel(Severity.Critical); - expect((scope as any).level).toEqual(Severity.Critical); + describe('level', () => { + test('set', () => { + const scope = new Scope(); + scope.setLevel(Severity.Critical); + expect((scope as any).level).toEqual(Severity.Critical); + }); + test('unset', () => { + const scope = new Scope(); + scope.setLevel(Severity.Critical); + scope.setLevel(); + expect((scope as any).level).toEqual(undefined); + }); }); test('chaining', () => { @@ -281,4 +346,15 @@ describe('Scope', () => { expect(processedEvent).toEqual(event); }); }); + + 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 }); + }); }); diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index a1aeadb7e7cb..56aeab925cdf 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -9,45 +9,70 @@ import { User } from './user'; */ export interface Scope { /** Add new event processor that will be called after {@link applyToEvent}. */ - addEventProcessor(callback: EventProcessor): Scope; + addEventProcessor(callback: EventProcessor): this; /** * Updates user context information for future events. + * If passed any falsy value, value will be unset. + * * @param user User context object to be set in the current context. */ - setUser(user: User): Scope; + setUser(user?: User): this; /** - * Updates tags context information for future events. + * Set an object that will be sent as tags data with the event. + * If passed any falsy value, value will be unset. * @param tags Tags context object to merge into current context. */ - setTag(key: string, value: string): Scope; + setTags(tags?: { [key: string]: string }): this; /** - * Updates extra context information for future events. + * Set key:value that will be sent as tags data with the event. + * @param key String key of tag + * @param value String value of tag + */ + setTag(key: string, value: string): this; + + /** + * Set an object that will be sent as extra data with the event. + * If passed any falsy value, value will be unset. * @param extra context object to merge into current context. */ - setExtra(key: string, extra: any): Scope; + setExtras(extra?: { [key: string]: any }): this; + + /** + * Set key:value that will be sent as extra data with the event. + * @param key String of extra + * @param extra Any kind of data. This data will be normailzed. + */ + setExtra(key: string, extra: any): this; /** * Sets the fingerprint on the scope to send with the events. + * If passed any falsy value, value will be unset. * @param fingerprint string[] to group events in Sentry. */ - setFingerprint(fingerprint: string[]): Scope; + setFingerprint(fingerprint?: string[]): this; /** * Sets the level on the scope for future events. + * If passed any falsy value, value will be unset. * @param level string {@link Severity} */ - setLevel(level: Severity): Scope; + setLevel(level?: Severity): this; /** Clears the current scope and resets its properties. */ - clear(): void; + clear(): this; /** * Sets the breadcrumbs in the scope * @param breadcrumbs Breadcrumb * @param maxBreadcrumbs number of max breadcrumbs to merged into event. */ - addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): void; + addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this; + + /** + * Clears all currently set Breadcrumbs. + */ + clearBreadcrumbs(): this; }