Repository navigation
Expand file tree
/
Copy pathshouldKeepEvent.ts
More file actions
267 lines (244 loc) · 14.1 KB
/
Copy pathshouldKeepEvent.ts
File metadata and controls
267 lines (244 loc) · 14.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
import { messageHeaderLength } from './redactRelayedMessage';
/**
* A structural subset of Datadog's RUM event, narrow so it stays assignable to `beforeSend`'s
* signature. `view` is on every event type, not just errors.
*/
export interface DatadogErrorEvent {
type: string;
error?: {
message?: string;
source?: string;
stack?: string;
handling_stack?: string;
/** The thrown value's `name`; absent when a non-Error was thrown. */
type?: string;
resource?: { url?: string };
/** The `cause` chain, nearest first. Read-only to `beforeSend`, which can only drop it. */
causes?: { message?: string; stack?: string; type?: string }[];
};
view?: {
name?: string;
url?: string;
referrer?: string;
};
/** Distinct from `error.resource.url`, which belongs to a failed request. */
resource?: { url?: string };
}
/**
* Scripts we embed but do not ship or control: the Reo.dev analytics bundle
* (`src/integrations/reo/reo.ts`) and Google Tag Manager (`src/integrations/google/gtm.ts`), whose
* container injects the marketing tags. Errors thrown inside them are vendor bugs we can neither
* reproduce nor fix.
*/
const THIRD_PARTY_SCRIPT_FRAME = /^https?:\/\/(?:static\.reo\.dev|www\.googletagmanager\.com)\//;
/**
* A content script the user's own browser extension injected into the page. Matched on the
* scheme suffix rather than the known spellings (`chrome-`, `moz-`, `safari-web-`,
* `ms-browser-`, …): Studio is only ever served over http(s), so no scheme spelled
* `*-extension:` can be ours, and an enumeration would leave every unlisted one scoring as a
* Studio frame.
*/
const EXTENSION_SCHEME = String.raw`[\w-]+-extension`;
const BROWSER_EXTENSION_FRAME = new RegExp(String.raw`^${EXTENSION_SCHEME}://`);
/**
* The script URL of one stack frame. The SDK re-serializes every stack it records as
* `<Name>: <message>` followed by one ` at <func> @ <url>:<line>:<col>` line per frame
* (`toStackTraceString`, `@datadog/browser-core/cjs/tools/stackTrace/handlingStack.js`), so the
* URL is browser-independently the trailing token. Isolating it is what keeps a URL sitting in
* the message text or a function name from standing in for a frame's origin.
*/
const FRAME_URL = new RegExp(
String.raw`^\s*at\s.* @ ((?:blob:)?(?:https?|${EXTENSION_SCHEME})://\S*)\s*$`,
);
/**
* A frame positioned inside evaluated code — which V8 spells `<anonymous>` for an `eval` frame too,
* so that needs no alternative of its own. Studio never evaluates code at runtime, so such a frame
* is never ours. The line:column is required: a bare `at <anonymous>` is the browser knowing
* nothing at all, which #1646 settled is skipped rather than attributed.
*/
const EVALUATED_FRAME = /^\s*at\s.*[\s(@]<anonymous>:\d+:\d+\s*$/;
/**
* `ConsoleLogger.error`'s styling prefix (`vs/platform/log/common/log.js`), which the RUM SDK
* flattens into the message text. It is what separates something Monaco logged from the same
* value reaching `console.error` any other way — React Query's global handler logs it bare.
*/
const MONACO_CONSOLE_ERROR_PREFIX = /^%c {2}ERR\b/;
/**
* Axios spells a timeout both ways (`lib/adapters/xhr.js`), and production sends both. Only the
* numeric one is a signature; `timeout exceeded` alone is a two-word English phrase.
*/
const NUMERIC_TIMEOUT_MESSAGE = /timeout of \d+ms exceeded/i;
const BARE_TIMEOUT_MESSAGE = /\btimeout exceeded\b/i;
/** A worker or object URL nests the real origin: `blob:https://host/<uuid>`. */
const NESTED_ORIGIN_PREFIX = /^blob:/;
/**
* The Datadog browser SDK monkey-patches `fetch`/XHR, so its own bundle sits at the top
* of every stack it instruments — including stacks whose real origin is third-party.
* Treat it as instrumentation rather than as a Studio frame when attributing an error.
* `vite.config.ts` puts `installBrowserTranslationDomGuard` in this chunk for the same reason:
* every script that inserts a node runs through its `insertBefore`, so a vendor tag that throws
* while GTM injects it would otherwise carry a Studio frame (#1696).
*/
const INSTRUMENTATION_FRAME = /\/assets\/vendor-datadog-[^/\s]*\.js/;
/**
* WebKit locates an inline `<script>` at the page's own URL (`https://host/:1:14`), and the inline
* scripts that run on Studio's page are the tags GTM injects (#1762). Studio's bundles always load
* from a script file, so such a frame counts for neither side.
*/
const DOCUMENT_FRAME = /^https?:\/\/[^/\s]+\/(?:index\.html)?(?:[?#]\S*)?:\d+:\d+$/;
/**
* True when the stack carries at least one frame that is provably not ours — a third-party
* script, a browser extension, or a position inside evaluated code — and none that is, i.e. no
* Studio code is on the stack at all. Frames the browser left unattributable in any other way
* count for neither side.
*/
function originatesInThirdPartyScript(stack: string, type: string | undefined, message: string) {
// A multi-line message puts interpolated customer text on its own line, where it can be shaped
// exactly like a frame (`redactRelayedMessage`); this runs before any redaction, so skip it.
const header = messageHeaderLength(type, message, stack);
let sawForeignFrame = false;
for (const line of stack.slice(header ?? 0).split('\n')) {
const url = FRAME_URL.exec(line)?.[1]?.replace(NESTED_ORIGIN_PREFIX, '');
if (!url) {
sawForeignFrame ||= EVALUATED_FRAME.test(line);
continue;
}
if (THIRD_PARTY_SCRIPT_FRAME.test(url) || BROWSER_EXTENSION_FRAME.test(url)) {
sawForeignFrame = true;
} else if (!INSTRUMENTATION_FRAME.test(url) && !DOCUMENT_FRAME.test(url)) {
// Studio code is on the stack, so the error is ours to answer for.
return false;
}
}
return sawForeignFrame;
}
/**
* Datadog `beforeSend` predicate: returns true to keep the event, false to discard
* it (false is what tells RUM to drop the event). Wired straight into `beforeSend`.
*
* The browser RUM SDK makes an error event from a thrown exception (`source: 'source'`), a
* `console.error` call (`'console'`), `addError` (`'custom'` — `addReactError` in `main.tsx`) or a
* browser report (`'report'`). A failed request is a `resource` event, never an error:
* `ErrorSource.NETWORK` is declared but nothing emits it, and `processError`
* (`browser-rum-core/…/errorCollection`) builds no `error.resource`. So a handled request failure
* reaches this filter only as whatever logged it, mostly React Query's global handler, with nothing
* naming the endpoint. Attribute by message, name and stack; an unreachable instance is visible only
* as `@type:resource @resource.status_code:0`.
*/
export function shouldKeepEvent(event: DatadogErrorEvent) {
if (event.type !== 'error') {
return true;
}
// Type-checked, not `?? ''`: a malformed field would throw out of this filter, which the SDK
// swallows into shipping the event unfiltered.
const message = typeof event.error?.message === 'string' ? event.error.message : '';
// Aborted requests are client-initiated cancellations (navigating away, a
// component unmounting, React Query cancelling an in-flight query) — not failures.
if (/Request aborted/i.test(message)) {
return false;
}
// Belt-and-suspenders: any stray "Fabric Connect not established" breadcrumb.
if (message.includes('Fabric Connect not established')) {
return false;
}
// `Object Not Found Matching Id:N, MethodName:update, ParamCount:4` is injected by a
// browser extension, not thrown by Studio: it arrives as a bare string (so RUM records
// `type: null` and "No stack, consider using an instance of Error") and only ever from
// the handful of sessions whose user has the extension installed. In the wild a single
// such session emits hundreds of these — on 2026-07-09, 2 sessions produced ~1160 events
// in 24h, doubling total RUM error volume — so it drowns real signal in Error Tracking
// without pointing at any Studio bug. Match the stable signature and drop it globally.
if (/Object Not Found Matching Id:\d+, MethodName:/i.test(message)) {
return false;
}
// "Missing requestHandler or method: <fn>" is Monaco's per-call symptom of a language
// worker that never started: when a worker script fails to load (stale hashed chunk
// after a redeploy, offline) or the tab is out of memory, Monaco silently swaps in a
// main-thread fallback that cannot host foreign worker modules (monaco-yaml, JSON, …),
// and then EVERY language-feature call — folding, links, symbols, validation, code
// actions — rejects with this error for the rest of the session (issue #1406: 7
// sessions emitted 161 of these in 7 days). The root causes carry their own, far more
// useful signals ("Failed to fetch dynamically imported module", the DataCloneError
// worker OOM of issue #1407), and the stale-deploy trigger now self-recovers via
// `installStaleDeployReload` — this repeated per-call echo adds nothing but volume.
if (/Missing requestHandler or method: /.test(message)) {
return false;
}
// Monaco's Safari-only clipboard workaround (`installWebKitWriteTextWorkaround`) emits a
// `Canceled` + `NotAllowedError` pair for every `click` or `keydown` on the editor container,
// so an autofill agent driving it with synthetic events produces them far faster than a human
// could — two Safari sessions each burst ~90 in about a second, each the majority of that
// day's RUM errors. Neither is a Studio bug and the user sees nothing; the autofill just
// doesn't land in the editor. #1670 has the mechanism and the Monaco source it comes from.
// Both stacks carry first-party frames (Monaco is in our bundle), so the stack attribution
// below can't reach them.
const errorName = event.error?.type;
// Monaco's `isCancellationError` (`vs/base/common/errors.js`) is `name === message ===
// 'Canceled'`, which VS Code uses to mean "benign, do not report". Holding the message to
// that exact sentinel leaves a cancellation that says *what* was cancelled — and React
// Query's differently spelled `CancelledError` — reaching Error Tracking.
if (errorName === 'Canceled' && message.trim() === 'Canceled') {
return false;
}
// A `NotAllowedError` is the browser refusing a permission-gated API, and Monaco reports this
// one by logging it. Keying on its logger prefix rather than on the bare name, or on
// `source: 'console'`, is what keeps a Studio permission failure visible: React Query's global
// handler console-logs every handled rejection (`src/react-query/queryClient.ts`), so both
// looser gates would silently swallow the day someone adds passkeys or notifications.
if (errorName === 'NotAllowedError' && MONACO_CONSOLE_ERROR_PREFIX.test(message)) {
return false;
}
// Errors thrown entirely inside an embedded third-party script or an injected extension
// content script are that vendor's bugs, not Studio's: we can't reproduce them, fix them,
// or act on them, and they arrive with stacks that point only at minified foreign code.
// Reo.dev alone contributed two distinct "issues" to Error Tracking within a day of first
// appearing (2026-07-28): a `RangeError: Invalid time zone specified: Etc/Unknown` from its
// own `DateTimeFormat` call, and a `TypeError: Failed to fetch` when its beacon is blocked.
// An extension's per-session rate is unbounded: four sessions emitted 1,486 identical
// unhandled TypeErrors in four days, 87% of all RUM errors in the last of them (#1645).
// Attribute on the stack rather than the message, so a genuine Studio error that happens to
// share a message is still kept.
const stack = typeof event.error?.stack === 'string' ? event.error.stack : '';
if (stack && originatesInThirdPartyScript(stack, errorName, message)) {
return false;
}
// A request timeout is a connectivity-class failure, never a Studio bug: the instance,
// cluster, or backend was too slow to answer, and backend latency is tracked server-side.
// The bare spelling needs the name because on its own it would also discard a Harper-relayed
// `Operation timeout exceeded`; the numeric one stays name-free, so a timeout Studio re-wraps
// into a plain `Error` (`secrets.ts`) is dropped exactly as before.
if (
NUMERIC_TIMEOUT_MESSAGE.test(message)
|| (errorName === 'AxiosError' && BARE_TIMEOUT_MESSAGE.test(message))
) {
return false;
}
// A 401 Unauthorized is an expected state, not a Studio bug: a session expires or the
// user signs out while a query (most often the 10s instance status poll) is still in
// flight, so the in-flight request 401s and the next poll fires before the redirect to
// sign-in tears the query down. Like the timeouts above, these surface as handled
// AxiosErrors through React Query's global error handler with no resource URL, so we
// match on the message alone. The auth layer already handles the real consequence
// (redirecting to sign-in). We deliberately do NOT drop 403 Forbidden — the user is
// authenticated but lacks permission, which usually points to a UI bug (an action
// shown that shouldn't be, or a mismatched resource id) worth keeping visible. (#1386)
if (/Request failed with status code 401\b/i.test(message)) {
return false;
}
// Deploying a component from a private git repository fails when the *instance* can't
// authenticate to that repository over SSH: no key installed, a key without access to the
// repo, or a git host missing from the instance's `ssh/known_hosts`. Harper reports it as a
// terminal deploy error whose message is already pure remediation guidance ("configure an
// SSH key on this Harper instance, ensure the key has access to the target repository, …"),
// which the deploy UI surfaces verbatim — `DeployProgress` also links to Config > SSH Keys.
// Nothing in Studio can be fixed in response; like the 401s above it's an expected state of
// an instance the customer still has to configure, and it created an Error Tracking issue
// off 8 events from a single session in the 24h to 2026-07-30. Dropping it also keeps the
// private repo URL the message embeds (owner + repo name) out of Error Tracking entirely;
// `redactErrorText` is the backstop for repo URLs in deploy errors we do keep. Match on the
// prefix rather than the SSH sentence, so a reworded remediation still matches.
if (/Failed to deploy private repository\b/i.test(message)) {
return false;
}
return true;
}