Skip to content

Commit 64d1f6a

Browse files
authored
feat(node): Add ignoreStaticAssets (#17370)
1 parent f307a22 commit 64d1f6a

File tree

5 files changed

+182
-2
lines changed

5 files changed

+182
-2
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
2+
const Sentry = require('@sentry/node');
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
// Test default for ignoreStaticAssets: true
10+
integrations: [Sentry.httpIntegration()],
11+
});
12+
13+
const express = require('express');
14+
const cors = require('cors');
15+
const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');
16+
17+
const app = express();
18+
19+
app.use(cors());
20+
21+
app.get('/test', (_req, res) => {
22+
res.send({ response: 'ok' });
23+
});
24+
25+
app.get('/favicon.ico', (_req, res) => {
26+
res.type('image/x-icon').send(Buffer.from([0]));
27+
});
28+
29+
app.get('/robots.txt', (_req, res) => {
30+
res.type('text/plain').send('User-agent: *\nDisallow:\n');
31+
});
32+
33+
app.get('/assets/app.js', (_req, res) => {
34+
res.type('application/javascript').send('/* js */');
35+
});
36+
37+
Sentry.setupExpressErrorHandler(app);
38+
39+
startExpressServerAndSendPortToRunner(app);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const { loggingTransport } = require('@sentry-internal/node-integration-tests');
2+
const Sentry = require('@sentry/node');
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
integrations: [Sentry.httpIntegration({ ignoreStaticAssets: false })],
10+
});
11+
12+
const express = require('express');
13+
const cors = require('cors');
14+
const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests');
15+
16+
const app = express();
17+
18+
app.use(cors());
19+
20+
app.get('/test', (_req, res) => {
21+
res.send({ response: 'ok' });
22+
});
23+
24+
app.get('/favicon.ico', (_req, res) => {
25+
res.type('image/x-icon').send(Buffer.from([0]));
26+
});
27+
28+
app.get('/robots.txt', (_req, res) => {
29+
res.type('text/plain').send('User-agent: *\nDisallow:\n');
30+
});
31+
32+
app.get('/assets/app.js', (_req, res) => {
33+
res.type('application/javascript').send('/* js */');
34+
});
35+
36+
Sentry.setupExpressErrorHandler(app);
37+
38+
startExpressServerAndSendPortToRunner(app);

dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,44 @@ describe('httpIntegration', () => {
185185
closeTestServer();
186186
});
187187
});
188+
189+
test('ignores static asset requests by default', async () => {
190+
const runner = createRunner(__dirname, 'server-ignoreStaticAssets.js')
191+
.expect({
192+
transaction: event => {
193+
expect(event.transaction).toBe('GET /test');
194+
expect(event.contexts?.trace?.data?.url).toMatch(/\/test$/);
195+
expect(event.contexts?.trace?.op).toBe('http.server');
196+
expect(event.contexts?.trace?.status).toBe('ok');
197+
},
198+
})
199+
.start();
200+
201+
// These should be ignored by default
202+
await runner.makeRequest('get', '/favicon.ico');
203+
await runner.makeRequest('get', '/robots.txt');
204+
await runner.makeRequest('get', '/assets/app.js');
205+
206+
// This one should be traced
207+
await runner.makeRequest('get', '/test');
208+
209+
await runner.completed();
210+
});
211+
212+
test('traces static asset requests when ignoreStaticAssets is false', async () => {
213+
const runner = createRunner(__dirname, 'server-traceStaticAssets.js')
214+
.expect({
215+
transaction: event => {
216+
expect(event.transaction).toBe('GET /favicon.ico');
217+
expect(event.contexts?.trace?.data?.url).toMatch(/\/favicon.ico$/);
218+
expect(event.contexts?.trace?.op).toBe('http.server');
219+
expect(event.contexts?.trace?.status).toBe('ok');
220+
},
221+
})
222+
.start();
223+
224+
await runner.makeRequest('get', '/favicon.ico');
225+
226+
await runner.completed();
227+
});
188228
});

