Skip to content

fix(sentry): give stackless console events a stack and stop issue explosion - #2810

Merged
innolope-dev merged 1 commit into
devfrom
fix/sentry-stackless-exception-noise
Aug 24, 2026
Merged

innolope-dev merged 1 commit into
devfrom
fix/sentry-stackless-exception-noise

Conversation

@innolope-dev

Copy link
Copy Markdown
Collaborator

What

Started from this PostHog view — $exception events where $cymbal_errors is set. That property is PostHog's flag for "empty exception list": the event arrived with a message but no exception.values, so no type and no stack.

Those events were reaching Sentry fine. The problem is the shape they arrived in, and how they grouped. 433 events / 255 people over 7d (~3.4% of exception volume).

Why they were stackless

captureConsoleIntegration only calls captureException when one of the console args is an Error instance; otherwise it calls captureMessage, which carries no stack at all (captureconsole.js). Every event on that page was either a console.error whose args held no Error, or one of our deliberate captureMessage calls.

Changes

1. attachStacktrace: true — synthesizes a stack for every message event, including fetchWithSentry / native-auth-capture, and third-party console noise where we can't touch the call site. One line, covers all 433.

2. Call sites that destroyed their payload — a stack can't recover an error the call site already threw away:

Site Was
SetupPasskey.tsx passed err.name, err.message instead of err — largest family, ~126/wk
useQRScanner.ts ×2 qr-scanner rejects with bare strings ('Camera not found.')
SumsubKycWrapper.tsx no Error object at all
withdraw/crypto/page.tsx logged [object Object] — money-moving flow
useSendMoney.ts logged [object Object] — money-moving flow

Adds utils/to-error.ts, replacing what were already three ad-hoc copies of the same normalization. It JSON-serializes plain objects so the shape survives into the title instead of collapsing.

Passkey filtering is unchanged — the AbortError / not allowed by the user variants still match, via exception type rather than message.

3. sanitizeUrl fingerprint gap — it normalized numeric query values but not the rest, so one upstream 429 was three separate issues:

Issue Title Events (7d)
PEANUT-UI-SGN exchange-rate?accountType=iban 273
PEANUT-UI-SQ4 exchange-rate?accountType=clabe 151
PEANUT-UI-SSA exchange-rate?accountType=gb 131

Now normalizes every value. A lookahead skips values the numeric pass already replaced, so fingerprints that were already correct keep their existing issue rather than forking a new one on deploy.

While adding tests for this I found a pre-existing bug: the numeric rule was unanchored, so ?address=0xaf88d065 had its leading 0 replaced and became {id}xaf88d065 — every wallet still got its own fingerprint and the value was unreadable. Anchored with (?=&|$).

4. OneSignal issue explosion — its op-failure message inlines a per-device onesignalId, so every device minted its own Sentry issue. These are fingerprinted, not dropped: push-subscription writes failing is exactly the signal that was missing while iOS push was dead on a mis-provisioned APNs key. Collapsed onto one issue per op name. The worker messenger's no-service-worker warning is pure noise and is ignored.

Note

attachStacktrace lands on threads, not exception.values, so PostHog's mirror will still flag the remaining third-party ones as an empty exception list. That flag only clears where we now pass a real Error. Sentry is fully fixed; the PostHog view shrinks but won't empty out.

Testing

  • New: to-error.test.ts (8 cases), sentry-fingerprint-url.test.ts (7 cases, incl. the no-refingerprint regression guard)
  • Extended sentry.utils.test.ts with the OneSignal fingerprint + ignore cases
  • 94 sentry/toError tests and 272 tests across the touched components pass; prettier and eslint clean

Overlap

Touches sentry.utils.ts, as does #2735 (chained-error matching + Capgo noise). No textual conflict — different regions, and collapseNoisyFingerprint composes with its shouldIgnoreError changes — but whichever lands second deserves a re-read.

…losion

Three related defects surfaced by PostHog's $cymbal_errors filter, which
flags $exception events arriving with an empty exception list. Those events
were reaching Sentry fine — but as message events with no frames, and with
grouping keys that forked a new issue per URL variant and per device.

captureConsoleIntegration only calls captureException when one of the console
args is an Error instance; otherwise it calls captureMessage, which carries no
stack. Set attachStacktrace so every message event gets one, including the
deliberate captureMessage calls in fetchWithSentry and native-auth-capture.
That covers third-party console noise too, where we cannot touch the call site.

A stack cannot recover a payload the call site already destroyed, so fix the
first-party sites that discarded theirs: SetupPasskey passed err.name and
err.message instead of err (the largest family, ~126/wk); qr-scanner rejects
with bare strings; the Sumsub loader had no Error at all; and the crypto
withdraw and useSendMoney handlers were logging [object Object] in
money-moving flows. Adds utils/to-error, replacing what were already three
ad-hoc copies of the same normalization, and serializing plain objects so the
shape survives into the title.

