Skip to content

Commit ce84fb3

Browse files
authored
feat(integration): Ensure LinkedErrors integration runs before all event processors (#8956)
While looking through our existing integrations, I noticed that the `LinkedErrors` integration in node had some weird/custom code to manually run the context lines integration after it processed, as we cannot guarantee any order etc. I figured it would be much cleaner to solve this with a proper hook (I went with `preprocessEvent`), as it actually makes sense for this to generally run before all other event processors run, IMHO, and we can decouple these integrations from each other.
1 parent 9e39717 commit ce84fb3

File tree

6 files changed

+117
-75
lines changed

6 files changed

+117
-75
lines changed

packages/browser/src/integrations/linkederrors.ts

Lines changed: 17 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Event, EventHint, EventProcessor, Hub, Integration } from '@sentry/types';
1+
import type { Client, Event, EventHint, Integration } from '@sentry/types';
22
import { applyAggregateErrorsToEvent } from '@sentry/utils';
33

44
import { exceptionFromError } from '../eventbuilder';
@@ -42,31 +42,25 @@ export class LinkedErrors implements Integration {
4242
this._limit = options.limit || DEFAULT_LIMIT;
4343
}
4444

45+
/** @inheritdoc */
46+
public setupOnce(): void {
47+
// noop
48+
}
49+
4550
/**
4651
* @inheritDoc
4752
*/
48-
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
49-
addGlobalEventProcessor((event: Event, hint?: EventHint) => {
50-
const hub = getCurrentHub();
51-
const client = hub.getClient();
52-
const self = hub.getIntegration(LinkedErrors);
53-
54-
if (!client || !self) {
55-
return event;
56-
}
57-
58-
const options = client.getOptions();
59-
applyAggregateErrorsToEvent(
60-
exceptionFromError,
61-
options.stackParser,
62-
options.maxValueLength,
63-
self._key,
64-
self._limit,
65-
event,
66-
hint,
67-
);
53+
public preprocessEvent(event: Event, hint: EventHint | undefined, client: Client): void {
54+
const options = client.getOptions();
6855

69-
return event;
70-
});
56+
applyAggregateErrorsToEvent(
57+
exceptionFromError,
58+
options.stackParser,
59+
options.maxValueLength,
60+
this._key,
61+
this._limit,
62+
event,
63+
hint,
64+
);
7165
}
7266
}

packages/core/src/baseclient.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
285285
*/
286286
public setupIntegrations(): void {
287287
if (this._isEnabled() && !this._integrationsInitialized) {
288-
this._integrations = setupIntegrations(this._options.integrations);
288+
this._integrations = setupIntegrations(this, this._options.integrations);
289289
this._integrationsInitialized = true;
290290
}
291291
}
@@ -315,7 +315,7 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
315315
* @inheritDoc
316316
*/
317317
public addIntegration(integration: Integration): void {
318-
setupIntegration(integration, this._integrations);
318+
setupIntegration(this, integration, this._integrations);
319319
}
320320

321321
/**
@@ -376,16 +376,23 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
376376
}
377377

378378
// Keep on() & emit() signatures in sync with types' client.ts interface
379+
/* eslint-disable @typescript-eslint/unified-signatures */
379380

380381
/** @inheritdoc */
381-
public on(hook: 'startTransaction' | 'finishTransaction', callback: (transaction: Transaction) => void): void;
382+
public on(hook: 'startTransaction', callback: (transaction: Transaction) => void): void;
383+
384+
/** @inheritdoc */
385+
public on(hook: 'finishTransaction', callback: (transaction: Transaction) => void): void;
382386

383387
/** @inheritdoc */
384388
public on(hook: 'beforeEnvelope', callback: (envelope: Envelope) => void): void;
385389

386390
/** @inheritdoc */
387391
public on(hook: 'beforeSendEvent', callback: (event: Event, hint?: EventHint) => void): void;
388392

393+
/** @inheritdoc */
394+
public on(hook: 'preprocessEvent', callback: (event: Event, hint?: EventHint) => void): void;
395+
389396
/** @inheritdoc */
390397
public on(
391398
hook: 'afterSendEvent',
@@ -412,14 +419,20 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
412419
}
413420

414421
/** @inheritdoc */
415-
public emit(hook: 'startTransaction' | 'finishTransaction', transaction: Transaction): void;
422+
public emit(hook: 'startTransaction', transaction: Transaction): void;
423+
424+
/** @inheritdoc */
425+
public emit(hook: 'finishTransaction', transaction: Transaction): void;
416426

417427
/** @inheritdoc */
418428
public emit(hook: 'beforeEnvelope', envelope: Envelope): void;
419429

420430
/** @inheritdoc */
421431
public emit(hook: 'beforeSendEvent', event: Event, hint?: EventHint): void;
422432

