Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
13 changes: 13 additions & 0 deletions config/rsbuild/rsbuild.common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import {fileURLToPath} from 'url';

import type Environment from './types.ts';

// Relative on purpose: module aliases are not resolved when this config is evaluated.
// @ts-expect-error -- Can't use .ts extensions without allowImportingTsExtensions in tsconfig
import SENTRY_APPLICATION_KEY from '../../src/libs/telemetry/sentryApplicationKey.ts'; // eslint-disable-line @dword-design/import-alias/prefer-alias
// @ts-expect-error -- Can't use .ts extensions without allowImportingTsExtensions in tsconfig
import CustomVersionFilePlugin from './CustomVersionFilePlugin.ts';
// @ts-expect-error -- Can't use .ts extensions without allowImportingTsExtensions in tsconfig
Expand Down Expand Up @@ -330,6 +333,13 @@ const getCommonConfiguration = async ({file = '.env', platform = 'web', isDevSer
...shared,
source: {
...shared.source,
define: {
...shared.source?.define,
// Did `@sentry/webpack-plugin` stamp `applicationKey` into the chunks? Gates
// `thirdPartyErrorFilterIntegration` in `src/libs/telemetry/integrations/index.web.ts`,
// which can only classify frames when it did.
__SENTRY_APPLICATION_KEY_STAMPED__: !!sentryWebpackPlugin,
},
entry: {main: './index.js'},
},
output: {
Expand Down Expand Up @@ -553,6 +563,9 @@ const getCommonConfiguration = async ({file = '.env', platform = 'web', isDevSer
assets: './dist/**/*.{js,map}',
filesToDeleteAfterUpload: './dist/**/*.map',
},
// Stamps every chunk so the SDK can tell our frames from injected ones at runtime.
// Reported to the app as `__SENTRY_APPLICATION_KEY_STAMPED__` (see `source.define`).
applicationKey: SENTRY_APPLICATION_KEY,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the build-time half of the guard pair (see my note on index.web.ts:34). The key is stamped whenever the plugin runs — gated on isDevelopment at line 320 — while the runtime integration is gated separately on isDevelopment().

They agree for all normal builds, but the two conditions are independent. A one-line comment here pointing at the runtime guard (and vice versa) would prevent a future edit to either from silently mislabeling every app error as third_party_code.

debug: false,
telemetry: false,
}),
Expand Down
6 changes: 5 additions & 1 deletion src/libs/telemetry/integrations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ const tracingIntegration = Sentry.reactNativeTracingIntegration({
// the web/native export shape in parity; it is filtered out of the integrations list on native.
const reportingObserverIntegration = undefined;

export {navigationIntegration, tracingIntegration, browserProfilingIntegration, breadcrumbsIntegration, consoleIntegration, reportingObserverIntegration};
// Only the web bundle is stamped with an application key by `@sentry/webpack-plugin`, so on native every
// frame would look foreign. Stub for export shape parity; filtered out of the integrations list here.
const thirdPartyErrorFilterIntegration = undefined;

export {navigationIntegration, tracingIntegration, browserProfilingIntegration, breadcrumbsIntegration, consoleIntegration, reportingObserverIntegration, thirdPartyErrorFilterIntegration};
28 changes: 27 additions & 1 deletion src/libs/telemetry/integrations/index.web.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import SENTRY_APPLICATION_KEY from '@libs/telemetry/sentryApplicationKey';

import * as SentryReact from '@sentry/react';

import {breadcrumbsIntegration, browserProfilingIntegration, consoleIntegration, navigationIntegration, shouldCreateSpanForRequest} from './common';

/**
* `typeof` guard rather than a bare read: the define is absent from bundles that do not go through
* `getCommonConfiguration` (Storybook, Jest), where reading the identifier directly would throw.
*/
function isApplicationKeyStamped(): boolean {
return typeof __SENTRY_APPLICATION_KEY_STAMPED__ !== 'undefined' && __SENTRY_APPLICATION_KEY_STAMPED__;
}

/**
* Browser tracing integration is enabled on Web to support web health measurements
* such as INP, LCP, FCP, CLS.
Expand All @@ -19,4 +29,20 @@ const reportingObserverIntegration = SentryReact.reportingObserverIntegration({
types: ['crash', 'intervention'],
});

export {navigationIntegration, tracingIntegration, browserProfilingIntegration, breadcrumbsIntegration, consoleIntegration, reportingObserverIntegration};
/**
* Tags errors whose stack holds no frame from our own bundle with `third_party_code: true`, so noise
* thrown by injected code (consent tools, tag managers, browser extensions) can be told apart from our
* own errors in issue search (GH #93837).
*
* Our bundle is recognized by the application key `@sentry/webpack-plugin` embeds in every chunk.
* Without a stamped key every frame looks foreign and *all* our errors get mislabeled, so the guard
* reads the same variable that gates the plugin (`config/rsbuild/rsbuild.common.ts`).
*/
const thirdPartyErrorFilterIntegration = isApplicationKeyStamped()
? SentryReact.thirdPartyErrorFilterIntegration({
filterKeys: [SENTRY_APPLICATION_KEY],
behaviour: 'apply-tag-if-exclusively-contains-third-party-frames',
})
: undefined;

export {navigationIntegration, tracingIntegration, browserProfilingIntegration, breadcrumbsIntegration, consoleIntegration, reportingObserverIntegration, thirdPartyErrorFilterIntegration};
12 changes: 12 additions & 0 deletions src/libs/telemetry/sentryApplicationKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Key that `@sentry/webpack-plugin` stamps into every web chunk at build time, and that
* `thirdPartyErrorFilterIntegration` matches frames against at runtime to tell our own code from
* injected code (consent tools, tag managers, browser extensions).
*
* Both halves live in different worlds (bundler config and app source) and can only agree by literal
* string, so they share this const. Keep this module free of imports: it is loaded by
* `config/rsbuild/rsbuild.common.ts`, where module aliases are not resolved yet.
*/
const SENTRY_APPLICATION_KEY = 'expensify-app';

export default SENTRY_APPLICATION_KEY;
22 changes: 18 additions & 4 deletions src/setup/telemetry/setupSentry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import {isDevelopment} from '@libs/Environment/Environment';
import {breadcrumbsIntegration, browserProfilingIntegration, consoleIntegration, navigationIntegration, reportingObserverIntegration, tracingIntegration} from '@libs/telemetry/integrations';
import {
breadcrumbsIntegration,
browserProfilingIntegration,
consoleIntegration,
navigationIntegration,
reportingObserverIntegration,
thirdPartyErrorFilterIntegration,
tracingIntegration,
} from '@libs/telemetry/integrations';
import {processBeforeSendLogs, processBeforeSendTransactions} from '@libs/telemetry/middlewares';

import CONFIG from '@src/CONFIG';
Expand All @@ -11,9 +19,15 @@ import pkg from '../../../package.json';
import makeDebugTransport from './debugTransport';

function setupSentry(): void {
const integrations = [navigationIntegration, tracingIntegration, browserProfilingIntegration, breadcrumbsIntegration, consoleIntegration, reportingObserverIntegration].filter(
(integration): integration is NonNullable<typeof integration> => integration !== undefined,
);
const integrations = [
navigationIntegration,
tracingIntegration,
browserProfilingIntegration,
breadcrumbsIntegration,
consoleIntegration,
reportingObserverIntegration,
thirdPartyErrorFilterIntegration,
].filter((integration): integration is NonNullable<typeof integration> => integration !== undefined);

Sentry.init({
dsn: CONFIG.SENTRY_DSN,
Expand Down
6 changes: 6 additions & 0 deletions src/types/modules/window.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ declare global {
// Injected by Rspack's DefinePlugin at build time; empty string in non-development builds.
// eslint-disable-next-line @typescript-eslint/naming-convention, no-underscore-dangle
const __GIT_BRANCH__: string;

// Injected by Rspack's DefinePlugin at build time: whether `@sentry/webpack-plugin` stamped its
// `applicationKey` into the chunks. Absent (hence optional) in bundles built without the app's
// Rsbuild defines, such as Storybook.
// eslint-disable-next-line @typescript-eslint/naming-convention, no-underscore-dangle
const __SENTRY_APPLICATION_KEY_STAMPED__: boolean | undefined;
}

export {};
Loading