sanitizeUrl normalized numeric query values but not the rest, so one 429 on
/bridge/exchange-rate was three issues (accountType=iban/clabe/gb). Normalize
every value, with a lookahead so already-correct fingerprints keep their
existing issue instead of forking on deploy. Anchoring the numeric rule fixes
a bug it caught: unanchored, it ate the leading 0 of an 0x-prefixed address
and left {id}xaf88d065, so every wallet still got its own fingerprint.

OneSignal inlines a per-device onesignalId into its op-failure message, minting
a Sentry issue per device. Collapse those onto one fingerprint per op name
rather than dropping them — push-subscription writes failing is the signal that
was missing while iOS push was dead on a mis-provisioned APNs key. Its worker
messenger's no-service-worker warning is pure noise and is ignored.
@vercel

vercel Bot commented Aug 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
peanut-wallet Building Building Preview Aug 24, 2026 9:08pm

Request Review

@coderabbitai

coderabbitai Bot commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 27 days. After that, they cost $0.25 per reviewed file.

Or wait 31 minutes for your next included review.

View limit details

Limit details: You’ve used all 2 included reviews currently available. Your 55 included PR review attempts over the past 7 days set your current allowance at 2 reviews per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: fe074fb7-7acb-4c44-853e-e44ebc8d2aed

📥 Commits

Reviewing files that changed from the base of the PR and between ec6cfbf and 3801dce.

📒 Files selected for processing (12)
  • sentry.utils.test.ts
  • sentry.utils.ts
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx
  • src/components/Global/QRScanner/useQRScanner.ts
  • src/components/Kyc/SumsubKycWrapper.tsx
  • src/components/Setup/Views/SetupPasskey.tsx
  • src/hooks/wallet/useSendMoney.ts
  • src/utils/__tests__/sentry-fingerprint-url.test.ts
  • src/utils/__tests__/to-error.test.ts
  • src/utils/sentry-init.ts
  • src/utils/sentry.utils.ts
  • src/utils/to-error.ts

Comment @coderabbitai help to get the list of available commands.

@innolope-dev innolope-dev self-assigned this Aug 24, 2026
@innolope-dev

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

Copy link
Copy Markdown
Contributor

Code-analysis diff

Painscore total: 7425.47 → 7431.14 (+5.67)
Findings: +2 net (+41 new, -39 resolved)

🆕 New findings (41)

  • critical complexity — src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 95, MI 50.3, SLOC 427
  • critical complexity — src/components/Global/QRScanner/useQRScanner.ts — CC 64, MI 55.91, SLOC 307
  • critical complexity — src/components/Kyc/SumsubKycWrapper.tsx — CC 60, MI 60.45, SLOC 306
  • high hotspot — src/app/(mobile-ui)/withdraw/crypto/page.tsx — 46 commits, +541/-275 lines since 6 months ago
  • high hotspot — src/utils/sentry.utils.ts — 43 commits, +772/-272 lines since 6 months ago
  • medium react-long-component — src/app/(mobile-ui)/withdraw/crypto/page.tsx:46 — WithdrawCryptoPage is 663 lines — split it
  • medium high-mdd — src/app/(mobile-ui)/withdraw/crypto/page.tsx:46 — WithdrawCryptoPage: MDD 137.4 (uses across many lines from declarations)
  • medium high-mdd — src/components/Global/QRScanner/useQRScanner.ts:97 — useQRScanner: MDD 124.8 (uses across many lines from declarations)
  • medium high-dlt — src/app/(mobile-ui)/withdraw/crypto/page.tsx:46 — WithdrawCryptoPage: DLT 66 (calls 66 distinct functions — high context load)
  • medium high-mdd — src/components/Setup/Views/SetupPasskey.tsx:37 — SetupPasskey: MDD 62.8 (uses across many lines from declarations)
  • medium high-dlt — src/components/Global/QRScanner/useQRScanner.ts:97 — useQRScanner: DLT 45 (calls 45 distinct functions — high context load)
  • medium high-mdd — src/hooks/wallet/useSendMoney.ts:52 — useSendMoney: MDD 44.5 (uses across many lines from declarations)
  • medium high-mdd — src/utils/sentry.utils.ts:471 — fetchWithSentry: MDD 44.1 (uses across many lines from declarations)
  • medium high-mdd — src/app/(mobile-ui)/withdraw/crypto/page.tsx:330 — : MDD 41.0 (uses across many lines from declarations)
  • medium complexity — src/components/Setup/Views/SetupPasskey.tsx — CC 29, MI 57.32, SLOC 153
  • medium method-complexity — src/app/(mobile-ui)/withdraw/crypto/page.tsx:330 — CC 21 SLOC 128
  • medium method-complexity — src/app/(mobile-ui)/withdraw/crypto/page.tsx:193 — CC 18 SLOC 92
  • medium complexity — src/hooks/wallet/useSendMoney.ts — CC 16, MI 56.52, SLOC 83
  • medium method-complexity — src/components/Global/QRScanner/useQRScanner.ts:227 — CC 15 SLOC 73
  • medium complexity — src/utils/sentry-init.ts — CC 12, MI 60.86, SLOC 37

