diff --git a/libs/browser/src/lib/consts/max-breadcrumbs.ts b/libs/browser/src/lib/consts/max-breadcrumbs.ts new file mode 100644 index 0000000..a57c4e1 --- /dev/null +++ b/libs/browser/src/lib/consts/max-breadcrumbs.ts @@ -0,0 +1 @@ +export const MAX_BREADCRUMBS = 100; diff --git a/libs/browser/src/lib/models/browser-sentry-client-options.ts b/libs/browser/src/lib/models/browser-sentry-client-options.ts index 6f3b839..44457a0 100644 --- a/libs/browser/src/lib/models/browser-sentry-client-options.ts +++ b/libs/browser/src/lib/models/browser-sentry-client-options.ts @@ -12,4 +12,5 @@ export interface BrowserSentryClientOptions extends SentryClientOptions { ignoreErrors?: Array; blacklistUrls?: Array; release?: string; + maxBreadcrumbs?: number; } diff --git a/libs/browser/src/lib/services/browser-micro-sentry-client.spec.ts b/libs/browser/src/lib/services/browser-micro-sentry-client.spec.ts index 0c2b840..a686081 100644 --- a/libs/browser/src/lib/services/browser-micro-sentry-client.spec.ts +++ b/libs/browser/src/lib/services/browser-micro-sentry-client.spec.ts @@ -1,13 +1,22 @@ import { BrowserMicroSentryClient } from './browser-micro-sentry-client'; import { Severity } from '@micro-sentry/core'; +import { MAX_BREADCRUMBS } from '../consts/max-breadcrumbs'; describe('BrowserMicroSentryClient', () => { let client: BrowserMicroSentryClient; + const maxBreadcrumbs = 10; + const getBreadcrumbs = (amount: number) => + [...Array(amount).keys()].map((index) => ({ + event_id: `id${index}`, + type: 'console', + level: Severity.critical, + })); beforeAll(() => { client = new BrowserMicroSentryClient({ dsn: 'http://secret@exampl.dsn/2', release: '1.0.0', + maxBreadcrumbs, }); }); @@ -205,6 +214,97 @@ describe('BrowserMicroSentryClient', () => { }); }); + it('should limit breadcrumbs amount - with custom limit; incremental addition', () => { + getBreadcrumbs(maxBreadcrumbs + 2).forEach((bc) => + client.addBreadcrumb(bc) + ); + + expect(client.state.breadcrumbs?.length).toBe(maxBreadcrumbs); + }); + + it('should limit breadcrumbs amount - with custom limit; all at once', () => { + client.setBreadcrumbs(getBreadcrumbs(maxBreadcrumbs + 2)); + + expect(client.state.breadcrumbs?.length).toBe(maxBreadcrumbs); + }); + + it('should limit breadcrumbs amount - with default limit; incremental addition', () => { + const anotherClient = new BrowserMicroSentryClient({}); + + getBreadcrumbs(MAX_BREADCRUMBS + 2).forEach((bc) => + anotherClient.addBreadcrumb(bc) + ); + + expect(anotherClient.state.breadcrumbs?.length).toBe(MAX_BREADCRUMBS); + }); + + it('should limit breadcrumbs amount - with default limit; all at once', () => { + const anotherClient = new BrowserMicroSentryClient({}); + anotherClient.setBreadcrumbs(getBreadcrumbs(MAX_BREADCRUMBS + 2)); + + expect(anotherClient.state.breadcrumbs?.length).toBe(MAX_BREADCRUMBS); + }); + + it('should save only last breadcrumbs; incremental addition', () => { + getBreadcrumbs(maxBreadcrumbs + 2).forEach((bc) => + client.addBreadcrumb(bc) + ); + + expect( + (client.state.breadcrumbs ?? []) + .map((breadcrumb) => breadcrumb.event_id) + .join(',') + ).toEqual('id2,id3,id4,id5,id6,id7,id8,id9,id10,id11'); + }); + + it('should save only last breadcrumbs; all at once', () => { + client.setBreadcrumbs(getBreadcrumbs(maxBreadcrumbs + 2)); + + expect( + (client.state.breadcrumbs ?? []) + .map((breadcrumb) => breadcrumb.event_id) + .join(',') + ).toEqual('id2,id3,id4,id5,id6,id7,id8,id9,id10,id11'); + }); + + it('should not add breadcrumbs at all if maxBreadcrumbs is set to 0; incremental addition', () => { + const anotherClient = new BrowserMicroSentryClient({ maxBreadcrumbs: 0 }); + + getBreadcrumbs(1).forEach((bc) => anotherClient.addBreadcrumb(bc)); + + expect(anotherClient.state.breadcrumbs?.length).toBe(0); + }); + + it('should not add breadcrumbs at all if maxBreadcrumbs is set to 0; all at once', () => { + const anotherClient = new BrowserMicroSentryClient({ maxBreadcrumbs: 0 }); + + anotherClient.setBreadcrumbs(getBreadcrumbs(1)); + + expect(anotherClient.state.breadcrumbs?.length).toBe(0); + }); + + it('should ignore maxBreadcrumbs option if maxBreadcrumbs is a negative number; incremental addition', () => { + const anotherClient = new BrowserMicroSentryClient({ + maxBreadcrumbs: -100, + }); + + getBreadcrumbs(MAX_BREADCRUMBS + 2).forEach((bc) => + anotherClient.addBreadcrumb(bc) + ); + + expect(anotherClient.state.breadcrumbs?.length).toBe(MAX_BREADCRUMBS); + }); + + it('should ignore maxBreadcrumbs option if maxBreadcrumbs is a negative number; all at once', () => { + const anotherClient = new BrowserMicroSentryClient({ + maxBreadcrumbs: -100, + }); + + anotherClient.setBreadcrumbs(getBreadcrumbs(MAX_BREADCRUMBS + 2)); + + expect(anotherClient.state.breadcrumbs?.length).toBe(MAX_BREADCRUMBS); + }); + it('should skip breadcrumbs if beforeBreadcrumb returns null', () => { client = new BrowserMicroSentryClient({ dsn: 'http://secret@exampl.dsn/2', @@ -237,7 +337,7 @@ describe('BrowserMicroSentryClient', () => { ]); }); - afterAll(() => { + afterEach(() => { client.clearState(); }); }); diff --git a/libs/browser/src/lib/services/browser-micro-sentry-client.ts b/libs/browser/src/lib/services/browser-micro-sentry-client.ts index f4c265f..77db041 100644 --- a/libs/browser/src/lib/services/browser-micro-sentry-client.ts +++ b/libs/browser/src/lib/services/browser-micro-sentry-client.ts @@ -12,12 +12,14 @@ import { State } from '../models/state'; import { MicroSentryPlugin } from '../models/plugin'; import { BrowserSentryClientOptions } from '../models/browser-sentry-client-options'; import { isMatchingPattern } from '../utils/is-matching-pattern'; +import { MAX_BREADCRUMBS } from '../consts/max-breadcrumbs'; function getWindow(): Window { return window; } export class BrowserMicroSentryClient extends MicroSentryClient { + private readonly breadcrumbsKeyName = 'breadcrumbs'; private destroyed = false; private readonly plugins: MicroSentryPlugin[]; private readonly beforeSend: NonNullable< @@ -33,6 +35,7 @@ export class BrowserMicroSentryClient extends MicroSentryClient { BrowserSentryClientOptions['ignoreErrors'] >; private readonly release?: string; + private readonly maxBreadcrumbs: number; constructor( private options: BrowserSentryClientOptions, @@ -47,7 +50,8 @@ export class BrowserMicroSentryClient extends MicroSentryClient { blacklistUrls = [], ignoreErrors = [], release = undefined, - } = this.options || {}; + maxBreadcrumbs = MAX_BREADCRUMBS, + } = this.options || {} || []; this.plugins = plugins.map((Plugin) => new Plugin(this)); this.beforeSend = beforeSend; @@ -55,6 +59,8 @@ export class BrowserMicroSentryClient extends MicroSentryClient { this.blacklistUrls = blacklistUrls; this.ignoreErrors = ignoreErrors; this.release = release; + this.maxBreadcrumbs = + maxBreadcrumbs >= 0 ? maxBreadcrumbs : MAX_BREADCRUMBS; } protected _state: State = {}; @@ -126,17 +132,20 @@ export class BrowserMicroSentryClient extends MicroSentryClient { } this.extendState({ - breadcrumbs: [ + [this.breadcrumbsKeyName]: [ { timestamp: Date.now() / 1_000, ...result, }, ], }); + + this.trimBreadcrumbs(); } setBreadcrumbs(breadcrumbs: Breadcrumb[] | undefined) { - this.setKeyState('breadcrumbs', breadcrumbs); + this.setKeyState(this.breadcrumbsKeyName, breadcrumbs); + this.trimBreadcrumbs(); } captureMessage(message: string, level?: Severity) { @@ -307,4 +316,18 @@ export class BrowserMicroSentryClient extends MicroSentryClient { private setKeyState(key: T, value: State[T]) { this._state[key] = value; } + + private getKeyState(key: T): State[T] { + return this._state[key]; + } + + private trimBreadcrumbs(): void { + const breadcrumbs = this.getKeyState(this.breadcrumbsKeyName); + if (breadcrumbs && (breadcrumbs.length ?? 0) > this.maxBreadcrumbs) { + this.setKeyState( + this.breadcrumbsKeyName, + this.maxBreadcrumbs > 0 ? breadcrumbs.slice(-this.maxBreadcrumbs) : [] + ); + } + } }