diff --git a/src/core/client.ts b/src/core/client.ts index 259d01681..0cab0af7e 100644 --- a/src/core/client.ts +++ b/src/core/client.ts @@ -82,7 +82,8 @@ export default abstract class Client { // First, we go with the global (shared) store. // Webserver middleware can then switch to the AsyncStore for async context tracking. - this.__store = new GlobalStore({ context: {}, breadcrumbs: [] }) + this.__store = GlobalStore + this.clear() this.__transport = transport this.logger = logger(this) } @@ -125,7 +126,9 @@ export default abstract class Client { setContext(context: Record): Client { if (typeof context === 'object') { const store = this.__store.getStore() - store.context = merge(store.context, context) + if (store) { + store.context = merge(store.context, context) + } } return this } @@ -134,6 +137,8 @@ export default abstract class Client { this.logger.warn('Deprecation warning: `Honeybadger.resetContext()` has been deprecated; please use `Honeybadger.clear()` instead.') const store = this.__store.getStore() + if (store === undefined) return this + if (typeof context === 'object' && context !== null) { store.context = context } @@ -146,8 +151,10 @@ export default abstract class Client { clear(): Client { const store = this.__store.getStore() - store.context = {} - store.breadcrumbs = [] + if (store) { + store.context = {} + store.breadcrumbs = [] + } return this } @@ -347,13 +354,17 @@ export default abstract class Client { const category = opts.category || 'custom' const timestamp = new Date().toISOString() - const store = this.__store.getStore() - let breadcrumbs = store.breadcrumbs + let store = this.__store.getStore() + if (!store) { + this.__setStore(GlobalStore) + store = this.__store.getStore() + } + let breadcrumbs = store.breadcrumbs || [] breadcrumbs.push({ category: category as string, - message: message, metadata: metadata as Record, - timestamp: timestamp + message, + timestamp, }) const limit = this.config.maxBreadcrumbs @@ -365,6 +376,10 @@ export default abstract class Client { return this } + getBreadcrumbs() { + return this.__store.getStore().breadcrumbs + } + protected __developmentMode(): boolean { if (this.config.reportData === true) { return false } return (this.config.environment && this.config.developmentEnvironments.includes(this.config.environment)) @@ -426,11 +441,6 @@ export default abstract class Client { */ protected __getStoreContentsOrDefault(): DefaultStoreContents { const existingStoreContents = this.__store.getStore(); - const storeContents = existingStoreContents || {}; - return { - context: {}, - breadcrumbs: [], - ...storeContents - }; + return Object.assign(GlobalStore.getStoreCopy(), existingStoreContents || {}) } } diff --git a/src/core/store.ts b/src/core/store.ts index 85261e1ee..979e24867 100644 --- a/src/core/store.ts +++ b/src/core/store.ts @@ -1,6 +1,6 @@ -import { HoneybadgerStore } from './types'; +import { HoneybadgerStore, DefaultStoreContents } from './types'; -export class GlobalStore implements HoneybadgerStore { +class SyncStore implements HoneybadgerStore { private store: T constructor(store: T) { @@ -11,8 +11,18 @@ export class GlobalStore implements HoneybadgerStore { return this.store } + getStoreCopy(): T { + return JSON.parse(JSON.stringify(this.store)) + } + run(store: T, callback: (...args: TArgs) => R, ...args: TArgs): R { this.store = store; return callback(...args); } -} \ No newline at end of file + + static create(): SyncStore { + return new SyncStore({ context: {}, breadcrumbs: [] }) + } +} + +export const GlobalStore = SyncStore.create() diff --git a/src/server.ts b/src/server.ts index f396ddfcc..dfbdc81f2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -14,6 +14,7 @@ import { errorHandler, requestHandler } from './server/middleware' import { lambdaHandler } from './server/aws_lambda' import { AsyncStore } from './server/async_store' import { ServerTransport } from './server/transport'; +import { GlobalStore } from './core/store'; const kHoneybadgerStore = Symbol.for('kHoneybadgerStore'); class Honeybadger extends Client { @@ -94,7 +95,7 @@ class Honeybadger extends Client { handler: (...args: never[]) => R, onError?: (...args: unknown[]) => unknown ): R|void { - const storeObject = (request[kHoneybadgerStore] || this.__getStoreContentsOrDefault()) as DefaultStoreContents; + const storeObject = (request[kHoneybadgerStore] || GlobalStore.getStoreCopy() ) as DefaultStoreContents; this.__setStore(AsyncStore); if (!request[kHoneybadgerStore]) { request[kHoneybadgerStore] = storeObject; diff --git a/src/server/async_store.ts b/src/server/async_store.ts index a2ccc3625..6e4d423b0 100644 --- a/src/server/async_store.ts +++ b/src/server/async_store.ts @@ -8,7 +8,7 @@ try { Store = new AsyncLocalStorage() } catch (e) { - Store = new GlobalStore({ context: {}, breadcrumbs: [] }) + Store = GlobalStore } export const AsyncStore = Store diff --git a/src/server/aws_lambda.ts b/src/server/aws_lambda.ts index a8174df90..8bd8dc0fd 100644 --- a/src/server/aws_lambda.ts +++ b/src/server/aws_lambda.ts @@ -4,6 +4,7 @@ import Honeybadger from '../core/client' import type { Handler, Callback, Context } from 'aws-lambda' import { AsyncStore } from './async_store'; import { ServerlessConfig } from '../core/types'; +import { GlobalStore } from '../core/store'; export type SyncHandler = ( event: TEvent, @@ -33,7 +34,7 @@ function asyncHandler(handler: AsyncHandler((resolve, reject) => { - AsyncStore.run({ context: {}, breadcrumbs: [] }, () => { + AsyncStore.run(GlobalStore.getStoreCopy(), () => { const timeoutHandler = setupTimeoutWarning(hb, context) try { handler(event, context) @@ -52,7 +53,7 @@ function asyncHandler(handler: AsyncHandler(handler: SyncHandler, hb: Honeybadger): SyncHandler { return function wrappedLambdaHandler(event, context, cb) { hb.__setStore(AsyncStore) - AsyncStore.run({ context: {}, breadcrumbs: [] }, () => { + AsyncStore.run(GlobalStore.getStoreCopy(), () => { const timeoutHandler = setupTimeoutWarning(hb, context) try { handler(event, context, (error, result) => { diff --git a/test/unit/helpers.ts b/test/unit/helpers.ts index 3a26b127f..efd61a662 100644 --- a/test/unit/helpers.ts +++ b/test/unit/helpers.ts @@ -35,10 +35,6 @@ export class TestClient extends BaseClient { return this.__store.getStore().context } - public getBreadcrumbs() { - return this.__store.getStore().breadcrumbs - } - public getPayload(noticeable: Noticeable, name: string | Partial = undefined, extra: Partial = undefined) { // called in client.notify() const notice = this.makeNotice(noticeable, name, extra) diff --git a/test/unit/server.test.ts b/test/unit/server.test.ts index f49b2d81d..5fada044f 100644 --- a/test/unit/server.test.ts +++ b/test/unit/server.test.ts @@ -87,6 +87,44 @@ describe('server client', function () { }) }) + it('combines previous global store when reporting', function () { + let expectedAssertions = 2; // Safeguard to ensure all handlers are called + + client.addBreadcrumb('global 1') + client.addBreadcrumb('global 2') + const req1 = {} + const req2 = {} + client.withRequest(req1, () => { + client.addBreadcrumb('async 1 from request 1') + }) + client.withRequest(req2, () => { + client.addBreadcrumb('async 1 from request 2') + expect(client.getBreadcrumbs()).toHaveLength(3) + expect(client.getBreadcrumbs().map(({ message }) => message)).toEqual( + ['global 1', 'global 2', 'async 1 from request 2'] + ) + expectedAssertions-- + }) + client.withRequest(req1, () => { + client.addBreadcrumb('async 2 from request 1') + expect(client.getBreadcrumbs()).toHaveLength(4) + expect(client.getBreadcrumbs().map(({ message }) => message)).toEqual( + ['global 1', 'global 2', 'async 1 from request 1', 'async 2 from request 1'] + ) + expectedAssertions-- + }) + + client.addBreadcrumb('global 3') + expect(client.getBreadcrumbs()).toHaveLength(3) + expect(client.getBreadcrumbs().map(({ message }) => message)).toEqual( + ['global 1', 'global 2', 'global 3'] + ) + + if (expectedAssertions !== 0) { + throw new Error(`Not all assertions ran. ${expectedAssertions} assertions did not run.`) + } + }) + describe('afterNotify', function () { beforeEach(function () { client.configure({