Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 79ed42b

Browse files
committed
feat(core): allow unregistering callback through on
This commit updates the return signature of the client's `on` function to be a void function, which, when executed, unregisters a callback. This adjustment is necessary for managing instances where objects are created and destroyed, ensuring that callbacks are properly unregistered to prevent self-referencing in callback closures and facilitate proper garbage collection. Typically, changing a type from `void` to `() => void` (or `VoidFunction`) shouldn't be considered a breaking change because `void` signifies the absence of a return value, implying that the return value of the `on` function should never be used by consumers. Opting for the `on` approach, which returns a cleanup function, "seems" simpler because having another function called `off` requires saving the callback reference for later removal. With our pattern, we encapsulate both the registration and removal of event listeners within a single function call.
1 parent 4b6cb4a commit 79ed42b

File tree

3 files changed

+120
-29
lines changed

3 files changed

+120
-29
lines changed

packages/core/src/baseclient.ts

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -386,37 +386,40 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
386386
/* eslint-disable @typescript-eslint/unified-signatures */
387387

388388
/** @inheritdoc */
389-
public on(hook: 'spanStart', callback: (span: Span) => void): void;
389+
public on(hook: 'spanStart', callback: (span: Span) => void): () => void;
390390

391391
/** @inheritdoc */
392-
public on(hook: 'spanEnd', callback: (span: Span) => void): void;
392+
public on(hook: 'spanEnd', callback: (span: Span) => void): () => void;
393393

394394
/** @inheritdoc */
395-
public on(hook: 'idleSpanEnableAutoFinish', callback: (span: Span) => void): void;
395+
public on(hook: 'idleSpanEnableAutoFinish', callback: (span: Span) => void): () => void;
396396

397397
/** @inheritdoc */
398-
public on(hook: 'beforeEnvelope', callback: (envelope: Envelope) => void): void;
398+
public on(hook: 'beforeEnvelope', callback: (envelope: Envelope) => void): () => void;
399399

400400
/** @inheritdoc */
401-
public on(hook: 'beforeSendEvent', callback: (event: Event, hint?: EventHint) => void): void;
401+
public on(hook: 'beforeSendEvent', callback: (event: Event, hint?: EventHint) => void): () => void;
402402

403403
/** @inheritdoc */
404-
public on(hook: 'preprocessEvent', callback: (event: Event, hint?: EventHint) => void): void;
404+
public on(hook: 'preprocessEvent', callback: (event: Event, hint?: EventHint) => void): () => void;
405405

406406
/** @inheritdoc */
407-
public on(hook: 'afterSendEvent', callback: (event: Event, sendResponse: TransportMakeRequestResponse) => void): void;
407+
public on(
408+
hook: 'afterSendEvent',
409+
callback: (event: Event, sendResponse: TransportMakeRequestResponse) => void,
410+
): () => void;
408411

409412
/** @inheritdoc */
410-
public on(hook: 'beforeAddBreadcrumb', callback: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => void): void;
413+
public on(hook: 'beforeAddBreadcrumb', callback: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => void): () => void;
411414

412415
/** @inheritdoc */
413-
public on(hook: 'createDsc', callback: (dsc: DynamicSamplingContext) => void): void;
416+
public on(hook: 'createDsc', callback: (dsc: DynamicSamplingContext) => void): () => void;
414417

415418
/** @inheritdoc */
416419
public on(
417420
hook: 'beforeSendFeedback',
418421
callback: (feedback: FeedbackEvent, options?: { includeReplay: boolean }) => void,
419-
): void;
422+
): () => void;
420423

421424
/** @inheritdoc */
422425
public on(
@@ -439,23 +442,41 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
439442
options: StartSpanOptions,
440443
traceOptions?: { sentryTrace?: string | undefined; baggage?: string | undefined },
441444
) => void,
442-
): void;
445+
): () => void;
443446

444447
/** @inheritdoc */
445-
public on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void;
448+
public on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): () => void;
446449

447-
public on(hook: 'flush', callback: () => void): void;
450+
public on(hook: 'flush', callback: () => void): () => void;
448451

449-
public on(hook: 'close', callback: () => void): void;
452+
public on(hook: 'close', callback: () => void): () => void;
450453

