From bfe2bb309d14ba27841bb591854bec02592c83d4 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 11 Aug 2025 10:12:50 +0200 Subject: [PATCH 1/3] ignore stuff --- .../server-ignoreStaticAssets.js | 39 ++++++++++++++++++ .../server-traceStaticAssets.js | 38 ++++++++++++++++++ .../suites/tracing/httpIntegration/test.ts | 40 +++++++++++++++++++ packages/node/src/integrations/http/index.ts | 36 +++++++++++++++++ packages/node/test/integrations/http.test.ts | 28 ++++++++++++- 5 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreStaticAssets.js create mode 100644 dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-traceStaticAssets.js diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreStaticAssets.js b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreStaticAssets.js new file mode 100644 index 000000000000..bcc47257a2f1 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-ignoreStaticAssets.js @@ -0,0 +1,39 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/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); diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-traceStaticAssets.js b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-traceStaticAssets.js new file mode 100644 index 000000000000..743d1b48e21f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/server-traceStaticAssets.js @@ -0,0 +1,38 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/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); diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts index 9682f4aa28ac..97043c998814 100644 --- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts @@ -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(); + }); }); diff --git a/packages/node/src/integrations/http/index.ts b/packages/node/src/integrations/http/index.ts index e56842be85cb..f9497da06266 100644 --- a/packages/node/src/integrations/http/index.ts +++ b/packages/node/src/integrations/http/index.ts @@ -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. @@ -284,6 +292,11 @@ function getConfigWithDefaults(options: Partial = {}): 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; @@ -316,3 +329,26 @@ function getConfigWithDefaults(options: Partial = {}): 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; +} diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index 89052a348ea4..be99a21f975a 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -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', () => { @@ -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); + }); + }); }); From 6d0557f941db01f1a06738a8654f2575c8af4193 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 11 Aug 2025 10:38:57 +0200 Subject: [PATCH 2/3] seer feedback --- packages/node/src/integrations/http.ts | 11 ++++------- packages/node/test/integrations/http.test.ts | 1 + 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 6e6877c3ef70..4f1eeeea9e9c 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -3,7 +3,7 @@ import { diag } from '@opentelemetry/api'; import type { HttpInstrumentationConfig } from '@opentelemetry/instrumentation-http'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import type { Span } from '@sentry/core'; -import { defineIntegration, getClient, hasSpansEnabled } from '@sentry/core'; +import { defineIntegration, getClient, hasSpansEnabled, stripUrlQueryAndFragment } from '@sentry/core'; import type { HTTPModuleRequestIncomingMessage, NodeClient } from '@sentry/node-core'; import { type SentryHttpInstrumentationOptions, @@ -336,17 +336,14 @@ function getConfigWithDefaults(options: Partial = {}): HttpInstrume * Only exported for tests. */ export function isStaticAssetRequest(urlPath: string): boolean { - if (urlPath === '/favicon.ico' || urlPath.startsWith('/favicon')) { - return true; - } - + const path = stripUrlQueryAndFragment(urlPath); // Common static file extensions - if (urlPath.match(/\.(ico|png|jpg|jpeg|gif|svg|css|js|woff|woff2|ttf|eot|webp|avif)$/)) { + if (path.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)$/)) { + if (path.match(/^\/(robots\.txt|sitemap\.xml|manifest\.json|browserconfig\.xml)$/)) { return true; } diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index be99a21f975a..363f298a1b4b 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -38,6 +38,7 @@ describe('httpIntegration', () => { ['/images/logo.svg', true], ['/img/photo.jpeg', true], ['/img/photo.jpg', true], + ['/img/photo.jpg?v=123', true], ['/img/photo.webp', true], ['/font/font.ttf', true], ['/robots.txt', true], From 2b56ae94acbf52f9db9db978ec225ff1dddf8410 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 11 Aug 2025 10:44:05 +0200 Subject: [PATCH 3/3] add tests --- packages/node/test/integrations/http.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index 363f298a1b4b..2a24464c801c 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -47,6 +47,9 @@ describe('httpIntegration', () => { ['/browserconfig.xml', true], // non-static routes ['/api/users', false], + ['/some-json.json', false], + ['/some-xml.xml', false], + ['/some-txt.txt', false], ['/users', false], ['/graphql', false], ['/', false],