Skip to content

feat(react-router): Create low quality transactions filter for react router #16219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { type Client, type Event, type EventHint, defineIntegration, logger } from '@sentry/core';
import type { NodeOptions } from '@sentry/node';

/**
* Integration that filters out noisy http transactions such as requests to node_modules, favicon.ico, @id/
*
*/

function _lowQualityTransactionsFilterIntegration(options: NodeOptions): {
name: string;
processEvent: (event: Event, hint: EventHint, client: Client) => Event | null;
} {
const matchedRegexes = [/GET \/node_modules\//, /GET \/favicon\.ico/, /GET \/@id\//];

return {
name: 'LowQualityTransactionsFilter',

processEvent(event: Event, _hint: EventHint, _client: Client): Event | null {
if (event.type !== 'transaction' || !event.transaction) {
return event;
}

const transaction = event.transaction;

if (matchedRegexes.some(regex => transaction.match(regex))) {
options.debug && logger.log('[ReactRouter] Filtered node_modules transaction:', event.transaction);
return null;
}

return event;
},
};
}

export const lowQualityTransactionsFilterIntegration = defineIntegration((options: NodeOptions) =>
_lowQualityTransactionsFilterIntegration(options),
);
10 changes: 9 additions & 1 deletion packages/react-router/src/server/sdk.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import type { Integration } from '@sentry/core';
import { applySdkMetadata, logger, setTag } from '@sentry/core';
import type { NodeClient, NodeOptions } from '@sentry/node';
import { init as initNodeSdk } from '@sentry/node';
import { getDefaultIntegrations as getNodeDefaultIntegrations, init as initNodeSdk } from '@sentry/node';
import { DEBUG_BUILD } from '../common/debug-build';
import { lowQualityTransactionsFilterIntegration } from './lowQualityTransactionsFilterIntegration';

function getDefaultIntegrations(options: NodeOptions): Integration[] {
return [...getNodeDefaultIntegrations(options), lowQualityTransactionsFilterIntegration(options)];
}

/**
* Initializes the server side of the React Router SDK
*/
export function init(options: NodeOptions): NodeClient | undefined {
const opts = {
...options,
defaultIntegrations: getDefaultIntegrations(options),
};

DEBUG_BUILD && logger.log('Initializing SDK...');
Expand All @@ -20,5 +27,6 @@ export function init(options: NodeOptions): NodeClient | undefined {
setTag('runtime', 'node');

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

return client;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { Event, EventType, Integration } from '@sentry/core';
import * as SentryCore from '@sentry/core';
import * as SentryNode from '@sentry/node';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { lowQualityTransactionsFilterIntegration } from '../../src/server/lowQualityTransactionsFilterIntegration';

const loggerLog = vi.spyOn(SentryCore.logger, 'log').mockImplementation(() => {});

describe('Low Quality Transactions Filter Integration', () => {
afterEach(() => {
vi.clearAllMocks();
SentryNode.getGlobalScope().clear();
});

describe('integration functionality', () => {
describe('filters out low quality transactions', () => {
it.each([
['node_modules requests', 'GET /node_modules/some-package/index.js'],
['favicon.ico requests', 'GET /favicon.ico'],
['@id/ requests', 'GET /@id/some-id'],
])('%s', (description, transaction) => {
const integration = lowQualityTransactionsFilterIntegration({ debug: true }) as Integration;
const event = {
type: 'transaction' as EventType,
transaction,
} as Event;

const result = integration.processEvent!(event, {}, {} as SentryCore.Client);

expect(result).toBeNull();

expect(loggerLog).toHaveBeenCalledWith('[ReactRouter] Filtered node_modules transaction:', transaction);
});
});

describe('allows high quality transactions', () => {
it.each([
['normal page requests', 'GET /api/users'],
['API endpoints', 'POST /data'],
['app routes', 'GET /projects/123'],
])('%s', (description, transaction) => {
const integration = lowQualityTransactionsFilterIntegration({}) as Integration;
const event = {
type: 'transaction' as EventType,
transaction,
} as Event;

const result = integration.processEvent!(event, {}, {} as SentryCore.Client);

expect(result).toEqual(event);
});
});

it('does not affect non-transaction events', () => {
const integration = lowQualityTransactionsFilterIntegration({}) as Integration;
const event = {
type: 'error' as EventType,
transaction: 'GET /node_modules/some-package/index.js',
} as Event;

const result = integration.processEvent!(event, {}, {} as SentryCore.Client);

expect(result).toEqual(event);
});
});
});
32 changes: 31 additions & 1 deletion packages/react-router/test/server/sdk.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { Integration } from '@sentry/core';
import type { NodeClient } from '@sentry/node';
import * as SentryNode from '@sentry/node';
import { SDK_VERSION } from '@sentry/node';
import { afterEach, describe, expect, it, vi } from 'vitest';
import * as LowQualityModule from '../../src/server/lowQualityTransactionsFilterIntegration';
import { init as reactRouterInit } from '../../src/server/sdk';

const nodeInit = vi.spyOn(SentryNode, 'init');
Expand Down Expand Up @@ -39,7 +42,34 @@ describe('React Router server SDK', () => {
});

it('returns client from init', () => {
expect(reactRouterInit({})).not.toBeUndefined();
const client = reactRouterInit({
dsn: 'https://[email protected]/1337',
}) as NodeClient;
expect(client).not.toBeUndefined();
});

it('adds the low quality transactions filter integration by default', () => {
const filterSpy = vi.spyOn(LowQualityModule, 'lowQualityTransactionsFilterIntegration');

reactRouterInit({
dsn: 'https://[email protected]/1337',
});

expect(filterSpy).toHaveBeenCalled();

expect(nodeInit).toHaveBeenCalledTimes(1);
const initOptions = nodeInit.mock.calls[0]?.[0];

expect(initOptions).toBeDefined();

const defaultIntegrations = initOptions?.defaultIntegrations as Integration[];
expect(Array.isArray(defaultIntegrations)).toBe(true);

const filterIntegration = defaultIntegrations.find(
integration => integration.name === 'LowQualityTransactionsFilter',
);

expect(filterIntegration).toBeDefined();
});
});
});
Loading