feat(api): forward original Error to Faro's beforeSend via originalError - #53
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
captureException() can throw at runtime when called with a non-Error plus context/fingerprint because the new object-spread logic may spread undefined into options_.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR improves @nais/apm’s manual exception capture by forwarding the original thrown Error instance into Faro’s pushError as originalError, enabling beforeSend logic to inspect item.payload.originalError when preserveOriginalError: true is enabled. It also adds beta-branch operational tooling/workflows and documents the new error-handling pattern.
Changes:
- Forward
originalErrorfromcaptureException()to FaropushError()when the input is a realError. - Update/extend tests and README to cover/document
originalErrorforwarding behavior. - Add/update GitHub workflows and release docs for beta sync and beta publishing constraints.
File summaries
| File | Description |
|---|---|
| src/api.ts | Adds originalError forwarding and reworks pushError options construction. |
| src/api.test.ts | Updates expectations for originalError and adds assertions around non-Error coercion. |
| src/index.test.ts | Updates integration-level expectation for pushError options to include originalError. |
| README.md | Documents how to use preserveOriginalError + beforeSend to branch on error type/shape. |
| RELEASING.md | Adds process guidance for keeping beta in sync and protecting beta branch history. |
| .github/workflows/sync-beta.yml | Introduces workflow to open a “sync main into beta” PR on each main push. |
| .github/workflows/release-please-beta.yml | Tightens manual beta publish to only allow workflow_dispatch from beta. |
| .github/workflows/ci.yaml | Runs CI on PRs targeting beta and pins core actions to commit SHAs. |
Review details
- Files reviewed: 8/8 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const originalError = error instanceof Error ? error : undefined; | ||
| const hasContext = Object.keys(context).length > 0; | ||
| const options_ = hasContext || originalError ? { ...(hasContext && { context }), ...(originalError && { originalError }) } : undefined; |
| it('coerces non-Error values and does not forward originalError', () => { | ||
| captureException('plain string failure'); | ||
| const pushed = api.pushError.mock.calls[0]?.[0] as Error; | ||
| expect(pushed).toBeInstanceOf(Error); | ||
| expect(pushed.message).toBe('plain string failure'); | ||
| expect(api.pushError.mock.calls[0]?.[1]).toBeUndefined(); | ||
| }); |
4312b20 to
ed14642
Compare
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
captureException() now passes error as originalError in the pushError
options whenever the input is a genuine Error instance. Combined with
init({ faro: { preserveOriginalError: true } }), the real Error
instance is available on item.payload.originalError inside a custom
beforeSend hook, before Faro strips it prior to sending to the
collector. This lets callers centrally branch on error type/shape
before reporting, matching Faro's own auto-capture instrumentations
which already thread originalError through in the same way.
Coerced non-Error values (e.g. plain strings) do not get an
originalError, since there is nothing more original than the
synthesized Error itself.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ed14642 to
aa45165
Compare
94cca9a to
aa45165
Compare
🤖 I have created a release *beep* *boop* --- ## [0.7.0-beta.1](apm-v0.6.4-beta.1...apm-v0.7.0-beta.1) (2026-09-11) ### Features * **api:** forward original Error to Faro's beforeSend via originalError ([#53](#53)) ([2e4766c](2e4766c)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
What
captureException()now forwards the realErrorinstance to Faro'spushErroras theoriginalErroroption whenever the input is a genuineError. Combined withinit({ faro: { preserveOriginalError: true } }), this makes the original thrown instance available onitem.payload.originalErrorinside a custombeforeSendhook, before Faro strips it prior to sending to the collector.This mirrors how Faro's own auto-capture instrumentations (
window.onerror,unhandledrejection) already threadoriginalErrorthrough —@nais/apm'scaptureException()wrapper previously did not, which meantpreserveOriginalError: truehad no effect for manually-reported exceptions.Coerced non-
Errorvalues (e.g. plain strings) do not get anoriginalError, since there's nothing more original than the synthesizedErroritself.Why
A consumer team asked for a way to centrally branch on error type/shape (e.g.
instanceof SomeCustomError) in one place before reporting to apm, using Faro's existingpreserveOriginalErrorescape hatch. That option only worked for Faro's own automatic instrumentations, not forcaptureException()calls.Changes
src/api.ts: passoriginalErrortofaro.api.pushError()whenerroris anErrorinstancesrc/api.test.ts/src/index.test.ts: updated/extended tests to assertoriginalErroris forwarded (and not forwarded for coerced non-Error values)README.md: documented the pattern with an examplebeforeSendhookTesting
pnpm test— all 230 tests pass (22 test files)