You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Due for payment 2026-10-21] [$250] [Sentry: APP-M6P + 10 siblings] Web RangeError Maximum call stack size exceeded on /, /onboarding/work-email and settings routes #102044
Version Number: 9.4.90 Reproducible in staging?: Needs to be confirmed Reproducible in production?: Yes — crash is live in production If this was caught during regression testing, add the test name, ID and link from BrowserStack: N/A — found via Sentry production crash scan Email or phone of affected tester (no customers): N/A Logs:https://stackoverflow.com/c/expensify/questions/4856 Expensify/Expensify Issue URL: Issue reported by: Sentry production crash scan Slack conversation (hyperlinked to channel name): N/A
Action Performed:
Open New Expensify on web.
Load the root route /, or go through onboarding to /onboarding/work-email.
Wait for the page to render.
Exact steps are not yet known. The crash is spread across /, /onboarding/work-email, /settings/profile, and /settings/subscription.
Expected Result:
The page renders. No crash.
Actual Result:
RangeError: Maximum call stack size exceeded. The stack shows two minified functions calling each other without end:
app:///home:226:63 (Ok)
app:///home:226:408 (Qk)
app:///home:226:63 (Ok)
app:///home:226:408 (Qk)
... repeating until the stack overflows
Sentry split this into 11 separate issues because the entry route differs, but the recursion signature is identical in each.
Workaround:
Unknown.
Platforms:
Select the officially supported platforms where the issue was reproduced:
Android: App
Android: mWeb Chrome
iOS: App
iOS: mWeb Safari
iOS: mWeb Chrome
Windows: Chrome
MacOS: Chrome / Safari
Screenshots/Videos
Sentry details
Sentry issues (11 siblings, same recursion signature):
First seen: 2026-09-18 (APP-M27), the rest on 2026-09-21
Last seen: ongoing
Platform: web, Chrome Mobile iOS and desktop Chrome
Mechanism: auto.browser.global_handlers.onerror
Sentry tags the frames third_party_code: True
First step — symbolication. The frames resolve to app:///home:226, a minified bundle line, so the two mutually recursive functions Ok and Qk cannot be named yet. Confirm that source maps for 9.4.90 uploaded correctly for the /home chunk, then re-read the stack. Until then the root cause is a guess.
Suspected cause: unbounded mutual recursion between two functions in the main bundle, reached during initial render on several unrelated routes. A cycle in data normalisation, a self-referential object walked by a recursive serializer, or a render loop are all consistent with the shape.
This is not our code, and Ok/Qk can never be symbolicated.app:///home is the HTML document, not a JS bundle — so the "first step: symbolication" in the issue body is a dead end, and no source-map fix will help.
Three independent checks agree:
App JS only ever ships as hashed chunks. Production right now serves main-bac62f7979647f1d.bundle.js, vendors-0ff4da366e27ee7f.bundle.js and heicTo-f009d6ccb336231a.bundle.js — the pattern set at config/rsbuild/rsbuild.common.ts:376. No chunk is named home.@sentry/react-native's createReactNativeRewriteFrames (installed on web too) strips the origin and prefixes app://, turning the document URL https://new.expensify.com/home into exactly app:///home.
The served document is 181 lines. I fetched https://new.expensify.com/home (HTTP 200, 13,096 bytes). Line 226 does not exist in it, so the code is injected at runtime and attributed to the document URL.
third_party_code: True is decisive. That tag is applied only when no frame carries our Sentry application key (src/libs/telemetry/integrations/index.web.ts:46-51), and the key is stamped into every emitted chunk (rsbuild.common.ts:586) — bundled node_modules included. So Ok/Qk are neither App code nor a vendored dependency. The tag is not vacuous here: these frames carry filename, line and column.
The page injects four scripts that fit: Ketch (web/index.html:39), Convert Experiments (:45), Google Tag Manager (:159), and Cloudflare's challenge-platform/scripts/jsd/main.js, added at the edge — it builds a hidden about:blank iframe and writes a script into it, and code in such an iframe inherits the parent document's URL. Two-character minified names and a mutually recursive pair are the standard shape of the obfuscators these scripts use. Which one owns line 226 can only be settled from the raw event, which I could not fetch (no Sentry access in this run).
Hypotheses I tested and ruled out
I chased the three plausible in-app explanations for an alternating two-frame overflow. All three are dead:
Onyx fastMerge ⇄ mergeObject (react-native-onyx@3.0.111, dist/utils.js:9/:39, calls at :27/:102) is the only genuine unguarded mutual recursion in our dependency tree — no depth counter, no cycle guard. But recursion depth is bounded by the source object's nesting only (target-only keys are shallow-copied), and JSON from OpenApp/ReconnectApp cannot be cyclic. Real payloads nest ~5-10 deep against a ~10k stack. Not reachable.
Report-ancestry walkers. There is no mutually recursive pair in ReportUtils.ts — every walker is self-recursive (one repeating frame, not two) or a while loop (hangs/OOMs, doesn't throw RangeError). getRootParentReport already has a visitedReportIDs cycle guard at src/libs/ReportUtils.ts:1362-1366. The self-parented-report fix from Prevent self-parented report when preexistingReportID equals parentReportID merged on 2026-08-26 and the guard is live at replaceOptimisticReportWithActualReport.ts:76-79. Separately, the unguarded walkers (isSelfDMOrSelfDMThread, getAncestors) only run on report/IOU pages — the LHN reads the immediate parent only, so none of them run on any of the four cited routes.
Navigation linking. The only mutual pair under src/libs/Navigation/ is getPathFromState ⇄ getPathFromStateWithDynamicRoute (getPathFromState.ts:180), and its base case is well-founded — popFocusedRoute strictly removes one leaf route per round trip. None of /, /onboarding/work-email, /settings/profile, /settings/subscription is a dynamic-route screen, so they never enter it. No commit in the 9.4.88-9.4.90 window removed or weakened a base case or depth guard.
The route spread itself is the tell: those four routes share no app code path beyond "the page loaded", which is exactly what an injected script does.
What changes do you think we should make in order to solve the problem?
Confirm it from the raw event. Someone with Sentry access opens APP-M6P, pulls the raw JSON, and reads exception.values[].stacktrace.frames[].filename for every frame (not just the top two) plus request.url. That names the injector in about two minutes.
Then suppress it as known third-party noise, using the pattern already established for exactly this class of error at src/setup/telemetry/setupSentry.ts:64-82 — which already carries two ignoreErrors entries for Convert Experiments bugs and one for an in-app-browser bridge. 25 users and ~137 events over 7 days is real quota for something we cannot fix.
There is no App code fix here beyond that filter, so Help Wanted / External is probably the wrong shape for this issue.
Do not chase the source maps. I verified web upload is already correct: output.sourceMap.js: 'source-map' in prod, plugin active for non-dev builds, sourcemaps.assets: './dist/**/*.{js,map}', and the upload release/dist are byte-identical to what the browser SDK reports at runtime (setupSentry.ts vs rsbuild.common.ts:570-571), with SENTRY_AUTH_TOKEN passed in buildWeb.yml. Bundle frames symbolicate fine; document frames never will.
What alternative solutions did you explore? (Optional)
Covered in the collapsed section above — Onyx merge recursion, report-ancestry cycles, and navigation route adaptation were each traced to file:line and eliminated.
Unrelated bug found while verifying the source-map pipeline
filesToDeleteAfterUpload: './dist/**/*.map' (rsbuild.common.ts:582) deletes every .map when the build ends. scripts/combine-web-sourcemaps.ts then globs dist/*.map with no empty-list guard and unconditionally writes dist/merged-source-map.js.map. In CI — where the auth token is set and upload succeeds — that file is effectively empty, and it is what gets published as the web-sourcemap.js.map release asset. Found by reading the code; I did not run the build. Worth its own issue.
Note on verification: browser control was unavailable in this run, so nothing here was reproduced in NewDot. Every claim above comes from static analysis of beb7231ce120254e8923e9749900b8e42e58432e plus one live fetch of the production HTML and bundle filenames.
Next Steps for Contributor+ team: To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/102044) to create a draft PR. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's proposal.
@mountiny@FitseTLT@WojtekBoman this issue was created 2 weeks ago. Are we close to approving a proposal? If not, what's blocking us from getting this issue assigned? Don't hesitate to create a thread in #expensify-open-source to align faster in real time. Thanks!
changed the title [-][$250] [Sentry: APP-M6P + 10 siblings] Web RangeError Maximum call stack size exceeded on /, /onboarding/work-email and settings routes[/-][+][Due for payment 2026-10-21] [$250] [Sentry: APP-M6P + 10 siblings] Web RangeError Maximum call stack size exceeded on /, /onboarding/work-email and settings routes[/+]on Oct 14, 2026
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.8-5 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
If no regressions arise, payment will be issued on 2026-10-21. 🎊
The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]
Activity
melvin-bot commentedon Sep 23, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @FitseTLT (
External)melvin-bot commentedon Sep 23, 2026
Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.
MelvinBot commentedon Sep 23, 2026
Proposal
What is the root cause of that problem?
This is not our code, and
Ok/Qkcan never be symbolicated.app:///homeis the HTML document, not a JS bundle — so the "first step: symbolication" in the issue body is a dead end, and no source-map fix will help.Three independent checks agree:
main-bac62f7979647f1d.bundle.js,vendors-0ff4da366e27ee7f.bundle.jsandheicTo-f009d6ccb336231a.bundle.js— the pattern set atconfig/rsbuild/rsbuild.common.ts:376. No chunk is namedhome.@sentry/react-native'screateReactNativeRewriteFrames(installed on web too) strips the origin and prefixesapp://, turning the document URLhttps://new.expensify.com/homeinto exactlyapp:///home.https://new.expensify.com/home(HTTP 200, 13,096 bytes). Line 226 does not exist in it, so the code is injected at runtime and attributed to the document URL.third_party_code: Trueis decisive. That tag is applied only when no frame carries our Sentry application key (src/libs/telemetry/integrations/index.web.ts:46-51), and the key is stamped into every emitted chunk (rsbuild.common.ts:586) — bundlednode_modulesincluded. SoOk/Qkare neither App code nor a vendored dependency. The tag is not vacuous here: these frames carry filename, line and column.The page injects four scripts that fit: Ketch (
web/index.html:39), Convert Experiments (:45), Google Tag Manager (:159), and Cloudflare'schallenge-platform/scripts/jsd/main.js, added at the edge — it builds a hiddenabout:blankiframe and writes a script into it, and code in such an iframe inherits the parent document's URL. Two-character minified names and a mutually recursive pair are the standard shape of the obfuscators these scripts use. Which one owns line 226 can only be settled from the raw event, which I could not fetch (no Sentry access in this run).Hypotheses I tested and ruled out
I chased the three plausible in-app explanations for an alternating two-frame overflow. All three are dead:
fastMerge⇄mergeObject(react-native-onyx@3.0.111,dist/utils.js:9/:39, calls at:27/:102) is the only genuine unguarded mutual recursion in our dependency tree — no depth counter, no cycle guard. But recursion depth is bounded by the source object's nesting only (target-only keys are shallow-copied), and JSON fromOpenApp/ReconnectAppcannot be cyclic. Real payloads nest ~5-10 deep against a ~10k stack. Not reachable.ReportUtils.ts— every walker is self-recursive (one repeating frame, not two) or awhileloop (hangs/OOMs, doesn't throwRangeError).getRootParentReportalready has avisitedReportIDscycle guard atsrc/libs/ReportUtils.ts:1362-1366. The self-parented-report fix from Prevent self-parented report when preexistingReportID equals parentReportID merged on 2026-08-26 and the guard is live atreplaceOptimisticReportWithActualReport.ts:76-79. Separately, the unguarded walkers (isSelfDMOrSelfDMThread,getAncestors) only run on report/IOU pages — the LHN reads the immediate parent only, so none of them run on any of the four cited routes.src/libs/Navigation/isgetPathFromState⇄getPathFromStateWithDynamicRoute(getPathFromState.ts:180), and its base case is well-founded —popFocusedRoutestrictly removes one leaf route per round trip. None of/,/onboarding/work-email,/settings/profile,/settings/subscriptionis a dynamic-route screen, so they never enter it. No commit in the 9.4.88-9.4.90 window removed or weakened a base case or depth guard.The route spread itself is the tell: those four routes share no app code path beyond "the page loaded", which is exactly what an injected script does.
What changes do you think we should make in order to solve the problem?
exception.values[].stacktrace.frames[].filenamefor every frame (not just the top two) plusrequest.url. That names the injector in about two minutes.src/setup/telemetry/setupSentry.ts:64-82— which already carries twoignoreErrorsentries for Convert Experiments bugs and one for an in-app-browser bridge. 25 users and ~137 events over 7 days is real quota for something we cannot fix.Help Wanted/Externalis probably the wrong shape for this issue.Do not chase the source maps. I verified web upload is already correct:
output.sourceMap.js: 'source-map'in prod, plugin active for non-dev builds,sourcemaps.assets: './dist/**/*.{js,map}', and the uploadrelease/distare byte-identical to what the browser SDK reports at runtime (setupSentry.tsvsrsbuild.common.ts:570-571), withSENTRY_AUTH_TOKENpassed inbuildWeb.yml. Bundle frames symbolicate fine; document frames never will.What alternative solutions did you explore? (Optional)
Covered in the collapsed section above — Onyx merge recursion, report-ancestry cycles, and navigation route adaptation were each traced to file:line and eliminated.
Unrelated bug found while verifying the source-map pipeline
filesToDeleteAfterUpload: './dist/**/*.map'(rsbuild.common.ts:582) deletes every.mapwhen the build ends.scripts/combine-web-sourcemaps.tsthen globsdist/*.mapwith no empty-list guard and unconditionally writesdist/merged-source-map.js.map. In CI — where the auth token is set and upload succeeds — that file is effectively empty, and it is what gets published as theweb-sourcemap.js.maprelease asset. Found by reading the code; I did not run the build. Worth its own issue.Note on verification: browser control was unavailable in this run, so nothing here was reproduced in NewDot. Every claim above comes from static analysis of
beb7231ce120254e8923e9749900b8e42e58432eplus one live fetch of the production HTML and bundle filenames.Related issue: [Due for payment 2026-09-03] Prevent self-parented report when preexistingReportID equals parentReportID (
Expensify/App#99594) describes the sameRangeErrorsymptom from a self-parented report. It is not a duplicate — its fix is already merged and it is open only to track payment — but it is why the report-ancestry hypothesis was worth ruling out explicitly.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/102044)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
view run
FitseTLT commentedon Sep 25, 2026
Will review on Monday
melvin-bot commentedon Sep 29, 2026
@FitseTLT Whoops! This issue is 2 days overdue. Let's get this updated quick!
FitseTLT commentedon Sep 29, 2026
Reviewing tomorrow
36 remaining items
mountiny commentedon Oct 7, 2026
Great. Thank you very much for digging into this. Are you able to create the narrower filter?
melvin-bot commentedon Oct 7, 2026
@mountiny @FitseTLT @WojtekBoman this issue was created 2 weeks ago. Are we close to approving a proposal? If not, what's blocking us from getting this issue assigned? Don't hesitate to create a thread in #expensify-open-source to align faster in real time. Thanks!
WojtekBoman commentedon Oct 8, 2026
Sure! I'll prepare the PR 🫡
WojtekBoman commentedon Oct 8, 2026
Here's the draft PR #103459. I’ve asked the team for an internal review, it should be ready tomorrow!
[-][$250] [Sentry: APP-M6P + 10 siblings] Web RangeError Maximum call stack size exceeded on /, /onboarding/work-email and settings routes[/-][+][Due for payment 2026-10-21] [$250] [Sentry: APP-M6P + 10 siblings] Web RangeError Maximum call stack size exceeded on /, /onboarding/work-email and settings routes[/+]melvin-bot commentedon Oct 14, 2026
@FitseTLT
The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.8-5 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:
If no regressions arise, payment will be issued on 2026-10-21. 🎊
The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]
FitseTLT commentedon Oct 14, 2026
No need for a checklist