451454
/** @inheritdoc */
452-
public on(hook: string, callback: unknown): void {
455+
public on(hook: string, callback: unknown): () => void {
456+
// Note that the code below, with nullish coalescing assignment,
457+
// may reduce the code, so it may be switched to when Node 14 support
458+
// is dropped (the `??=` operator is supported since Node 15).
459+
// (this._hooks[hook] ??= []).push(callback);
453460
if (!this._hooks[hook]) {
454461
this._hooks[hook] = [];
455462
}
456463

457464
// @ts-expect-error We assue the types are correct
458465
this._hooks[hook].push(callback);
466+
467+
// This function returns a callback execution handler that, when invoked,
468+
// deregisters a callback. This is crucial for managing instances where callbacks
469+
// need to be unregistered to prevent self-referencing in callback closures,
470+
// ensuring proper garbage collection.
471+
return () => {
472+
const hooks = this._hooks[hook];
473+
474+
if (hooks) {
475+
// @ts-expect-error We assue the types are correct
476+
const cbIndex = hooks.indexOf(callback);
477+
hooks.splice(cbIndex, 1);
478+
}
479+
};
459480
}
460481

461482
/** @inheritdoc */

packages/core/test/lib/base.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1880,4 +1880,59 @@ describe('BaseClient', () => {
18801880
});
18811881
});
18821882
});
1883+
1884+
describe('hook removal with `on`', () => {
1885+
it('should return a cleanup function that, when executed, unregisters a hook', async () => {
1886+
expect.assertions(8);
1887+
1888+
const client = new TestClient(
1889+
getDefaultTestClientOptions({
1890+
dsn: PUBLIC_DSN,
1891+
enableSend: true,
1892+
}),
1893+
);
1894+
1895+
// @ts-expect-error Accessing private transport API
1896+
const mockSend = jest.spyOn(client._transport, 'send').mockImplementation(() => {
1897+
return Promise.resolve({ statusCode: 200 });
1898+
});
1899+
1900+
const errorEvent: Event = { message: 'error' };
1901+
1902+
const callback = jest.fn();
1903+
const removeAfterSendEventListenerFn = client.on('afterSendEvent', callback);
1904+
1905+
// @ts-expect-error Accessing private client API
1906+
expect(client._hooks['afterSendEvent']).toEqual([callback]);
1907+
1908+
client.sendEvent(errorEvent);
1909+
jest.runAllTimers();
1910+
// Wait for two ticks
1911+
// note that for whatever reason, await new Promise(resolve => setTimeout(resolve, 0)) causes the test to hang
1912+
await undefined;
1913+
await undefined;
1914+
1915+
expect(mockSend).toBeCalledTimes(1);
1916+
expect(callback).toBeCalledTimes(1);
1917+
expect(callback).toBeCalledWith(errorEvent, { statusCode: 200 });
1918+
1919+
// Should unregister `afterSendEvent` callback.
1920+
removeAfterSendEventListenerFn();
1921+
// @ts-expect-error Accessing private client API
1922+
expect(client._hooks['afterSendEvent']).toEqual([]);
1923+
1924+
client.sendEvent(errorEvent);
1925+
jest.runAllTimers();
1926+
// Wait for two ticks
1927+
// note that for whatever reason, await new Promise(resolve => setTimeout(resolve, 0)) causes the test to hang
1928+
await undefined;
1929+
await undefined;
1930+
1931+
expect(mockSend).toBeCalledTimes(2);
1932+
// Note that the `callback` has still been called only once and not twice,
1933+
// because we unregistered it.
1934+
expect(callback).toBeCalledTimes(1);
1935+
expect(callback).toBeCalledWith(errorEvent, { statusCode: 200 });
1936+
});
1937+
});
18831938
});

