langfuse/sentry-instrumentation
| Decide whether and how errors report to Sentry. Use when touching capture or error-handling paths in `web/**`, triaging Sentry noise, or changing Sentry settings.
npx skills add https://github.com/langfuse/langfuse --skill sentry-instrumentation
One idea: an event in Sentry is a promise that a human should act. If
nobody would act on it, do not send it. If you send it, make it legible (a
real Error with a stack — never a string/object/SyntheticEvent) and
routable (an area tag, a message stable enough to group). Sentry is not a
log sink; the server-side observability stack is where firehose logs live.
When adding or changing an error path in web/**, decide explicitly whether it
should reach Sentry — do not captureException (or console.error) reflexively.
Run the decision tree, then say in one line what you chose and why (in the plan
or PR description). The three outcomes:
| The failure is… | Do | Never |
|---|---|---|
| an expected user-facing state — a missing/forbidden resource, expired session, invalid user input, a malformed URL in user content | render the UX (error page / toast / plain text) | capture — it is the product working as designed |
| a transport / offline / infra failure — fetch failed, a 5xx on a poll, an LB blip | let the UI degrade; the server owns this signal | capture client-side — it is an amplified, lower-fidelity copy of a server truth |
| our code failed — an invariant broke, a parse threw, a worker failed to load | captureException a real Error with an area tag via the shared helpers | pass a raw string/object/Event |
console.error is a capture API here. instrumentation-client.ts enables
captureConsoleIntegration({ levels: ["error"] }), so **every console.error
becomes a Sentry event.** Logging an object yields an opaque [object Object]
fingerprint; logging non-actionable info still mints an issue. For
non-actionable logging use console.warn; to report a real failure, capture it
properly (below).
captureException:**
captureUnknownError(context, value, extra?)— turns any caught value into a legible Error (real Errors pass through
with their stack; everything else is synthesized), tags area, and logs via
console.warn so the console integration does not double-capture.
reportParserWorkerError(hook, event)— extracts the real fields from a Worker ErrorEvent (message/filename/lineno)
instead of stringifying it to [object ErrorEvent].
web/src/utils/sentryFilters.ts**
(documented, unit-tested), called from beforeSend in
web/instrumentation-client.ts (the
only Sentry.init in the codebase — beforeSend,
captureConsoleIntegration, denyUrls, replay masking). Add every new filter
as a named predicate there, never as an ad-hoc inline check. beforeSend
now holds NO inline checks — every filter routes through a named predicate
(isNoisyHttpClientPollEvent, isDenylistedNoiseEvent,
isReactDevtoolsInternalEvent). Keep it that way.
handleTrpcError(web/src/utils/api.ts) is the single
chokepoint every query/mutation error flows through — the place to drop
expected codes and tag the rest (the seam-classification lever, PR #15243).
area, keep the message static. `captureException(err, { tags: { area },extra }). Fingerprints group on the message, so put variable IDs in extra`,
never in the message string.
> PR references name the lever that established each pattern. Verify a cited
> symbol or swap against the current tree before relying on it — the code, not
> this doc, is the source of truth.
A NOT_FOUND / FORBIDDEN / UNAUTHORIZED the UI already renders is not a
regression. ⚠ TRPCClientError: Trace not found was the #2 issue by volume
(~30k events); PR #15243 drops those at the tRPC seam and switches the
"Project Not Found" state (which captured on every mount via
ErrorPageWithSentry) to the non-capturing ErrorPage.
Error, never a string / object / SyntheticEvent /ErrorEvent.** Those collapse to opaque [object Object] / `[object
ErrorEvent]` fingerprints with no stack. ⚠ The 5EX cluster (opaque non-Error
captures, PR #15175) and the parse-worker [object ErrorEvent] family (PR
captureUnknownError /reportParserWorkerError.
console.error mints a Sentry event — pick the level deliberately. Useconsole.warn for non-actionable logs (the #15173 pattern); use
captureException for real failures. Never console.error(someObject).
One LB 502 or a poll blip becomes thousands of browser events. ⚠ The 418
HTTP Client Error saga (~44k events → 8/24h after scoping the httpClient
integration, PR #15145) and the next-auth CLIENT_FETCH_ERROR /
Failed to fetch families (denylist, PR #15238). The real outage is visible
server-side; the client copy is noise.
over suppressing the event. ⚠ User-content markdown URLs rendered through a
Next.js <Link> logged Invalid href … passed to next/router (~40k events)
— note getSafeLinkUrl does NOT prevent this: a malformed-but-parseable URL
(a second https:// → repeated //) passes validation and <Link>'s router
still rejects it. PR #15245 renders these as a native <a> (which never runs
the router's href validation), removing the family at the source — no new
filter needed. (The older inline beforeSend invalid-href check — which
never fired anyway, wrong field, Rule 6 — was removed in #15276.)
beforeSend / denylist rule: narrow signature + written rationale +a NEGATIVE fixture proving a real error still passes.** This is the MANDATORY
review gate — for any suppression change, answer "does this rule hide a real
error?" in the PR, and add a test asserting a real 5xx / unknown code / thrown
error is NOT dropped (the sentryFilters.clienttest.ts pattern from #15145 /
captures, benign strings) carry their text on event.message /
event.logentry.message, NOT event.exception.values[0].value — a filter
that only reads the exception value silently never fires on them (the reason
the original invalid-href filter never worked).
extra, or tag. No prompttext, trace content, tokens, share-link secrets, user/session ids. Respect
the replay masking already configured for HIPAA/regions in
instrumentation-client.ts. A message that interpolates user data both leaks
and shatters grouping (Rule 5).
validations run only in the real client runtime, not jsdom — a green unit
test does NOT prove the console error is gone. Reproduce it in a browser
against the running app (Playwright), do an A/B, and confirm the event
disappears. ⚠ PR #15245 was verified this way — its native <a> fired 0
where the prior <Link> fired the error ×12. Unit tests lock the *contract*; the
browser proves the *noise removal*.
transport / our-code — and write the one-line decision. That IS the PR note.
capture). Real → captureException a real Error via the helpers, with an
area tag, static message, structured extra, no PII.
sentryFilters.ts reading theright event field, with a written rationale and a negative fixture (Rule 6).
Prefer root-causing the emission instead (Rule 5).
event removal (Rule 8).
"does this hide a real error?" answer + the negative-fixture test.
state (Rule 1).
captureException(nonError) or console.error(object) → opaque fingerprint(Rules 2, 3).
beforeSend filter with no rationale, no negative fixture, or onethat reads the wrong event field (Rule 6).
(Rules 5, 7).
beforeSend check instead of a named, tested predicate insentryFilters.ts.
— the capture contract in depth: the shared-helper APIs, the beforeSend /
denylist authoring protocol (field-reading, negative fixtures), fingerprinting
and area tagging, the known noise families, and the shipped-PR case studies
(#15145 / #15173 / #15174 / #15175 / #15238 / #15243 / #15245). Read when
authoring a suppression rule, hardening a capture path, or triaging a family.
Take langfuse/sentry-instrumentation from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.