From ecc6587b7e58fb9c699996885a4db17a2b83b711 Mon Sep 17 00:00:00 2001 From: "Rodrigo Lino da Costa (via MelvinBot)" Date: Thu, 17 Sep 2026 20:14:54 +0000 Subject: [PATCH 1/4] Filter high-volume Sentry spans: browser resource spans, Google Ads/Analytics requests, and the Ping heartbeat Co-authored-by: Rodrigo Lino da Costa --- cspell.json | 2 ++ src/libs/telemetry/integrations/common.ts | 18 +++++++++- src/libs/telemetry/integrations/index.web.ts | 5 +++ tests/unit/shouldCreateSpanForRequestTest.ts | 35 ++++++++++++++++++++ 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/unit/shouldCreateSpanForRequestTest.ts diff --git a/cspell.json b/cspell.json index 45bf825e039c..c4cc037494c0 100644 --- a/cspell.json +++ b/cspell.json @@ -873,6 +873,7 @@ "oxfmt", "oxlint", "padangle", + "pagead", "parasharrajat", "passcodes", "passplus", @@ -955,6 +956,7 @@ "RNFS", "RNLinksdk", "rnmapbox", + "rmkt", "rock", "rowcount", "rowindex", diff --git a/src/libs/telemetry/integrations/common.ts b/src/libs/telemetry/integrations/common.ts index fc5d1c7cf1f5..c9a0f04be72d 100644 --- a/src/libs/telemetry/integrations/common.ts +++ b/src/libs/telemetry/integrations/common.ts @@ -2,7 +2,23 @@ import * as SentryReact from '@sentry/react'; import * as Sentry from '@sentry/react-native'; const shouldCreateSpanForRequest = (url: string): boolean => { - const filteredPhrases = ['/api/Log', 'firebaselogging-pa.googleapis.com', 'analytics.google.com', 'rs.fullstory.com', 'api.github.com', 'group-ib.com', 'fp-api.expensify.com']; + const filteredPhrases = [ + '/api/Log', + 'firebaselogging-pa.googleapis.com', + 'analytics.google.com', + 'rs.fullstory.com', + 'api.github.com', + 'group-ib.com', + 'fp-api.expensify.com', + // NetInfo polls this on a timer for every client, so it is heartbeat volume with nothing to debug in a span (GH #101449). + '/api/Ping', + // Google Ads/Analytics conversion endpoints. They are third-party requests we cannot act on, and they were + // one of our largest sources of span volume (GH #101449). + 'ccm/collect', + 'rmkt/collect', + 'pagead/form-data', + 'ccm/form-data', + ]; return !filteredPhrases.some((phrase) => url.includes(phrase)); }; diff --git a/src/libs/telemetry/integrations/index.web.ts b/src/libs/telemetry/integrations/index.web.ts index 5e30f0a3969e..27227ca594d7 100644 --- a/src/libs/telemetry/integrations/index.web.ts +++ b/src/libs/telemetry/integrations/index.web.ts @@ -16,9 +16,14 @@ function isApplicationKeyStamped(): boolean { /** * Browser tracing integration is enabled on Web to support web health measurements * such as INP, LCP, FCP, CLS. + * + * `ignoreResourceSpans` stops the SDK from creating a span per stylesheet/font (`resource.link`) and per script + * (`resource.script`) on every pageload and navigation - together our two highest-volume span types (GH #101449). + * Web vitals are unaffected: they are measurements on the pageload span rather than resource spans. */ const tracingIntegration = SentryReact.browserTracingIntegration({ shouldCreateSpanForRequest, + ignoreResourceSpans: ['resource.link', 'resource.script'], }); /** diff --git a/tests/unit/shouldCreateSpanForRequestTest.ts b/tests/unit/shouldCreateSpanForRequestTest.ts new file mode 100644 index 000000000000..ed0a3c884c5a --- /dev/null +++ b/tests/unit/shouldCreateSpanForRequestTest.ts @@ -0,0 +1,35 @@ +import {shouldCreateSpanForRequest} from '@libs/telemetry/integrations/common'; + +// The module builds integrations at import time, so the SDK factories are stubbed out. +jest.mock('@sentry/react-native', () => ({ + reactNavigationIntegration: jest.fn(), + breadcrumbsIntegration: jest.fn(), + consoleLoggingIntegration: jest.fn(), +})); + +jest.mock('@sentry/react', () => ({ + browserProfilingIntegration: jest.fn(), +})); + +describe('shouldCreateSpanForRequest', () => { + it('creates a span for the API commands we debug with', () => { + expect(shouldCreateSpanForRequest('https://www.expensify.com/api/OpenReport?')).toBe(true); + }); + + it('drops the NetInfo reachability heartbeat, which every client issues on a timer', () => { + expect(shouldCreateSpanForRequest('https://www.expensify.com/api/Ping?accountID=123')).toBe(false); + }); + + it('drops the logging command, so writing logs cannot generate more telemetry', () => { + expect(shouldCreateSpanForRequest('https://www.expensify.com/api/Log?')).toBe(false); + }); + + it.each([ + ['ccm/collect', 'https://www.google.com/ccm/collect?tid=G-12345&en=page_view'], + ['rmkt/collect', 'https://www.google.com/rmkt/collect?tid=12345'], + ['pagead/form-data', 'https://googleads.g.doubleclick.net/pagead/form-data?id=12345'], + ['ccm/form-data', 'https://www.google.com/ccm/form-data?id=12345'], + ])('drops the third-party Google Ads/Analytics request to %s', (_endpoint, url) => { + expect(shouldCreateSpanForRequest(url)).toBe(false); + }); +}); From 8ef77754cca60bf176723b39765917898abbafcd Mon Sep 17 00:00:00 2001 From: "Rodrigo Lino da Costa (via MelvinBot)" Date: Thu, 17 Sep 2026 20:20:53 +0000 Subject: [PATCH 2/4] Add a test that the web tracing integration ignores resource.link and resource.script spans Co-authored-by: Rodrigo Lino da Costa --- tests/unit/webTracingIntegrationTest.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/unit/webTracingIntegrationTest.ts diff --git a/tests/unit/webTracingIntegrationTest.ts b/tests/unit/webTracingIntegrationTest.ts new file mode 100644 index 000000000000..51245726e5c9 --- /dev/null +++ b/tests/unit/webTracingIntegrationTest.ts @@ -0,0 +1,24 @@ +// The module builds integrations at import time, so the SDK factories are stubbed out. +jest.mock('@sentry/react', () => ({ + browserTracingIntegration: jest.fn(() => ({name: 'BrowserTracing'})), + reportingObserverIntegration: jest.fn(() => ({name: 'ReportingObserver'})), + thirdPartyErrorFilterIntegration: jest.fn(() => ({name: 'ThirdPartyErrorFilter'})), + browserProfilingIntegration: jest.fn(() => ({name: 'BrowserProfiling'})), +})); + +jest.mock('@sentry/react-native', () => ({ + reactNavigationIntegration: jest.fn(), + breadcrumbsIntegration: jest.fn(), + consoleLoggingIntegration: jest.fn(), +})); + +describe('web tracing integration', () => { + it('tells the browser SDK not to create resource spans for stylesheets/fonts and scripts', async () => { + // Given the web integrations module, which configures browserTracingIntegration at import time + const {browserTracingIntegration} = await import('@sentry/react'); + await import('@libs/telemetry/integrations/index.web'); + + // Then the two highest-volume resource span ops are never created + expect(browserTracingIntegration).toHaveBeenCalledWith(expect.objectContaining({ignoreResourceSpans: ['resource.link', 'resource.script']})); + }); +}); From 5ca763562ae3c4765d97ec8ec1529c113d783560 Mon Sep 17 00:00:00 2001 From: "Rodrigo Lino da Costa (via MelvinBot)" Date: Thu, 17 Sep 2026 20:58:21 +0000 Subject: [PATCH 3/4] Hold the browserTracingIntegration mock in a variable instead of importing @sentry/react @sentry/react is a transitive dependency of @sentry/react-native, so knip reports it as unlisted when a file imports it by name. The dynamic import in the test added a new knip finding; reading the mock from a hoisted variable keeps the assertion and drops the import. Co-authored-by: Rodrigo Lino da Costa --- tests/unit/webTracingIntegrationTest.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/unit/webTracingIntegrationTest.ts b/tests/unit/webTracingIntegrationTest.ts index 51245726e5c9..fe04cf90d4de 100644 --- a/tests/unit/webTracingIntegrationTest.ts +++ b/tests/unit/webTracingIntegrationTest.ts @@ -1,6 +1,10 @@ +// Held outside the factory so the assertion can read the calls without importing `@sentry/react`, +// which is a transitive dependency that knip flags when imported by name. +const mockBrowserTracingIntegration = jest.fn(() => ({name: 'BrowserTracing'})); + // The module builds integrations at import time, so the SDK factories are stubbed out. jest.mock('@sentry/react', () => ({ - browserTracingIntegration: jest.fn(() => ({name: 'BrowserTracing'})), + browserTracingIntegration: mockBrowserTracingIntegration, reportingObserverIntegration: jest.fn(() => ({name: 'ReportingObserver'})), thirdPartyErrorFilterIntegration: jest.fn(() => ({name: 'ThirdPartyErrorFilter'})), browserProfilingIntegration: jest.fn(() => ({name: 'BrowserProfiling'})), @@ -15,10 +19,9 @@ jest.mock('@sentry/react-native', () => ({ describe('web tracing integration', () => { it('tells the browser SDK not to create resource spans for stylesheets/fonts and scripts', async () => { // Given the web integrations module, which configures browserTracingIntegration at import time - const {browserTracingIntegration} = await import('@sentry/react'); await import('@libs/telemetry/integrations/index.web'); // Then the two highest-volume resource span ops are never created - expect(browserTracingIntegration).toHaveBeenCalledWith(expect.objectContaining({ignoreResourceSpans: ['resource.link', 'resource.script']})); + expect(mockBrowserTracingIntegration).toHaveBeenCalledWith(expect.objectContaining({ignoreResourceSpans: ['resource.link', 'resource.script']})); }); }); From 23db03c6cced9dfa96ab1038470ebaccd4163e34 Mon Sep 17 00:00:00 2001 From: "Rodrigo Lino da Costa (via MelvinBot)" Date: Fri, 18 Sep 2026 12:01:30 +0000 Subject: [PATCH 4/4] Trim telemetry span-filter comments per review feedback Co-authored-by: Rodrigo Lino da Costa --- src/libs/telemetry/integrations/common.ts | 3 --- src/libs/telemetry/integrations/index.web.ts | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/libs/telemetry/integrations/common.ts b/src/libs/telemetry/integrations/common.ts index c9a0f04be72d..f2d42ed49822 100644 --- a/src/libs/telemetry/integrations/common.ts +++ b/src/libs/telemetry/integrations/common.ts @@ -10,10 +10,7 @@ const shouldCreateSpanForRequest = (url: string): boolean => { 'api.github.com', 'group-ib.com', 'fp-api.expensify.com', - // NetInfo polls this on a timer for every client, so it is heartbeat volume with nothing to debug in a span (GH #101449). '/api/Ping', - // Google Ads/Analytics conversion endpoints. They are third-party requests we cannot act on, and they were - // one of our largest sources of span volume (GH #101449). 'ccm/collect', 'rmkt/collect', 'pagead/form-data', diff --git a/src/libs/telemetry/integrations/index.web.ts b/src/libs/telemetry/integrations/index.web.ts index 27227ca594d7..6c7c773553e2 100644 --- a/src/libs/telemetry/integrations/index.web.ts +++ b/src/libs/telemetry/integrations/index.web.ts @@ -18,8 +18,7 @@ function isApplicationKeyStamped(): boolean { * such as INP, LCP, FCP, CLS. * * `ignoreResourceSpans` stops the SDK from creating a span per stylesheet/font (`resource.link`) and per script - * (`resource.script`) on every pageload and navigation - together our two highest-volume span types (GH #101449). - * Web vitals are unaffected: they are measurements on the pageload span rather than resource spans. + * (`resource.script`) on every pageload and navigation */ const tracingIntegration = SentryReact.browserTracingIntegration({ shouldCreateSpanForRequest,