packages/types/src/client.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,14 @@ export interface Client<O extends ClientOptions = ClientOptions> {
181181
/**
182182
* Register a callback for whenever a span is started.
183183
* Receives the span as argument.
184+
* @returns A function that, when executed, removes the registered callback.
184185
*/
185-
on(hook: 'spanStart', callback: (span: Span) => void): void;
186+
on(hook: 'spanStart', callback: (span: Span) => void): () => void;
186187

187188
/**
188189
* Register a callback before span sampling runs. Receives a `samplingDecision` object argument with a `decision`
189190
* property that can be used to make a sampling decision that will be enforced, before any span sampling runs.
191+
* @returns A function that, when executed, removes the registered callback.
190192
*/
191193
on(
192194
hook: 'beforeSampling',
@@ -204,83 +206,96 @@ export interface Client<O extends ClientOptions = ClientOptions> {
204206
/**
205207
* Register a callback for whenever a span is ended.
206208
* Receives the span as argument.
209+
* @returns A function that, when executed, removes the registered callback.
207210
*/
208-
on(hook: 'spanEnd', callback: (span: Span) => void): void;
211+
on(hook: 'spanEnd', callback: (span: Span) => void): () => void;
209212

210213
/**
211214
* Register a callback for when an idle span is allowed to auto-finish.
215+
* @returns A function that, when executed, removes the registered callback.
212216
*/
213-
on(hook: 'idleSpanEnableAutoFinish', callback: (span: Span) => void): void;
217+
on(hook: 'idleSpanEnableAutoFinish', callback: (span: Span) => void): () => void;
214218

215219
/**
216220
* Register a callback for transaction start and finish.
221+
* @returns A function that, when executed, removes the registered callback.
217222
*/
218-
on(hook: 'beforeEnvelope', callback: (envelope: Envelope) => void): void;
223+
on(hook: 'beforeEnvelope', callback: (envelope: Envelope) => void): () => void;
219224

220225
/**
221226
* Register a callback for before sending an event.
222227
* This is called right before an event is sent and should not be used to mutate the event.
223228
* Receives an Event & EventHint as arguments.
229+
* @returns A function that, when executed, removes the registered callback.
224230
*/
225-
on(hook: 'beforeSendEvent', callback: (event: Event, hint?: EventHint | undefined) => void): void;
231+
on(hook: 'beforeSendEvent', callback: (event: Event, hint?: EventHint | undefined) => void): () => void;
226232

227233
/**
228234
* Register a callback for preprocessing an event,
229235
* before it is passed to (global) event processors.
230236
* Receives an Event & EventHint as arguments.
237+
* @returns A function that, when executed, removes the registered callback.
231238
*/
232-
on(hook: 'preprocessEvent', callback: (event: Event, hint?: EventHint | undefined) => void): void;
239+
on(hook: 'preprocessEvent', callback: (event: Event, hint?: EventHint | undefined) => void): () => void;
233240

234241
/**
235242
* Register a callback for when an event has been sent.
243+
* @returns A function that, when executed, removes the registered callback.
236244
*/
237-
on(hook: 'afterSendEvent', callback: (event: Event, sendResponse: TransportMakeRequestResponse) => void): void;
245+
on(hook: 'afterSendEvent', callback: (event: Event, sendResponse: TransportMakeRequestResponse) => void): () => void;
238246

239247
/**
240248
* Register a callback before a breadcrumb is added.
249+
* @returns A function that, when executed, removes the registered callback.
241250
*/
242-
on(hook: 'beforeAddBreadcrumb', callback: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => void): void;
251+
on(hook: 'beforeAddBreadcrumb', callback: (breadcrumb: Breadcrumb, hint?: BreadcrumbHint) => void): () => void;
243252

244253
/**
245254
* Register a callback when a DSC (Dynamic Sampling Context) is created.
255+
* @returns A function that, when executed, removes the registered callback.
246256
*/
247-
on(hook: 'createDsc', callback: (dsc: DynamicSamplingContext) => void): void;
257+
on(hook: 'createDsc', callback: (dsc: DynamicSamplingContext) => void): () => void;
248258

249259
/**
250260
* Register a callback when a Feedback event has been prepared.
251261
* This should be used to mutate the event. The options argument can hint
252262
* about what kind of mutation it expects.
263+
* @returns A function that, when executed, removes the registered callback.
253264
*/
254265
on(
255266
hook: 'beforeSendFeedback',
256267
callback: (feedback: FeedbackEvent, options?: { includeReplay?: boolean }) => void,
257-
): void;
268+
): () => void;
258269

259270
/**
260271
* A hook for the browser tracing integrations to trigger a span start for a page load.
272+
* @returns A function that, when executed, removes the registered callback.
261273
*/
262274
on(
263275
hook: 'startPageLoadSpan',
264276
callback: (
265277
options: StartSpanOptions,
266278
traceOptions?: { sentryTrace?: string | undefined; baggage?: string | undefined },
267279
) => void,
268-
): void;
280+
): () => void;
269281

270282
/**
271283
* A hook for browser tracing integrations to trigger a span for a navigation.
284+
* @returns A function that, when executed, removes the registered callback.
272285
*/
273-
on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): void;
286+
on(hook: 'startNavigationSpan', callback: (options: StartSpanOptions) => void): () => void;
274287

275288
/**
276289
* A hook that is called when the client is flushing
290+
* @returns A function that, when executed, removes the registered callback.
277291
*/
278-
on(hook: 'flush', callback: () => void): void;
292+
on(hook: 'flush', callback: () => void): () => void;
279293

280294
/**
281295
* A hook that is called when the client is closing
296+
* @returns A function that, when executed, removes the registered callback.
282297
*/
283-
on(hook: 'close', callback: () => void): void;
298+
on(hook: 'close', callback: () => void): () => void;
284299

285300
/** Fire a hook whener a span starts. */
286301
emit(hook: 'spanStart', span: Span): void;

0 commit comments

Comments
 (0)