…and 21 more.

✅ Resolved (39)

  • src/app/(mobile-ui)/withdraw/crypto/page.tsx — CC 95, MI 50.32, SLOC 426
  • src/components/Global/QRScanner/useQRScanner.ts — CC 64, MI 55.98, SLOC 305
  • src/components/Kyc/SumsubKycWrapper.tsx — CC 60, MI 60.46, SLOC 306
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx — 45 commits, +539/-274 lines since 6 months ago
  • src/utils/sentry.utils.ts — 42 commits, +753/-269 lines since 6 months ago
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:45 — WithdrawCryptoPage is 663 lines — split it
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:45 — WithdrawCryptoPage: MDD 137.4 (uses across many lines from declarations)
  • src/components/Global/QRScanner/useQRScanner.ts:96 — useQRScanner: MDD 124.8 (uses across many lines from declarations)
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:45 — WithdrawCryptoPage: DLT 65 (calls 65 distinct functions — high context load)
  • src/components/Setup/Views/SetupPasskey.tsx:37 — SetupPasskey: MDD 59.6 (uses across many lines from declarations)
  • src/hooks/wallet/useSendMoney.ts:51 — useSendMoney: MDD 44.5 (uses across many lines from declarations)
  • src/components/Global/QRScanner/useQRScanner.ts:96 — useQRScanner: DLT 44 (calls 44 distinct functions — high context load)
  • src/utils/sentry.utils.ts:455 — fetchWithSentry: MDD 44.1 (uses across many lines from declarations)
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:329 — : MDD 41.0 (uses across many lines from declarations)
  • src/components/Setup/Views/SetupPasskey.tsx — CC 29, MI 57.28, SLOC 153
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:329 — CC 21 SLOC 127
  • src/app/(mobile-ui)/withdraw/crypto/page.tsx:192 — CC 18 SLOC 92
  • src/hooks/wallet/useSendMoney.ts — CC 16, MI 56.64, SLOC 82
  • src/components/Global/QRScanner/useQRScanner.ts:226 — CC 15 SLOC 72
  • src/utils/sentry-init.ts — CC 12, MI 61.17, SLOC 36

…and 19 more.

📈 Painscore deltas (top movers)

File Before After Δ
src/utils/to-error.ts 0.0 4.4 +4.4

@github-actions

Copy link
Copy Markdown
Contributor

🧪 UI test report — ✅ all green

Suites

  • ✅ unit: 3525 ran, 0 failed, 0 skipped, 1.0m

📊 Coverage (unit)

metric %
statements 69.2%
branches 54.4%
functions 60.0%
lines 70.1%
⏱ 10 slowest test cases
time test
4.1s src/components/Card/share-asset/__tests__/shareAssetLayout.test.ts › never places two stickers in heavy overlap (broad seed sweep)
1.0s src/hooks/query/__tests__/user.test.tsx › does NOT clear a token that rotated mid-request (stale 401 racing a fresh login)
0.6s src/utils/__tests__/crisp.test.ts › retries configuration on the next open after a failure
0.5s src/utils/__tests__/sentry.utils.test.ts › defaults to the client budget under a browser global
0.4s src/utils/__tests__/crisp.test.ts › settles, and hands back a usable plugin, against a real-shaped plugin proxy
0.4s src/utils/__tests__/crisp.test.ts › configures once across repeated support opens
0.3s src/utils/__tests__/crisp.test.ts › resets the native session on logout once support has been opened
0.3s src/utils/__tests__/auth-token.test.ts › is none — never guarded — when only the guarded marker is present
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › Bank withdrawal keeps the $1 minimum for sub-$1 amounts
0.3s src/app/(mobile-ui)/withdraw/__tests__/withdraw-states.test.tsx › the lazy bank view survives a re-render without blanking
📍 Inline annotations are in the **Unit test report** check above. Coverage artifact: `coverage-unit`. Generated by `.github/workflows/tests.yml`.

@innolope-dev
innolope-dev merged commit 6aa258a into dev Aug 24, 2026
20 of 21 checks passed
innolope-dev added a commit that referenced this pull request Aug 24, 2026
#2810 added collapseNoisyFingerprint at the same spot this branch added
isTransientCapgoNoise. Independent additions, both kept: Capgo's transient
updater logs are suppressed in shouldIgnoreError, OneSignal's per-device op
failures are re-fingerprinted in beforeSendHandler.

This branch was successfully deployed

1 active deployment
Preview — 3801dce9 Deployed Aug 24, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant