From 54958bb7ecb16a977f1bca9f1f9aef844d7cce63 Mon Sep 17 00:00:00 2001 From: Bethany Berkowitz Date: Mon, 21 Aug 2023 16:21:56 -0400 Subject: [PATCH 1/5] Ability to check for the listener before adding it --- .../uncaught_exception_monitor.ts | 26 ++++++++++++++++--- .../integrations/uncaught_exception_plugin.ts | 8 +++--- .../uncaught_exception_monitor.server.test.ts | 20 ++++++++++++-- .../uncaught_exception_plugin.server.test.ts | 2 +- 4 files changed, 46 insertions(+), 10 deletions(-) diff --git a/packages/js/src/server/integrations/uncaught_exception_monitor.ts b/packages/js/src/server/integrations/uncaught_exception_monitor.ts index 36e8345b..bf229638 100644 --- a/packages/js/src/server/integrations/uncaught_exception_monitor.ts +++ b/packages/js/src/server/integrations/uncaught_exception_monitor.ts @@ -6,14 +6,34 @@ export default class UncaughtExceptionMonitor { protected __isReporting: boolean protected __handlerAlreadyCalled: boolean protected __client: typeof Client + // TODO: binding this makes the name 'bound honeybadgerUncaughtExceptionlistener' + // could instead do something like 'makeListener()' + // the name is not critical afaik, but I'd like it to be clean + protected __listener = function honeybadgerUncaughtExceptionListener(uncaughtError) { + this.handleUncaughtException(uncaughtError) + }.bind(this) - constructor(client: typeof Client) { + constructor() { this.__isReporting = false - this.__handlerAlreadyCalled = false - this.__client = client + this.__handlerAlreadyCalled = false this.removeAwsLambdaListener() } + setClient(client: typeof Client) { + this.__client = client + } + + maybeAddListener() { + const listeners = process.listeners('uncaughtException') + if (!listeners.includes(this.__listener)) { + process.on('uncaughtException', this.__listener) + } + } + + maybeRemoveListener() { + + } + removeAwsLambdaListener() { const isLambda = !!process.env.LAMBDA_TASK_ROOT if (!isLambda) { return } diff --git a/packages/js/src/server/integrations/uncaught_exception_plugin.ts b/packages/js/src/server/integrations/uncaught_exception_plugin.ts index 1d227cb0..8ef1ff44 100644 --- a/packages/js/src/server/integrations/uncaught_exception_plugin.ts +++ b/packages/js/src/server/integrations/uncaught_exception_plugin.ts @@ -2,16 +2,16 @@ import { Types } from '@honeybadger-io/core' import Client from '../../server' import UncaughtExceptionMonitor from './uncaught_exception_monitor' +const uncaughtExceptionMonitor = new UncaughtExceptionMonitor() + export default function (): Types.Plugin { return { load: (client: typeof Client) => { + uncaughtExceptionMonitor.setClient(client) if (!client.config.enableUncaught) { return } - const uncaughtExceptionMonitor = new UncaughtExceptionMonitor(client) - process.on('uncaughtException', function honeybadgerUncaughtExceptionListener(uncaughtError) { - uncaughtExceptionMonitor.handleUncaughtException(uncaughtError) - }) + uncaughtExceptionMonitor.maybeAddListener() } } } diff --git a/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts b/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts index 94eae6d0..646b8ab7 100644 --- a/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts +++ b/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts @@ -18,7 +18,8 @@ describe('UncaughtExceptionMonitor', () => { { apiKey: 'testKey', afterUncaught: jest.fn(), logger: nullLogger() }, new TestTransport() ) as unknown as typeof Singleton - uncaughtExceptionMonitor = new UncaughtExceptionMonitor(client) + uncaughtExceptionMonitor = new UncaughtExceptionMonitor() + uncaughtExceptionMonitor.setClient(client) // Have to mock fatallyLogAndExit or we will crash the test fatallyLogAndExitSpy = jest .spyOn(util, 'fatallyLogAndExit') @@ -42,7 +43,7 @@ describe('UncaughtExceptionMonitor', () => { // Using any rather than the real type so we can test and spy on private methods // eslint-disable-next-line @typescript-eslint/no-explicit-any - const newMonitor = new UncaughtExceptionMonitor(client) as any + const newMonitor = new UncaughtExceptionMonitor() as any expect(removeLambdaSpy).toHaveBeenCalledTimes(1) expect(newMonitor.__isReporting).toBe(false) expect(newMonitor.__handlerAlreadyCalled).toBe(false) @@ -51,6 +52,21 @@ describe('UncaughtExceptionMonitor', () => { }) }) + describe('maybeAddListener', () => { + it('adds our listener if it is not already added', () => { + uncaughtExceptionMonitor.maybeAddListener() + const listeners = process.listeners('uncaughtException') + expect(listeners.length).toBe(1) + }) + + it('does not add it again if called again', () => { + uncaughtExceptionMonitor.maybeAddListener() + uncaughtExceptionMonitor.maybeAddListener() + const listeners = process.listeners('uncaughtException') + expect(listeners.length).toBe(1) + }) + }) + describe('handleUncaughtException', () => { const error = new Error('dang, broken again') diff --git a/packages/js/test/unit/server/integrations/uncaught_exception_plugin.server.test.ts b/packages/js/test/unit/server/integrations/uncaught_exception_plugin.server.test.ts index c1ca9620..5ad7e7d2 100644 --- a/packages/js/test/unit/server/integrations/uncaught_exception_plugin.server.test.ts +++ b/packages/js/test/unit/server/integrations/uncaught_exception_plugin.server.test.ts @@ -38,7 +38,7 @@ describe('Uncaught Exception Plugin', () => { load(client) const listeners = process.listeners('uncaughtException') expect(listeners.length).toBe(1) - expect(listeners[0].name).toBe('honeybadgerUncaughtExceptionListener') + expect(listeners[0].name).toBe('bound honeybadgerUncaughtExceptionListener') const error = new Error('uncaught') process.emit('uncaughtException', error) From 3235fad8b178c51817fd05282ea3ec30b402542a Mon Sep 17 00:00:00 2001 From: Bethany Berkowitz Date: Wed, 23 Aug 2023 11:37:32 -0400 Subject: [PATCH 2/5] Fix naming / this binding of listener function --- .../uncaught_exception_monitor.ts | 75 +++++++++---------- .../uncaught_exception_monitor.server.test.ts | 14 ++-- .../uncaught_exception_plugin.server.test.ts | 2 +- 3 files changed, 46 insertions(+), 45 deletions(-) diff --git a/packages/js/src/server/integrations/uncaught_exception_monitor.ts b/packages/js/src/server/integrations/uncaught_exception_monitor.ts index bf229638..c208c794 100644 --- a/packages/js/src/server/integrations/uncaught_exception_monitor.ts +++ b/packages/js/src/server/integrations/uncaught_exception_monitor.ts @@ -6,16 +6,12 @@ export default class UncaughtExceptionMonitor { protected __isReporting: boolean protected __handlerAlreadyCalled: boolean protected __client: typeof Client - // TODO: binding this makes the name 'bound honeybadgerUncaughtExceptionlistener' - // could instead do something like 'makeListener()' - // the name is not critical afaik, but I'd like it to be clean - protected __listener = function honeybadgerUncaughtExceptionListener(uncaughtError) { - this.handleUncaughtException(uncaughtError) - }.bind(this) + protected __listener: (error: Error) => void constructor() { this.__isReporting = false this.__handlerAlreadyCalled = false + this.__listener = this.makeListener() this.removeAwsLambdaListener() } @@ -23,6 +19,41 @@ export default class UncaughtExceptionMonitor { this.__client = client } + makeListener() { + const honeybadgerUncaughtExceptionListener = (uncaughtError: Error) => { + if (this.__isReporting || !this.__client) { return } + + if (!this.__client.config.enableUncaught) { + this.__client.config.afterUncaught(uncaughtError) + if (!this.hasOtherUncaughtExceptionListeners()) { + fatallyLogAndExit(uncaughtError) + } + return + } + + // report only the first error - prevent reporting recursive errors + if (this.__handlerAlreadyCalled) { + if (!this.hasOtherUncaughtExceptionListeners()) { + fatallyLogAndExit(uncaughtError) + } + return + } + + this.__isReporting = true + this.__client.notify(uncaughtError, { + afterNotify: (_err, _notice) => { + this.__isReporting = false + this.__handlerAlreadyCalled = true + this.__client.config.afterUncaught(uncaughtError) + if (!this.hasOtherUncaughtExceptionListeners()) { + fatallyLogAndExit(uncaughtError) + } + } + }) + } + return honeybadgerUncaughtExceptionListener + } + maybeAddListener() { const listeners = process.listeners('uncaughtException') if (!listeners.includes(this.__listener)) { @@ -55,36 +86,4 @@ export default class UncaughtExceptionMonitor { }) return allListeners.length - domainListeners.length > 1 } - - handleUncaughtException(uncaughtError: Error) { - if (this.__isReporting) { return } - - if (!this.__client.config.enableUncaught) { - this.__client.config.afterUncaught(uncaughtError) - if (!this.hasOtherUncaughtExceptionListeners()) { - fatallyLogAndExit(uncaughtError) - } - return - } - - // report only the first error - prevent reporting recursive errors - if (this.__handlerAlreadyCalled) { - if (!this.hasOtherUncaughtExceptionListeners()) { - fatallyLogAndExit(uncaughtError) - } - return - } - - this.__isReporting = true - this.__client.notify(uncaughtError, { - afterNotify: (_err, _notice) => { - this.__isReporting = false - this.__handlerAlreadyCalled = true - this.__client.config.afterUncaught(uncaughtError) - if (!this.hasOtherUncaughtExceptionListeners()) { - fatallyLogAndExit(uncaughtError) - } - } - }) - } } \ No newline at end of file diff --git a/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts b/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts index 646b8ab7..d00b6284 100644 --- a/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts +++ b/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts @@ -47,6 +47,8 @@ describe('UncaughtExceptionMonitor', () => { expect(removeLambdaSpy).toHaveBeenCalledTimes(1) expect(newMonitor.__isReporting).toBe(false) expect(newMonitor.__handlerAlreadyCalled).toBe(false) + expect(newMonitor.__listener).toStrictEqual(expect.any(Function)) + expect(newMonitor.__listener.name).toBe('honeybadgerUncaughtExceptionListener') process.env = restoreEnv }) @@ -67,11 +69,11 @@ describe('UncaughtExceptionMonitor', () => { }) }) - describe('handleUncaughtException', () => { + describe('__listener', () => { const error = new Error('dang, broken again') it('calls notify, afterUncaught, and fatallyLogAndExit', (done) => { - uncaughtExceptionMonitor.handleUncaughtException(error) + uncaughtExceptionMonitor.__listener(error) expect(notifySpy).toHaveBeenCalledTimes(1) expect(notifySpy).toHaveBeenCalledWith( error, @@ -86,7 +88,7 @@ describe('UncaughtExceptionMonitor', () => { it('returns if it is already reporting', () => { uncaughtExceptionMonitor.__isReporting = true - uncaughtExceptionMonitor.handleUncaughtException(error) + uncaughtExceptionMonitor.__listener(error) expect(notifySpy).not.toHaveBeenCalled() expect(fatallyLogAndExitSpy).not.toHaveBeenCalled() }) @@ -94,21 +96,21 @@ describe('UncaughtExceptionMonitor', () => { it('returns if it was already called and there are other listeners', () => { process.on('uncaughtException', () => true) process.on('uncaughtException', () => true) - uncaughtExceptionMonitor.handleUncaughtException(error) + uncaughtExceptionMonitor.__listener(error) expect(notifySpy).toHaveBeenCalledTimes(1) client.afterNotify(() => { expect(fatallyLogAndExitSpy).not.toHaveBeenCalled() expect(uncaughtExceptionMonitor.__handlerAlreadyCalled).toBe(true) // Doesn't notify a second time - uncaughtExceptionMonitor.handleUncaughtException(error) + uncaughtExceptionMonitor.__listener(error) expect(notifySpy).toHaveBeenCalledTimes(1) }) }) it('exits if it was already called and there are no other listeners', () => { uncaughtExceptionMonitor.__handlerAlreadyCalled = true - uncaughtExceptionMonitor.handleUncaughtException(error) + uncaughtExceptionMonitor.__listener(error) expect(notifySpy).not.toHaveBeenCalled() expect(fatallyLogAndExitSpy).toHaveBeenCalledWith(error) }) diff --git a/packages/js/test/unit/server/integrations/uncaught_exception_plugin.server.test.ts b/packages/js/test/unit/server/integrations/uncaught_exception_plugin.server.test.ts index 5ad7e7d2..c1ca9620 100644 --- a/packages/js/test/unit/server/integrations/uncaught_exception_plugin.server.test.ts +++ b/packages/js/test/unit/server/integrations/uncaught_exception_plugin.server.test.ts @@ -38,7 +38,7 @@ describe('Uncaught Exception Plugin', () => { load(client) const listeners = process.listeners('uncaughtException') expect(listeners.length).toBe(1) - expect(listeners[0].name).toBe('bound honeybadgerUncaughtExceptionListener') + expect(listeners[0].name).toBe('honeybadgerUncaughtExceptionListener') const error = new Error('uncaught') process.emit('uncaughtException', error) From ca109e8feebdfe9ad017aa3b09262266887cdde8 Mon Sep 17 00:00:00 2001 From: Bethany Berkowitz Date: Wed, 23 Aug 2023 11:48:55 -0400 Subject: [PATCH 3/5] maybeRemoveListener function --- .../uncaught_exception_monitor.ts | 5 ++++- .../uncaught_exception_monitor.server.test.ts | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/packages/js/src/server/integrations/uncaught_exception_monitor.ts b/packages/js/src/server/integrations/uncaught_exception_monitor.ts index c208c794..96cc6671 100644 --- a/packages/js/src/server/integrations/uncaught_exception_monitor.ts +++ b/packages/js/src/server/integrations/uncaught_exception_monitor.ts @@ -62,7 +62,10 @@ export default class UncaughtExceptionMonitor { } maybeRemoveListener() { - + const listeners = process.listeners('uncaughtException') + if (listeners.includes(this.__listener)) { + process.removeListener('uncaughtException', this.__listener) + } } removeAwsLambdaListener() { diff --git a/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts b/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts index d00b6284..c58987f9 100644 --- a/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts +++ b/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts @@ -69,6 +69,25 @@ describe('UncaughtExceptionMonitor', () => { }) }) + describe('maybeRemoveListener', () => { + it('removes our listener if it is present', () => { + uncaughtExceptionMonitor.maybeAddListener() + process.on('uncaughtException', (err) => { console.log(err) }) + expect(process.listeners('uncaughtException').length).toBe(2) + + uncaughtExceptionMonitor.maybeRemoveListener() + expect(process.listeners('uncaughtException').length).toBe(1) + }) + + it('does nothing if our listener is not present', () => { + process.on('uncaughtException', (err) => { console.log(err) }) + expect(process.listeners('uncaughtException').length).toBe(1) + + uncaughtExceptionMonitor.maybeRemoveListener() + expect(process.listeners('uncaughtException').length).toBe(1) + }) + }) + describe('__listener', () => { const error = new Error('dang, broken again') From ccd13ec898d346dace794efb1242604b1fc17707 Mon Sep 17 00:00:00 2001 From: Bethany Berkowitz Date: Wed, 23 Aug 2023 13:53:30 -0400 Subject: [PATCH 4/5] Check other exception listeners against ours --- .../integrations/uncaught_exception_monitor.ts | 16 +++++++++------- .../uncaught_exception_monitor.server.test.ts | 8 ++------ 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/js/src/server/integrations/uncaught_exception_monitor.ts b/packages/js/src/server/integrations/uncaught_exception_monitor.ts index 96cc6671..485a93f8 100644 --- a/packages/js/src/server/integrations/uncaught_exception_monitor.ts +++ b/packages/js/src/server/integrations/uncaught_exception_monitor.ts @@ -79,14 +79,16 @@ export default class UncaughtExceptionMonitor { * we want to report the exception to Honeybadger and * mimic the default behavior of NodeJs, * which is to exit the process with code 1 + * + * Node sets up domainUncaughtExceptionClear when we use domains. + * Since they're not set up by a user, they shouldn't affect whether we exit or not */ - hasOtherUncaughtExceptionListeners() { + hasOtherUncaughtExceptionListeners(): boolean { const allListeners = process.listeners('uncaughtException') - // Node sets up these listeners when we use domains - // Since they're not set up by a user, they shouldn't affect whether we exit or not - const domainListeners = allListeners.filter(listener => { - return listener.name === 'domainUncaughtExceptionClear' - }) - return allListeners.length - domainListeners.length > 1 + const otherListeners = allListeners.filter(listener => ( + listener.name !== 'domainUncaughtExceptionClear' + && listener !== this.__listener + )) + return otherListeners.length > 0 } } \ No newline at end of file diff --git a/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts b/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts index c58987f9..9b49e8f7 100644 --- a/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts +++ b/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts @@ -137,9 +137,7 @@ describe('UncaughtExceptionMonitor', () => { describe('hasOtherUncaughtExceptionListeners', () => { it('returns true if there are user-added listeners', () => { - process.on('uncaughtException', function honeybadgerUncaughtExceptionListener() { - return - }) + uncaughtExceptionMonitor.maybeAddListener() process.on('uncaughtException', function domainUncaughtExceptionClear() { return }) @@ -150,9 +148,7 @@ describe('UncaughtExceptionMonitor', () => { }) it('returns false if there are only our expected listeners', () => { - process.on('uncaughtException', function honeybadgerUncaughtExceptionListener() { - return - }) + uncaughtExceptionMonitor.maybeAddListener() process.on('uncaughtException', function domainUncaughtExceptionClear() { return }) From a89d9d99b943c39db34bc98cd6ceb8e12b91cbd0 Mon Sep 17 00:00:00 2001 From: Bethany Berkowitz Date: Wed, 23 Aug 2023 14:16:38 -0400 Subject: [PATCH 5/5] Allow for reloading plugin --- .../integrations/uncaught_exception_plugin.ts | 7 +++-- .../uncaught_exception_monitor.server.test.ts | 27 +++++++++-------- .../uncaught_exception_plugin.server.test.ts | 30 +++++++++++++++---- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/packages/js/src/server/integrations/uncaught_exception_plugin.ts b/packages/js/src/server/integrations/uncaught_exception_plugin.ts index 8ef1ff44..2f541f7d 100644 --- a/packages/js/src/server/integrations/uncaught_exception_plugin.ts +++ b/packages/js/src/server/integrations/uncaught_exception_plugin.ts @@ -8,10 +8,11 @@ export default function (): Types.Plugin { return { load: (client: typeof Client) => { uncaughtExceptionMonitor.setClient(client) - if (!client.config.enableUncaught) { - return + if (client.config.enableUncaught) { + uncaughtExceptionMonitor.maybeAddListener() + } else { + uncaughtExceptionMonitor.maybeRemoveListener() } - uncaughtExceptionMonitor.maybeAddListener() } } } diff --git a/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts b/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts index 9b49e8f7..8ade2f6f 100644 --- a/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts +++ b/packages/js/test/unit/server/integrations/uncaught_exception_monitor.server.test.ts @@ -4,6 +4,10 @@ import Singleton from '../../../../src/server' import UncaughtExceptionMonitor from '../../../../src/server/integrations/uncaught_exception_monitor' import * as aws from '../../../../src/server/aws_lambda' +function getListenerCount() { + return process.listeners('uncaughtException').length +} + describe('UncaughtExceptionMonitor', () => { // Using any rather than the real type so we can test and spy on private methods // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -55,17 +59,14 @@ describe('UncaughtExceptionMonitor', () => { }) describe('maybeAddListener', () => { - it('adds our listener if it is not already added', () => { - uncaughtExceptionMonitor.maybeAddListener() - const listeners = process.listeners('uncaughtException') - expect(listeners.length).toBe(1) - }) - - it('does not add it again if called again', () => { + it('adds our listener a maximum of one time', () => { + expect(getListenerCount()).toBe(0) + // Adds our listener uncaughtExceptionMonitor.maybeAddListener() + expect(getListenerCount()).toBe(1) + // Doesn't add a duplicate uncaughtExceptionMonitor.maybeAddListener() - const listeners = process.listeners('uncaughtException') - expect(listeners.length).toBe(1) + expect(getListenerCount()).toBe(1) }) }) @@ -73,18 +74,18 @@ describe('UncaughtExceptionMonitor', () => { it('removes our listener if it is present', () => { uncaughtExceptionMonitor.maybeAddListener() process.on('uncaughtException', (err) => { console.log(err) }) - expect(process.listeners('uncaughtException').length).toBe(2) + expect(getListenerCount()).toBe(2) uncaughtExceptionMonitor.maybeRemoveListener() - expect(process.listeners('uncaughtException').length).toBe(1) + expect(getListenerCount()).toBe(1) }) it('does nothing if our listener is not present', () => { process.on('uncaughtException', (err) => { console.log(err) }) - expect(process.listeners('uncaughtException').length).toBe(1) + expect(getListenerCount()).toBe(1) uncaughtExceptionMonitor.maybeRemoveListener() - expect(process.listeners('uncaughtException').length).toBe(1) + expect(getListenerCount()).toBe(1) }) }) diff --git a/packages/js/test/unit/server/integrations/uncaught_exception_plugin.server.test.ts b/packages/js/test/unit/server/integrations/uncaught_exception_plugin.server.test.ts index c1ca9620..88d97396 100644 --- a/packages/js/test/unit/server/integrations/uncaught_exception_plugin.server.test.ts +++ b/packages/js/test/unit/server/integrations/uncaught_exception_plugin.server.test.ts @@ -3,6 +3,14 @@ import { TestTransport, TestClient, nullLogger } from '../../helpers' import * as util from '../../../../src/server/util' import Singleton from '../../../../src/server' +function getListeners() { + return process.listeners('uncaughtException') +} + +function getListenerCount() { + return getListeners().length +} + describe('Uncaught Exception Plugin', () => { let client: typeof Singleton let notifySpy: jest.SpyInstance @@ -34,9 +42,9 @@ describe('Uncaught Exception Plugin', () => { describe('load', () => { const load = plugin().load - it('attaches a listener for uncaughtException', () => { + it('adds a listener for uncaughtException if enableUncaught is true', () => { load(client) - const listeners = process.listeners('uncaughtException') + const listeners = getListeners() expect(listeners.length).toBe(1) expect(listeners[0].name).toBe('honeybadgerUncaughtExceptionListener') @@ -45,11 +53,23 @@ describe('Uncaught Exception Plugin', () => { expect(notifySpy).toHaveBeenCalledTimes(1) }) - it('returns if enableUncaught is not true', () => { + it('does not add a listener if enableUncaught is false', () => { + client.configure({ enableUncaught: false }) + load(client) + expect(getListenerCount()).toBe(0) + }) + + it('adds or removes listener if needed when reloaded', () => { + load(client) + expect(getListenerCount()).toBe(1) + client.configure({ enableUncaught: false }) load(client) - const listeners = process.listeners('uncaughtException') - expect(listeners.length).toBe(0) + expect(getListenerCount()).toBe(0) + + client.configure({ enableUncaught: true }) + load(client) + expect(getListenerCount()).toBe(1) }) }) })