Record exceptions from rejected zone.js promises in span callbacks - #206
Record exceptions from rejected zone.js promises in span callbacks#206ayaangazali wants to merge 4 commits into
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🔗 Linked repositories identifiedCodeRabbit considers these linked repositories for cross-repo context during reviews:
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: Your plan includes up to 3 reviews per rolling hour; 1 remains after this review. 📝 WalkthroughWalkthroughThe Possibly related PRs
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/logfire-api/src/index.test.ts (1)
841-868: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAssert rejection propagation explicitly.
The test verifies return identity, exception recording, status, and span completion, but it never proves that the caller still receives
error. Add an assertion that awaiting the returned thenable rejects witherror.Suggested assertion
expect(result).toBe(zonePromise) + await expect(Promise.resolve(result)).rejects.toBe(error)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/logfire-api/src/index.test.ts` around lines 841 - 868, Update the test around the result returned by span('test', ...) to explicitly await the returned thenable and assert that it rejects with the original error. Keep the existing identity, exception recording, status, and completion assertions unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@packages/logfire-api/src/index.test.ts`:
- Around line 841-868: Update the test around the result returned by
span('test', ...) to explicitly await the returned thenable and assert that it
rejects with the original error. Keep the existing identity, exception
recording, status, and completion assertions unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 2b8fa357-b21d-4e1f-9744-7e06bd205bbd
📒 Files selected for processing (3)
.changeset/thenable-exception-recording.mdpackages/logfire-api/src/index.test.tspackages/logfire-api/src/index.ts
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
pydantic/logfire(manual)pydantic/pydantic-ai(manual)
petyosi
left a comment
There was a problem hiding this comment.
This fixes a real telemetry gap, but I think the fallback should use one settlement subscription before we merge it.
Calling both .then(undefined, onRejected) and .finally(onFinally) creates two derived chains. The ignored promise returned by .finally() preserves the rejection, so it can report an unhandled rejection even when the caller handles the original promise. This behavior already exists in the fallback, but this change should avoid carrying it forward.
Please use a single .then(onFulfilled, onRejected) subscription for promise-like values in this branch:
- end the span in the fulfillment handler;
- record the exception and end the span in the rejection handler;
- continue to return the original result unchanged.
Keeping the existing finally check as the gate is reasonable if we want to avoid consuming arbitrary lazy thenables.
Please also make the regression test representative of the zone.js case. With zone.js patching global Promise, a native async function can return an intrinsic native promise for which result instanceof Promise is false, while finally is present. The test should verify that the original rejection reaches the caller, the exception and error status are recorded, the span ends once, and no extra unhandled rejection is produced.
|
Good catch on the second chain, and you are right that it was mine to avoid rather than inherit. 4a96d77 switches to a single The test is rebuilt around the shape you described. It replaces the global I checked it fails for both regressions rather than just passing: against main it fails with zero |
4a96d77 to
83053c1
Compare
|
Flagging for re-review, since this still shows as changes requested but the change you asked for landed on 2026-08-04. The two derived chains are gone. Settlement is observed through a single Rebased onto current main (817fca1) and re-verified there, since the original run predated the dependency refresh in #212. Build is green, and the mutation check still holds: dropping the rejection handler fails the zone.js test with |
petyosi
left a comment
There was a problem hiding this comment.
One correctness regression remains in the new settlement path.
packages/logfire-api/src/index.ts:622: The new then lookup and the call below are both unguarded. A callback result with a working finally but a throwing then getter worked on main, which only called finally; this branch now makes span() throw after the callback succeeded and never ends the span. A then() method that throws synchronously has the same result.
Please protect both lookup and subscription, preserve the original result, ensure the span ends, and add regressions for both cases.
83053c1 to
4ecb704
Compare
|
You were right and my previous comment answered the wrong review. The 08-04 single-settlement change had landed, but the 08-10 point about the unguarded lookup was still open on the branch, and I read it as already covered. Fixed now in 8acb8e8.
Two regressions, each verified by reverting the source:
I left the Green on 79bcac0. Happy to close this if the zone.js path is not worth carrying. Built this with Claude Code's help and reviewed the diff myself. |
Reading and calling `then` run caller code, so neither may turn a callback that already succeeded into a throw or leave the span open.
8acb8e8 to
e47e0a6
Compare
|
Rebased onto #236 merged Behaviour is unchanged from your last review. Both regressions still bite: replacing |
|
Merged locally and landed on main as eca8805 — thanks for the thorough follow-through across the review rounds; both sets of requested changes were verified as addressed. Two follow-ups landed alongside it: 03b4d62 moves the test's unhandledRejection cleanup into a finally block, and cd89805 records the failure on the span when then throws before any settlement handler runs, so that path no longer closes with an OK status. (Squash-merged outside GitHub, so closing manually.) |
In
spanWithSettings(packages/logfire-api/src/index.ts) a callback returning a native promise getsrecordSpanExceptionon rejection, which records the error and sets the span status toERROR. The zone.js branch right below it, the one the "clunky detection" comment refers to, only attaches.finally(() => span.end()). So under zone.js, which is to say in Angular apps, a span whose callback rejects ends up looking successful: no exception event and no error status, even though the rejection still propagates to the caller.This adds the rejection handler to that branch when the value also exposes
then, reusing the samerecordSpanExceptioncall as the native path. The returned chain is ignored on purpose, exactly as the native branch does, soresultis still handed back untouched and the caller keeps seeing the rejection. I deliberately kept this scoped to the.finallybranch rather than treating every thenable as a promise, because the existing "thenable callback result is returned untouched" test pins the behavior for values that only havethen.The new test builds a non-native object exposing
thenandfinallythe way a zone.js promise does, and it fails on current main with zero calls torecordException. Ran the repo preflight andpnpm run check, both green. Patch changeset forlogfireincluded.quick disclosure: worked through this with Claude Code and reviewed the diff myself. Freshman here, and I do not have a real Angular app to hand, so if the zone.js shape I mocked is off I would be glad to be corrected :)