433+
/** @inheritdoc */
434+
public emit(hook: 'preprocessEvent', event: Event, hint?: EventHint): void;
435+
423436
/** @inheritdoc */
424437
public emit(hook: 'afterSendEvent', event: Event, sendResponse: TransportMakeRequestResponse | void): void;
425438

@@ -439,6 +452,8 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
439452
}
440453
}
441454

455+
/* eslint-enable @typescript-eslint/unified-signatures */
456+
442457
/** Updates existing session based on the provided event */
443458
protected _updateSessionFromEvent(session: Session, event: Event): void {
444459
let crashed = false;
@@ -527,6 +542,9 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
527542
if (!hint.integrations && integrations.length > 0) {
528543
hint.integrations = integrations;
529544
}
545+
546+
this.emit('preprocessEvent', event, hint);
547+
530548
return prepareEvent(options, event, hint, scope).then(evt => {
531549
if (evt === null) {
532550
return evt;

packages/core/src/integration.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Integration, Options } from '@sentry/types';
1+
import type { Client, Integration, Options } from '@sentry/types';
22
import { arrayify, logger } from '@sentry/utils';
33

44
import { getCurrentHub } from './hub';
@@ -84,28 +84,34 @@ export function getIntegrationsToSetup(options: Options): Integration[] {
8484
* @param integrations array of integration instances
8585
* @param withDefault should enable default integrations
8686
*/
87-
export function setupIntegrations(integrations: Integration[]): IntegrationIndex {
87+
export function setupIntegrations(client: Client, integrations: Integration[]): IntegrationIndex {
8888
const integrationIndex: IntegrationIndex = {};
8989

9090
integrations.forEach(integration => {
9191
// guard against empty provided integrations
9292
if (integration) {
93-
setupIntegration(integration, integrationIndex);
93+
setupIntegration(client, integration, integrationIndex);
9494
}
9595
});
9696

9797
return integrationIndex;
9898
}
9999

100100
/** Setup a single integration. */
101-
export function setupIntegration(integration: Integration, integrationIndex: IntegrationIndex): void {
101+
export function setupIntegration(client: Client, integration: Integration, integrationIndex: IntegrationIndex): void {
102102
integrationIndex[integration.name] = integration;
103103

104104
if (installedIntegrations.indexOf(integration.name) === -1) {
105105
integration.setupOnce(addGlobalEventProcessor, getCurrentHub);
106106
installedIntegrations.push(integration.name);
107-
__DEBUG_BUILD__ && logger.log(`Integration installed: ${integration.name}`);
108107
}
108+
109+
if (client.on && typeof integration.preprocessEvent === 'function') {
110+
const callback = integration.preprocessEvent.bind(integration);
111+
client.on('preprocessEvent', (event, hint) => callback(event, hint, client));
112+
}
113+
114+
__DEBUG_BUILD__ && logger.log(`Integration installed: ${integration.name}`);
109115
}
110116

111117
// Polyfill for Array.findIndex(), which is not supported in ES5
Lines changed: 18 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import type { Event, EventHint, EventProcessor, Hub, Integration } from '@sentry/types';
1+
import type { Client, Event, EventHint, Integration } from '@sentry/types';
22
import { applyAggregateErrorsToEvent } from '@sentry/utils';
33

44
import { exceptionFromError } from '../eventbuilder';
5-
import { ContextLines } from './contextlines';
65

76
const DEFAULT_KEY = 'cause';
87
const DEFAULT_LIMIT = 5;
@@ -37,39 +36,25 @@ export class LinkedErrors implements Integration {
3736
this._limit = options.limit || DEFAULT_LIMIT;
3837
}
3938

39+
/** @inheritdoc */
40+
public setupOnce(): void {
41+
// noop
42+
}
43+
4044
/**
4145
* @inheritDoc
4246
*/
43-
public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void {
44-
addGlobalEventProcessor(async (event: Event, hint?: EventHint) => {
45-
const hub = getCurrentHub();
46-
const client = hub.getClient();
47-
const self = hub.getIntegration(LinkedErrors);
48-
49-
if (!client || !self) {
50-
return event;
51-
}
52-
53-
const options = client.getOptions();
54-
55-
applyAggregateErrorsToEvent(
56-
exceptionFromError,
57-
options.stackParser,
58-
options.maxValueLength,
59-
self._key,
60-
self._limit,
61-
event,
62-
hint,
63-
);
64-
65-
// If the ContextLines integration is enabled, we add source code context to linked errors
66-
// because we can't guarantee the order that integrations are run.
67-
const contextLines = getCurrentHub().getIntegration(ContextLines);
68-
if (contextLines) {
69-
await contextLines.addSourceContext(event);
70-
}
71-
72-
return event;
73-
});
47+
public preprocessEvent(event: Event, hint: EventHint | undefined, client: Client): void {
48+
const options = client.getOptions();
49+
50+
applyAggregateErrorsToEvent(
51+
exceptionFromError,
52+
options.stackParser,
53+
options.maxValueLength,
54+
this._key,
55+
this._limit,
56+
event,
57+
hint,
58+
);
7459
}
7560
}

packages/types/src/client.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -165,21 +165,38 @@ export interface Client<O extends ClientOptions = ClientOptions> {
165165

166166
// HOOKS
167167
// TODO(v8): Make the hooks non-optional.
168+
/* eslint-disable @typescript-eslint/unified-signatures */
168169

169170
/**
170-
* Register a callback for transaction start and finish.
171+
* Register a callback for transaction start.
172+
* Receives the transaction as argument.
171173
*/
172-
on?(hook: 'startTransaction' | 'finishTransaction', callback: (transaction: Transaction) => void): void;
174+
on?(hook: 'startTransaction', callback: (transaction: Transaction) => void): void;
175+
176+
/**
177+
* Register a callback for transaction finish.
178+
* Receives the transaction as argument.
179+
*/
180+
on?(hook: 'finishTransaction', callback: (transaction: Transaction) => void): void;
173181

174182
/**
175183
* Register a callback for transaction start and finish.
176184
*/
177185
on?(hook: 'beforeEnvelope', callback: (envelope: Envelope) => void): void;
178186

179187
/**
180-
* Register a callback for before an event is sent.
188+
* Register a callback for before sending an event.
189+
* This is called right before an event is sent and should not be used to mutate the event.
190+
* Receives an Event & EventHint as arguments.
181191
*/
182-
on?(hook: 'beforeSendEvent', callback: (event: Event, hint?: EventHint | void) => void): void;
192+
on?(hook: 'beforeSendEvent', callback: (event: Event, hint?: EventHint | undefined) => void): void;
193+
194+
/**
195+
* Register a callback for preprocessing an event,
196+
* before it is passed to (global) event processors.
197+
* Receives an Event & EventHint as arguments.
198+
*/
199+
on?(hook: 'preprocessEvent', callback: (event: Event, hint?: EventHint | undefined) => void): void;
183200

184201
/**
185202
* Register a callback for when an event has been sent.
@@ -206,23 +223,36 @@ export interface Client<O extends ClientOptions = ClientOptions> {
206223
on?(hook: 'otelSpanEnd', callback: (otelSpan: unknown, mutableOptions: { drop: boolean }) => void): void;
207224

208225
/**
209-
* Fire a hook event for transaction start and finish. Expects to be given a transaction as the
210-
* second argument.
226+
* Fire a hook event for transaction start.
227+
* Expects to be given a transaction as the second argument.
211228
*/
212-
emit?(hook: 'startTransaction' | 'finishTransaction', transaction: Transaction): void;
229+
emit?(hook: 'startTransaction', transaction: Transaction): void;
230+
231+
/**
232+
* Fire a hook event for transaction finish.
233+
* Expects to be given a transaction as the second argument.
234+
*/
235+
emit?(hook: 'finishTransaction', transaction: Transaction): void;
213236

214237
/*
215238
* Fire a hook event for envelope creation and sending. Expects to be given an envelope as the
216239
* second argument.
217240
*/
218241
emit?(hook: 'beforeEnvelope', envelope: Envelope): void;
219242

220-
/*
221-
* Fire a hook event before sending an event. Expects to be given an Event & EventHint as the
222-
* second/third argument.
243+
/**
244+
* Fire a hook event before sending an event.
245+
* This is called right before an event is sent and should not be used to mutate the event.
246+
* Expects to be given an Event & EventHint as the second/third argument.
223247
*/
224248
emit?(hook: 'beforeSendEvent', event: Event, hint?: EventHint): void;
225249

250+
/**
251+
* Fire a hook event to process events before they are passed to (global) event processors.
252+
* Expects to be given an Event & EventHint as the second/third argument.
253+
*/
254+
emit?(hook: 'preprocessEvent', event: Event, hint?: EventHint): void;
255+
226256
/*
227257
* Fire a hook event after sending an event. Expects to be given an Event as the
228258
* second argument.
@@ -245,4 +275,6 @@ export interface Client<O extends ClientOptions = ClientOptions> {
245275
* The option argument may be mutated to drop the span.
246276
*/
247277
emit?(hook: 'otelSpanEnd', otelSpan: unknown, mutableOptions: { drop: boolean }): void;
278+
279+
/* eslint-enable @typescript-eslint/unified-signatures */
248280
}

packages/types/src/integration.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { Client } from './client';
2+
import type { Event, EventHint } from './event';
13
import type { EventProcessor } from './eventprocessor';
24
import type { Hub } from './hub';
35

@@ -23,4 +25,9 @@ export interface Integration {
2325
* This takes no options on purpose, options should be passed in the constructor
2426
*/
2527
setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void;
28+
29+
/**
30+
* An optional hook that allows to preprocess an event _before_ it is passed to all other event processors.
31+
*/
32+
preprocessEvent?(event: Event, hint: EventHint | undefined, client: Client): void;
2633
}

0 commit comments

Comments
 (0)