Skip to content

Commit 35c4f73

Browse files
authored
feat: flush events on process exit (#1430)
1 parent c598260 commit 35c4f73

File tree

13 files changed

+331
-6
lines changed

13 files changed

+331
-6
lines changed

packages/core/src/client.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,14 @@ export abstract class Client {
325325
})
326326
}
327327

328+
/**
329+
* This method currently flushes the event (Insights) queue.
330+
* In the future, it should also flush the error queue (assuming an error throttler is implemented).
331+
*/
332+
flushAsync(): Promise<void> {
333+
return this.__eventsLogger.flushAsync();
334+
}
335+
328336
__getBreadcrumbs() {
329337
return this.__store.getContents('breadcrumbs').slice()
330338
}

packages/core/src/throttled_events_logger.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,19 @@ export class ThrottledEventsLogger implements EventsLogger {
3030
}
3131
}
3232

33+
flushAsync(): Promise<void> {
34+
this.logger.debug('[Honeybadger] Flushing events')
35+
return this.send();
36+
}
37+
3338
private processQueue() {
3439
if (this.queue.length === 0 || this.isProcessing) {
3540
return
3641
}
3742

3843
this.isProcessing = true
39-
const eventsData = this.queue.slice()
40-
this.queue = []
4144

42-
const data = NdJson.stringify(eventsData)
43-
this.makeHttpRequest(data)
45+
this.send()
4446
.then(() => {
4547
setTimeout(() => {
4648
this.isProcessing = false
@@ -57,6 +59,18 @@ export class ThrottledEventsLogger implements EventsLogger {
5759
})
5860
}
5961

62+
private async send(): Promise<void> {
63+
if (this.queue.length === 0) {
64+
return;
65+
}
66+
67+
const eventsData = this.queue.slice()
68+
this.queue = []
69+
70+
const data = NdJson.stringify(eventsData)
71+
return this.makeHttpRequest(data)
72+
}
73+
6074
private async makeHttpRequest(data: string): Promise<void> {
6175
return this.transport
6276
.send({

packages/core/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export interface Logger {
1111
export interface EventsLogger {
1212
configure: (opts: Partial<Config>) => void
1313
log(data: EventPayload): void
14+
flushAsync(): Promise<void>
1415
}
1516

1617
export type EventPayload = {

packages/js/src/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Client, Util, Types, Plugins as CorePlugins } from '@honeybadger-io/cor
44
import { getSourceFile, readConfigFromFileSystem } from './server/util'
55
import uncaughtException from './server/integrations/uncaught_exception_plugin'
66
import unhandledRejection from './server/integrations/unhandled_rejection_plugin'
7+
import shutdown from './server/integrations/shutdown_plugin'
78
import { errorHandler, requestHandler } from './server/middleware'
89
import { lambdaHandler } from './server/aws_lambda'
910
import { ServerTransport } from './server/transport'
@@ -15,6 +16,7 @@ const { endpoint } = Util
1516
const DEFAULT_PLUGINS = [
1617
uncaughtException(),
1718
unhandledRejection(),
19+
shutdown(),
1820
CorePlugins.events(),
1921
]
2022

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import Client from '../../server'
2+
import { fatallyLogAndExitGracefully } from '../util'
3+
4+
export default class ShutdownMonitor {
5+
6+
// SIGTERM is raised by AWS Lambda when the function is being shut down
7+
// SIGINT is raised by the user pressing CTRL+C
8+
private static KILL_SIGNALS = ['SIGTERM', 'SIGINT'] as const
9+
10+
protected __isReporting: boolean
11+
protected __client: typeof Client
12+
protected __listener: (signal: string) => void
13+
14+
constructor() {
15+
this.__isReporting = false
16+
this.__listener = this.makeListener()
17+
}
18+
19+
setClient(client: typeof Client) {
20+
this.__client = client
21+
}
22+
23+
makeListener() {
24+
// noinspection UnnecessaryLocalVariableJS
25+
const honeybadgerShutdownListener = async (signal: NodeJS.Signals) => {
26+
if (this.__isReporting || !this.__client) {
27+
return
28+
}
29+
30+
this.__isReporting = true
31+
32+
await this.__client.flushAsync()
33+
34+
this.__isReporting = false
35+
36+
if (!this.hasOtherShutdownListeners(signal)) {
37+
fatallyLogAndExitGracefully(signal)
38+
}
39+
}
40+
41+
return honeybadgerShutdownListener
42+
}
43+
44+
maybeAddListener() {
45+
for (const signal of ShutdownMonitor.KILL_SIGNALS) {
46+
const signalListeners = process.listeners(signal);
47+
if (!signalListeners.includes(this.__listener)) {
48+
process.on(signal, this.__listener)
49+
}
50+
}
51+
}
52+
53+
maybeRemoveListener() {
54+
for (const signal of ShutdownMonitor.KILL_SIGNALS) {
55+
const signalListeners = process.listeners(signal);
56+
if (signalListeners.includes(this.__listener)) {
57+
process.removeListener(signal, this.__listener)
58+
}
59+
}
60+
}
61+
62+
hasOtherShutdownListeners(signal: NodeJS.Signals) {
63+
const otherListeners = process
64+
.listeners(signal)
65+
.filter(listener => listener !== this.__listener)
66+
67+
return otherListeners.length > 0;
68+
69+
}
70+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Types } from '@honeybadger-io/core'
2+
import Client from '../../server'
3+
import ShutdownMonitor from './shutdown_monitor'
4+
5+
const shutdownMonitor = new ShutdownMonitor()
6+
7+
export default function (): Types.Plugin {
8+
return {
9+
load: (client: typeof Client) => {
10+
shutdownMonitor.setClient(client)
11+
// at the moment, the shutdown monitor only sends events from the queue
12+
// if we implement a queue for throttling errors, we won't have to check for `config.eventsEnabled`
13+
if (client.config.eventsEnabled) {
14+
shutdownMonitor.maybeAddListener()
15+
} else {
16+
shutdownMonitor.maybeRemoveListener()
17+
}
18+
},
19+
shouldReloadOnConfigure: true,
20+
}
21+
}

packages/js/src/server/integrations/uncaught_exception_monitor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default class UncaughtExceptionMonitor {
2020
}
2121

2222
makeListener() {
23+
// noinspection UnnecessaryLocalVariableJS
2324
const honeybadgerUncaughtExceptionListener = (uncaughtError: Error) => {
2425
if (this.__isReporting || !this.__client) { return }
2526

packages/js/src/server/integrations/unhandled_rejection_monitor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export default class UnhandledRejectionMonitor {
1717
}
1818

1919
makeListener() {
20+
// noinspection UnnecessaryLocalVariableJS
2021
const honeybadgerUnhandledRejectionListener = (reason: unknown, _promise: Promise<unknown>) => {
2122
this.__isReporting = true;
2223
this.__client.notify(reason as Types.Noticeable, { component: 'unhandledRejection' }, {

packages/js/src/server/util.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import { promisify } from 'util'
66

77
const readFile = promisify(fs.readFile)
88

9+
export function fatallyLogAndExitGracefully(signal: NodeJS.Signals): never {
10+
console.log(`[Honeybadger] Received ${signal}, exiting immediately`)
11+
process.exit()
12+
}
13+
914
export function fatallyLogAndExit(err: Error, source: string): never {
1015
console.error(`[Honeybadger] Exiting process due to ${source}`)
1116
console.error(err.stack || err)

packages/js/test/e2e/playwright.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export default defineConfig({
1515
forbidOnly: !!process.env.CI,
1616
/* Retry on CI only */
1717
retries: process.env.CI ? 2 : 0,
18-
workers: 8,
18+
workers: 4,
1919
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
2020
reporter: [
2121
[process.env.CI ? 'github' : 'list'],

0 commit comments

Comments
 (0)