Skip to content

Commit 79ec0d6

Browse files
committed
Update low quality transaction filter to an integration in react-router
1 parent e4fe07f commit 79ec0d6

File tree

4 files changed

+133
-109
lines changed

4 files changed

+133
-109
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { type Client,type Event, type EventHint, defineIntegration, logger } from '@sentry/core';
2+
import type { NodeOptions } from '@sentry/node';
3+
4+
/**
5+
* Integration that filters out noisy http transactions such as requests to node_modules, favicon.ico, @id/
6+
*
7+
*/
8+
9+
function _lowQualityTransactionsFilterIntegration(options: NodeOptions): {
10+
name: string;
11+
processEvent: (event: Event, hint: EventHint, client: Client) => Event | null;
12+
} {
13+
const matchedRegexes = [/GET \/node_modules\//, /GET \/favicon\.ico/, /GET \/@id\//];
14+
15+
return {
16+
name: 'LowQualityTransactionsFilter',
17+
18+
processEvent(event: Event, _hint: EventHint, _client: Client): Event | null {
19+
if (event.type !== 'transaction' || !event.transaction) {
20+
return event;
21+
}
22+
23+
if (matchedRegexes.some(regex => event.transaction?.match(regex))) {
24+
options.debug && logger.log('[ReactRouter] Filtered node_modules transaction:', event.transaction);
25+
return null;
26+
}
27+
28+
return event;
29+
},
30+
};
31+
}
32+
33+
34+
export const lowQualityTransactionsFilterIntegration = defineIntegration(
35+
(options: NodeOptions) => _lowQualityTransactionsFilterIntegration(options),
36+
);
Lines changed: 12 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1-
import { type EventProcessor, applySdkMetadata, getGlobalScope, logger, setTag } from '@sentry/core';
1+
import type { Integration} from '@sentry/core';
2+
import { applySdkMetadata, logger, setTag } from '@sentry/core';
23
import type { NodeClient, NodeOptions } from '@sentry/node';
3-
import { init as initNodeSdk } from '@sentry/node';
4+
import { getDefaultIntegrations as getNodeDefaultIntegrations,init as initNodeSdk} from '@sentry/node';
45
import { DEBUG_BUILD } from '../common/debug-build';
6+
import { lowQualityTransactionsFilterIntegration } from './lowQualityTransactionsFilterIntegration';
7+
8+
function getDefaultIntegrations(options: NodeOptions): Integration[] {
9+
return [
10+
... getNodeDefaultIntegrations(options),
11+
lowQualityTransactionsFilterIntegration(options),
12+
];
13+
}
514

615
/**
716
* Initializes the server side of the React Router SDK
817
*/
918
export function init(options: NodeOptions): NodeClient | undefined {
1019
const opts = {
1120
...options,
21+
defaultIntegrations: getDefaultIntegrations(options),
1222
};
1323

1424
DEBUG_BUILD && logger.log('Initializing SDK...');
@@ -21,33 +31,5 @@ export function init(options: NodeOptions): NodeClient | undefined {
2131

2232
DEBUG_BUILD && logger.log('SDK successfully initialized');
2333

24-
getGlobalScope().addEventProcessor(lowQualityTransactionsFilter(options));
25-
2634
return client;
2735
}
28-
29-
const matchedRegexes = [/GET \/node_modules\//, /GET \/favicon\.ico/, /GET \/@id\//];
30-
31-
/**
32-
* Filters out noisy transactions such as requests to node_modules, favicon.ico, @id/
33-
*
34-
* @param options The NodeOptions passed to the SDK
35-
* @returns An EventProcessor that filters low-quality transactions
36-
*/
37-
export function lowQualityTransactionsFilter(options: NodeOptions): EventProcessor {
38-
return Object.assign(
39-
(event => {
40-
if (event.type !== 'transaction' || !event.transaction) {
41-
return event;
42-
}
43-
44-
if (matchedRegexes.some(regex => event.transaction?.match(regex))) {
45-
options.debug && logger.log('[ReactRouter] Filtered node_modules transaction:', event.transaction);
46-
return null;
47-
}
48-
49-
return event;
50-
}) satisfies EventProcessor,
51-
{ id: 'ReactRouterLowQualityTransactionsFilter' },
52-
);
53-
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { Event, EventType, Integration } from '@sentry/core';
2+
import * as SentryCore from '@sentry/core';
3+
import * as SentryNode from '@sentry/node';
4+
import { afterEach, describe, expect, it, vi } from 'vitest';
5+
import { lowQualityTransactionsFilterIntegration } from '../../src/server/lowQualityTransactionsFilterIntegration';
6+
7+
const loggerLog = vi.spyOn(SentryCore.logger, 'log').mockImplementation(() => {});
8+
9+
describe('Low Quality Transactions Filter Integration', () => {
10+
afterEach(() => {
11+
vi.clearAllMocks();
12+
SentryNode.getGlobalScope().clear();
13+
});
14+
15+
describe('integration functionality', () => {
16+
describe('filters out low quality transactions', () => {
17+
it.each([
18+
['node_modules requests', 'GET /node_modules/some-package/index.js'],
19+
['favicon.ico requests', 'GET /favicon.ico'],
20+
['@id/ requests', 'GET /@id/some-id']
21+
])('%s', (description, transaction) => {
22+
const integration = lowQualityTransactionsFilterIntegration({ debug: true }) as Integration;
23+
const event = {
24+
type: 'transaction' as EventType,
25+
transaction,
26+
} as Event;
27+
28+
const result = integration.processEvent!(event, {}, {} as SentryCore.Client);
29+
30+
expect(result).toBeNull();
31+
32+
expect(loggerLog).toHaveBeenCalledWith(
33+
'[ReactRouter] Filtered node_modules transaction:',
34+
transaction
35+
);
36+
});
37+
});
38+
39+
describe('allows high quality transactions', () => {
40+
it.each([
41+
['normal page requests', 'GET /api/users'],
42+
['API endpoints', 'POST /data'],
43+
['app routes', 'GET /projects/123']
44+
])('%s', (description, transaction) => {
45+
const integration = lowQualityTransactionsFilterIntegration({}) as Integration;
46+
const event = {
47+
type: 'transaction' as EventType,
48+
transaction,
49+
} as Event;
50+
51+
const result = integration.processEvent!(event, {}, {} as SentryCore.Client);
52+
53+
expect(result).toEqual(event);
54+
});
55+
});
56+
57+
it('does not affect non-transaction events', () => {
58+
const integration = lowQualityTransactionsFilterIntegration({}) as Integration;
59+
const event = {
60+
type: 'error' as EventType,
61+
transaction: 'GET /node_modules/some-package/index.js',
62+
} as Event;
63+
64+
const result = integration.processEvent!(event, {}, {} as SentryCore.Client);
65+
66+
expect(result).toEqual(event);
67+
});
68+
});
69+
});
Lines changed: 16 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import type { Event, EventType } from '@sentry/core';
2-
import { getGlobalScope } from '@sentry/core';
1+
import type { Integration } from '@sentry/core';
32
import type { NodeClient } from '@sentry/node';
43
import * as SentryNode from '@sentry/node';
54
import { SDK_VERSION } from '@sentry/node';
6-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7-
import { init as reactRouterInit, lowQualityTransactionsFilter } from '../../src/server/sdk';
5+
import { afterEach, describe, expect, it, vi } from 'vitest';
6+
import * as LowQualityModule from '../../src/server/lowQualityTransactionsFilterIntegration';
7+
import { init as reactRouterInit } from '../../src/server/sdk';
88

99
const nodeInit = vi.spyOn(SentryNode, 'init');
1010

@@ -48,91 +48,28 @@ describe('React Router server SDK', () => {
4848
expect(client).not.toBeUndefined();
4949
});
5050

51-
it('registers the low quality transactions filter', async () => {
52-
const addEventProcessor = vi.spyOn(getGlobalScope(), 'addEventProcessor');
53-
addEventProcessor.mockClear();
51+
it('adds the low quality transactions filter integration by default', () => {
52+
const filterSpy = vi.spyOn(LowQualityModule, 'lowQualityTransactionsFilterIntegration');
5453

5554
reactRouterInit({
5655
dsn: 'https://public@dsn.ingest.sentry.io/1337',
57-
}) as NodeClient;
58-
59-
expect(addEventProcessor).toHaveBeenCalledTimes(1);
60-
const processor = addEventProcessor.mock.calls[0]![0];
61-
expect(processor?.id).toEqual('ReactRouterLowQualityTransactionsFilter');
62-
});
63-
64-
describe('transaction filtering', () => {
65-
const beforeSendEvent = vi.fn(event => event);
66-
let client: NodeClient;
67-
68-
beforeEach(() => {
69-
vi.clearAllMocks();
70-
beforeSendEvent.mockClear();
71-
SentryNode.getGlobalScope().clear();
72-
73-
client = reactRouterInit({
74-
dsn: 'https://public@dsn.ingest.sentry.io/1337',
75-
}) as NodeClient;
76-
77-
client.on('beforeSendEvent', beforeSendEvent);
7856
});
7957

80-
describe('filters out low quality transactions', () => {
81-
it.each(['GET /node_modules/react/index.js', 'GET /favicon.ico', 'GET /@id/package'])(
82-
'%s',
83-
async transaction => {
84-
client.captureEvent({ type: 'transaction', transaction });
85-
86-
await client.flush();
87-
88-
expect(beforeSendEvent).not.toHaveBeenCalled();
89-
},
90-
);
91-
});
58+
expect(filterSpy).toHaveBeenCalled();
9259

93-
describe('allows high quality transactions', () => {
94-
it.each(['GET /', 'GET /users', 'POST /api/data', 'GET /projects/123'])('%s', async transaction => {
95-
client.captureEvent({ type: 'transaction', transaction });
96-
97-
await client.flush();
98-
99-
expect(beforeSendEvent).toHaveBeenCalledWith(expect.objectContaining({ transaction }), expect.any(Object));
100-
});
101-
});
102-
});
103-
});
60+
expect(nodeInit).toHaveBeenCalledTimes(1);
61+
const initOptions = nodeInit.mock.calls[0]?.[0];
10462

105-
describe('lowQualityTransactionsFilter', () => {
106-
describe('filters out low quality transactions', () => {
107-
it.each([
108-
['node_modules request', 'GET /node_modules/react/index.js'],
109-
['favicon.ico request', 'GET /favicon.ico'],
110-
['@id request', 'GET /@id/package'],
111-
])('%s', (description, transaction) => {
112-
const filter = lowQualityTransactionsFilter({});
113-
const event = {
114-
type: 'transaction' as EventType,
115-
transaction,
116-
} as Event;
63+
expect(initOptions).toBeDefined();
11764

118-
expect(filter(event, {})).toBeNull();
119-
});
120-
});
65+
const defaultIntegrations = initOptions?.defaultIntegrations as Integration[];
66+
expect(Array.isArray(defaultIntegrations)).toBe(true);
12167

122-
describe('does not filter good transactions', () => {
123-
it.each([
124-
['normal page request', 'GET /users'],
125-
['API request', 'POST /api/users'],
126-
['app route', 'GET /projects/123'],
127-
])('%s', (description, transaction) => {
128-
const filter = lowQualityTransactionsFilter({});
129-
const event = {
130-
type: 'transaction' as EventType,
131-
transaction,
132-
} as Event;
68+
const filterIntegration = defaultIntegrations.find(
69+
integration => integration.name === 'LowQualityTransactionsFilter'
70+
);
13371

134-
expect(filter(event, {})).toBe(event);
135-
});
72+
expect(filterIntegration).toBeDefined();
13673
});
13774
});
13875
});

0 commit comments

Comments
 (0)