packages/node/src/integrations/http.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { diag } from '@opentelemetry/api';
33
import type { HttpInstrumentationConfig } from '@opentelemetry/instrumentation-http';
44
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
55
import type { Span } from '@sentry/core';
6-
import { defineIntegration, getClient, hasSpansEnabled } from '@sentry/core';
6+
import { defineIntegration, getClient, hasSpansEnabled, stripUrlQueryAndFragment } from '@sentry/core';
77
import type { HTTPModuleRequestIncomingMessage, NodeClient } from '@sentry/node-core';
88
import {
99
type SentryHttpInstrumentationOptions,
@@ -74,6 +74,14 @@ interface HttpOptions {
7474
*/
7575
ignoreIncomingRequests?: (urlPath: string, request: IncomingMessage) => boolean;
7676

77+
/**
78+
* Whether to automatically ignore common static asset requests like favicon.ico, robots.txt, etc.
79+
* This helps reduce noise in your transactions.
80+
*
81+
* @default `true`
82+
*/
83+
ignoreStaticAssets?: boolean;
84+
7785
/**
7886
* Do not capture spans for incoming HTTP requests with the given status codes.
7987
* By default, spans with 404 status code are ignored.
@@ -284,6 +292,11 @@ function getConfigWithDefaults(options: Partial<HttpOptions> = {}): HttpInstrume
284292
return true;
285293
}
286294

295+
// Default static asset filtering
296+
if (options.ignoreStaticAssets !== false && method === 'GET' && urlPath && isStaticAssetRequest(urlPath)) {
297+
return true;
298+
}
299+
287300
const _ignoreIncomingRequests = options.ignoreIncomingRequests;
288301
if (urlPath && _ignoreIncomingRequests?.(urlPath, request)) {
289302
return true;
@@ -316,3 +329,23 @@ function getConfigWithDefaults(options: Partial<HttpOptions> = {}): HttpInstrume
316329

317330
return instrumentationConfig;
318331
}
332+
333+
/**
334+
* Check if a request is for a common static asset that should be ignored by default.
335+
*
336+
* Only exported for tests.
337+
*/
338+
export function isStaticAssetRequest(urlPath: string): boolean {
339+
const path = stripUrlQueryAndFragment(urlPath);
340+
// Common static file extensions
341+
if (path.match(/\.(ico|png|jpg|jpeg|gif|svg|css|js|woff|woff2|ttf|eot|webp|avif)$/)) {
342+
return true;
343+
}
344+
345+
// Common metadata files
346+
if (path.match(/^\/(robots\.txt|sitemap\.xml|manifest\.json|browserconfig\.xml)$/)) {
347+
return true;
348+
}
349+
350+
return false;
351+
}

packages/node/test/integrations/http.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from 'vitest';
2-
import { _shouldInstrumentSpans } from '../../src/integrations/http';
2+
import { _shouldInstrumentSpans, isStaticAssetRequest } from '../../src/integrations/http';
33
import { conditionalTest } from '../helpers/conditional';
44

55
describe('httpIntegration', () => {
@@ -27,4 +27,34 @@ describe('httpIntegration', () => {
2727
expect(actual).toBe(true);
2828
});
2929
});
30+
31+
describe('isStaticAssetRequest', () => {
32+
it.each([
33+
['/favicon.ico', true],
34+
['/apple-touch-icon.png', true],
35+
['/static/style.css', true],
36+
['/assets/app.js', true],
37+
['/fonts/font.woff2', true],
38+
['/images/logo.svg', true],
39+
['/img/photo.jpeg', true],
40+
['/img/photo.jpg', true],
41+
['/img/photo.jpg?v=123', true],
42+
['/img/photo.webp', true],
43+
['/font/font.ttf', true],
44+
['/robots.txt', true],
45+
['/sitemap.xml', true],
46+
['/manifest.json', true],
47+
['/browserconfig.xml', true],
48+
// non-static routes
49+
['/api/users', false],
50+
['/some-json.json', false],
51+
['/some-xml.xml', false],
52+
['/some-txt.txt', false],
53+
['/users', false],
54+
['/graphql', false],
55+
['/', false],
56+
])('returns %s -> %s', (urlPath, expected) => {
57+
expect(isStaticAssetRequest(urlPath)).toBe(expected);
58+
});
59+
});
3060
});

0 commit comments

Comments
 (0)