Skip to content

feat(node): Add ignoreStaticAssets #17370

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
merged 4 commits into from
Aug 11, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
const Sentry = require('@sentry/node');

Sentry.init({
dsn: 'https://[email protected]/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
// Test default for ignoreStaticAssets: true
integrations: [Sentry.httpIntegration()],
});

const express = require('express');
const cors = require('cors');
const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');

const app = express();

app.use(cors());

app.get('/test', (_req, res) => {
res.send({ response: 'ok' });
});

app.get('/favicon.ico', (_req, res) => {
res.type('image/x-icon').send(Buffer.from([0]));
});

app.get('/robots.txt', (_req, res) => {
res.type('text/plain').send('User-agent: *\nDisallow:\n');
});

app.get('/assets/app.js', (_req, res) => {
res.type('application/javascript').send('/* js */');
});

Sentry.setupExpressErrorHandler(app);

startExpressServerAndSendPortToRunner(app);
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
const Sentry = require('@sentry/node');

Sentry.init({
dsn: 'https://[email protected]/1337',
release: '1.0',
tracesSampleRate: 1.0,
transport: loggingTransport,
integrations: [Sentry.httpIntegration({ ignoreStaticAssets: false })],
});

const express = require('express');
const cors = require('cors');
const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');

const app = express();

app.use(cors());

app.get('/test', (_req, res) => {
res.send({ response: 'ok' });
});

app.get('/favicon.ico', (_req, res) => {
res.type('image/x-icon').send(Buffer.from([0]));
});

app.get('/robots.txt', (_req, res) => {
res.type('text/plain').send('User-agent: *\nDisallow:\n');
});

app.get('/assets/app.js', (_req, res) => {
res.type('application/javascript').send('/* js */');
});

Sentry.setupExpressErrorHandler(app);

startExpressServerAndSendPortToRunner(app);
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,44 @@ describe('httpIntegration', () => {
closeTestServer();
});
});

test('ignores static asset requests by default', async () => {
const runner = createRunner(__dirname, 'server-ignoreStaticAssets.js')
.expect({
transaction: event => {
expect(event.transaction).toBe('GET /test');
expect(event.contexts?.trace?.data?.url).toMatch(/\/test$/);
expect(event.contexts?.trace?.op).toBe('http.server');
expect(event.contexts?.trace?.status).toBe('ok');
},
})
.start();

// These should be ignored by default
await runner.makeRequest('get', '/favicon.ico');
await runner.makeRequest('get', '/robots.txt');
await runner.makeRequest('get', '/assets/app.js');

// This one should be traced
await runner.makeRequest('get', '/test');

await runner.completed();
});

test('traces static asset requests when ignoreStaticAssets is false', async () => {
const runner = createRunner(__dirname, 'server-traceStaticAssets.js')
.expect({
transaction: event => {
expect(event.transaction).toBe('GET /favicon.ico');
expect(event.contexts?.trace?.data?.url).toMatch(/\/favicon.ico$/);
expect(event.contexts?.trace?.op).toBe('http.server');
expect(event.contexts?.trace?.status).toBe('ok');
},
})
.start();

await runner.makeRequest('get', '/favicon.ico');

await runner.completed();
});
});
36 changes: 36 additions & 0 deletions packages/node/src/integrations/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ interface HttpOptions {
*/
ignoreIncomingRequests?: (urlPath: string, request: IncomingMessage) => boolean;

/**
* Whether to automatically ignore common static asset requests like favicon.ico, robots.txt, etc.
* This helps reduce noise in your transactions.
*
* @default `true`
*/
ignoreStaticAssets?: boolean;

/**
* Do not capture spans for incoming HTTP requests with the given status codes.
* By default, spans with 404 status code are ignored.
Expand Down Expand Up @@ -284,6 +292,11 @@ function getConfigWithDefaults(options: Partial<HttpOptions> = {}): HttpInstrume
return true;
}

// Default static asset filtering
if (options.ignoreStaticAssets !== false && method === 'GET' && urlPath && isStaticAssetRequest(urlPath)) {
return true;
}

const _ignoreIncomingRequests = options.ignoreIncomingRequests;
if (urlPath && _ignoreIncomingRequests?.(urlPath, request)) {
return true;
Expand Down Expand Up @@ -316,3 +329,26 @@ function getConfigWithDefaults(options: Partial<HttpOptions> = {}): HttpInstrume

return instrumentationConfig;
}

/**
* Check if a request is for a common static asset that should be ignored by default.
*
* Only exported for tests.
*/
export function isStaticAssetRequest(urlPath: string): boolean {
if (urlPath === '/favicon.ico' || urlPath.startsWith('/favicon')) {
return true;
}

// Common static file extensions
if (urlPath.match(/\.(ico|png|jpg|jpeg|gif|svg|css|js|woff|woff2|ttf|eot|webp|avif)$/)) {
return true;
}

// Common metadata files
if (urlPath.match(/^\/(robots\.txt|sitemap\.xml|manifest\.json|browserconfig\.xml)$/)) {
return true;
}

return false;
}
28 changes: 27 additions & 1 deletion packages/node/test/integrations/http.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { _shouldInstrumentSpans } from '../../src/integrations/http';
import { _shouldInstrumentSpans, isStaticAssetRequest } from '../../src/integrations/http';
import { conditionalTest } from '../helpers/conditional';

describe('httpIntegration', () => {
Expand Down Expand Up @@ -27,4 +27,30 @@ describe('httpIntegration', () => {
expect(actual).toBe(true);
});
});

describe('isStaticAssetRequest', () => {
it.each([
['/favicon.ico', true],
['/apple-touch-icon.png', true],
['/static/style.css', true],
['/assets/app.js', true],
['/fonts/font.woff2', true],
['/images/logo.svg', true],
['/img/photo.jpeg', true],
['/img/photo.jpg', true],
['/img/photo.webp', true],
['/font/font.ttf', true],
['/robots.txt', true],
['/sitemap.xml', true],
['/manifest.json', true],
['/browserconfig.xml', true],
// non-static routes
['/api/users', false],
['/users', false],
['/graphql', false],
['/', false],
])('returns %s -> %s', (urlPath, expected) => {
expect(isStaticAssetRequest(urlPath)).toBe(expected);
});
});
});