fix(error-tracking): collapse repeated frame cycles in parsed stacks - #4798
fix(error-tracking): collapse repeated frame cycles in parsed stacks#4798posthog[bot] wants to merge 9 commits into
Conversation
A recursion filled the 50 frame budget with one repeating cycle, so the outer frames that name the culprit were cut, and the frames that were left depended on where the runtime cut the stack. One fault then opened a new error tracking issue on every throw. The parser now keeps a single copy of a repeated frame cycle, removes the partial copy left at the cut end, and keeps the same rotation when the whole trace is the cycle. Frames are compared without the column, because the runtime reports the position of the failed call for the innermost frame. Generated-By: PostHog Desktop Task-Id: 65434eb9-ae46-4199-b0bc-d945c502ac45
🦔 PostHog Review reviewed this pull requestFound 0 must fix, 5 should fix, 1 consider. Published 6 findings (view the review). Resolved comments: 5 fixed, 1 already settled |
posthog-node Compliance ReportDate: 2026-09-05 07:26:17 UTC ✅ All Tests Passed!46/46 tests passed Capture Tests✅ 29/29 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
posthog-js Compliance ReportDate: 2026-09-05 07:22:50 UTC ✅ All Tests Passed!26/26 tests passed Capture Tests✅ 26/26 tests passed View Details
|
|
Size Change: +28.3 kB (+0.14%) Total Size: 20.5 MB 📦 View Changed
ℹ️ View Unchanged
|
|
PostHog Review alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏 |
On the bundle size reportI measured the change on its own, so the numbers can be weighed:
Happy to drop the rotation step if a reviewer would rather have the bytes. It only matters when the runtime cut every frame outside the recursion, which is the common shape for a stack overflow, so I would keep it. |
A stack can hold a second repeated section outside the recursion, such as a test runner that calls itself once per nested block. That section replaced the record of the recursion, so the partial copy the runtime's cut had left was never trimmed and the frames still moved from one throw to the next. Record every collapsed section instead of only the last one, and trim them from the outermost inward so the recorded positions still hold. Generated-By: PostHog Desktop Task-Id: fda4b3f8-e696-471c-9395-c637d0863104
One copy of a cycle can hold the same call site more than once, and the smallest frame then names two rotations. The parser kept whichever copy of that frame came first, which moves with the depth the runtime reached, so a trace cut down to the cycle still gave two frame orders for one fault. Compare the rotations frame by frame from their first frame on, so the whole copy decides which one is kept. Generated-By: PostHog Desktop Task-Id: fda4b3f8-e696-471c-9395-c637d0863104
…he limit A leftover copy of a cycle is dropped once the whole stack is read, but it held a slot of the frame limit while the parse loop ran. The loop then stopped before the outermost lines, so a stack that reaches the limit lost a different number of outer frames depending on where the runtime cut it. The limit now counts the frames that survive the trim, so the same stack reads the same outer frames on every throw. Generated-By: PostHog Desktop Task-Id: fda4b3f8-e696-471c-9395-c637d0863104
A minified bundle holds one line number per function, so two calls to the same function from different call sites compare equal and that pair collapses before the whole path has repeated. Every cut of such a stack now returns the same frames, but nothing held that in place. Generated-By: PostHog Desktop Task-Id: fda4b3f8-e696-471c-9395-c637d0863104
…most one A minified bundle puts every function on one line and hands out the same short names, so two neighbouring frames can share a name, file and line and be different code. Dropping the column from every comparison merged them, which lost a frame from stacks that hold no recursion at all and reported the surviving frame at the wrong position. Frames now have to match on their column. The innermost frame keeps the exemption, because the runtime reports the position of the call that ran out of stack there rather than the call site the copies under it share. A recursion whose path calls one function twice now keeps both of those frames in the copy it reports, since their columns tell them apart. Generated-By: PostHog Desktop Task-Id: fda4b3f8-e696-471c-9395-c637d0863104
…id not name The innermost frame carries the position of the call that ran out of stack, and that position moves from one throw to the next. A frame the runtime named is matched without it while the stack is read, but a frame with no name holds nothing else that says which code it is, so its position had to count and the moving frame stayed in the trace. One fault then still opened more than one issue for a recursion reported with no function name. The copies of a cycle say which code the frames at the innermost end hold once the whole stack is read, so the leftover copy there is now dropped at that point, with the innermost frame matched without its position. Generated-By: PostHog Desktop Task-Id: fda4b3f8-e696-471c-9395-c637d0863104
|
Problem
createStackParserstops after 50 parsed lines and nothing collapses a repeated frame cycle. A recursion spends the whole budget on one repeating pair, so:Changes
packages/core/src/error-tracking/parsers/index.ts:Before and after, for a mutual recursion between two minified functions
Note
Non-recursive stacks are unchanged. A short legitimate recursion now reports one copy of its cycle rather than every level, which is the trade for a stable fingerprint. One existing test asserted 15 frames from a test-runner stack that calls itself once per nested
describeblock; it now asserts 13.Release info Sub-libraries affected
Libraries affected
Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Autonomy: Fully autonomous
createDefaultStackParser. Before the change the frame list moved between two orders and two lengths; after it, all six runs give byte-identical frames.packages/core/src/error-tracking/parsers/index.spec.tscover the cycle collapse, a single self-calling function, outer frames surviving 400 recursion levels, equal frames wherever the cut fell, and the column of the failed call.@posthog/coresuite (1126 tests), theposthog-nodesuite (938 tests), and the browser exception tests. Formatting, oxlint, andtscare clean.Created with PostHog Desktop from this inbox report.