Skip to content

process: implement process.report.writeReport() - #34401

Open
robobun wants to merge 7 commits into
claude/ccc1369c/process-report-exclude-envfrom
claude/farm/ee2ae18e/process-report-writeReport
Open

robobun wants to merge 7 commits into
claude/ccc1369c/process-report-exclude-envfrom
claude/farm/ee2ae18e/process-report-writeReport

Conversation

@robobun

@robobun robobun commented Jul 16, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #34400.

What

process.report.writeReport() was a // TODO: stub that returned callFrame->argument(0) verbatim, so:

process.report.writeReport()            // undefined
process.report.writeReport(err) === err // true (returned the Error object back)
process.report.writeReport("out.json")  // "out.json", but NO file written

This reads as success to anything inspecting the return value while nothing is actually written to disk.

Fix

Process_functionWriteReport in src/jsc/bindings/BunProcess.cpp now:

  • Parses writeReport([filename][, err]) with Node's overload rules (object first-arg is err).
  • Validates file as a string and err as an object (ERR_INVALID_ARG_TYPE).
  • Reads compact / directory / filename / excludeEnv / excludeNetwork from the process.report receiver.
  • Generates the default filename report.YYYYMMDD.HHMMSS.<pid>.0.<seq>.json when none is provided.
  • Builds the report via constructReportObjectComplete, serializes with JSC::JSONStringify (2-space indent, or single line when compact), and writes it with a trailing newline.
  • For "stdout" / "stderr", writes the JSON to that stream and returns the name (no stderr banner).
  • Otherwise writes to directory/filename (or cwd), prints Writing Node.js report to file: ... / Node.js report completed to stderr, and returns the filename. If the file can't be opened, prints Failed to open Node.js report file: ... (errno: N) to stderr and returns "".

The err argument is now threaded through to the report body. constructReportJavaScriptStack is extracted as a shared helper (used by both POSIX and Windows builders) that derives javascriptStack.message and javascriptStack.stack from the supplied error's .stack when present, falling back to the synthetic ERR_SYNTHETIC callstack otherwise. getReport(err) is wired the same way and now validates err. The Windows report builder also receives the resolved fileName so header.filename is populated there too.

Verification

Added a process.report.writeReport describe block to test/js/node/process/process.test.js covering: default/explicit filename, Error-arg overload (return value is a string, not the Error), file contents are parseable JSON with header.filename set, argument validation, directory + compact + filename config, open-failure returns "", "stdout" special target, and javascriptStack.message reflecting a supplied Error for both writeReport and getReport.

All writeReport tests fail on current main and pass with this change. Behavior checked against Node v26.3.0.

Not in this PR

reportOnUncaughtException, reportOnSignal, reportOnFatalError, and the --report-* CLI flags still do not trigger reports; those need separate wiring into the uncaught-exception and signal paths.


no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/process/process.test.js

@robobun

robobun commented Jul 16, 2026 •

Copy link
Copy Markdown
Collaborator Author

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. process: fix report.excludeEnv copy-paste, add signal/excludeNetwork, honor in getReport() #34400 - Both fix the same signal/excludeEnv key swap bug in constructProcessReportObject in BunProcess.cpp and add overlapping process.report tests

🤖 Generated with Claude Code

process.report.writeReport() was an echo stub: it returned its first
argument verbatim (even an Error object) and never wrote a file. Callers
checking the return value against the filename they passed would see
"success" while nothing was written.

writeReport() now serializes the getReport() object to JSON and writes it
to disk, matching Node.js:
- writeReport() generates report.YYYYMMDD.HHMMSS.<pid>.0.<seq>.json in
  the report directory (or cwd) and returns the filename
- writeReport(filename) writes to that filename and returns it
- writeReport(err) / writeReport(filename, err) accept an Error in either
  position; the return value is always the filename string
- filename "stdout"/"stderr" writes the report to that stream
- process.report.compact / directory / filename are honored
- on open failure, writes a diagnostic to stderr and returns ""
- validates the file argument as a string and err as an object with
  ERR_INVALID_ARG_TYPE

Also fixes process.report.signal, which was being written under the
"excludeEnv" key by mistake (so report.signal was undefined and
report.excludeEnv was "SIGUSR2").

Wiring reportOnUncaughtException / reportOnSignal and the --report-* CLI
flags to actually trigger reports is not included here.
@robobun
robobun force-pushed the claude/farm/ee2ae18e/process-report-writeReport branch from 38cc556 to accc60b Compare July 16, 2026 20:51
@robobun
robobun changed the base branch from main to claude/ccc1369c/process-report-exclude-env July 16, 2026 20:51
@robobun

robobun commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

Not a duplicate of #34400. That PR fixes the signal/excludeEnv property mix-up and makes getReport() honor excludeEnv/excludeNetwork; this one implements writeReport(), which was a stub that echoed its first argument and never wrote a file. The one-line signal key fix is the only overlap.

Rebased on #34400 and retargeted to its branch so they stack cleanly; writeReport() now also reads excludeEnv/excludeNetwork from process.report and passes them through to the report builder.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Process report output

Layer / File(s) Summary
Report generation and configuration
src/jsc/bindings/BunProcess.cpp
writeReport now builds and JSON-stringifies complete reports, generates default filenames, handles arguments and destinations, and sets the report signal configuration.
Output destinations and behavioral coverage
test/js/node/process/process.test.js
Subprocess tests cover filenames, errors, validation, configuration options, file failures, and stdout output.

Possibly related PRs

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main change: implementing process.report.writeReport().
Description check ✅ Passed The description covers the PR purpose and verification, with enough detail despite using different section headings than the template.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 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.

Inline comments:
In `@src/jsc/bindings/BunProcess.cpp`:
- Around line 2526-2549: Update constructReportObjectComplete and its callers to
accept and propagate the process.report.excludeEnv flag, then conditionally omit
the environmentVariables construction when the flag is enabled. Ensure the
report options parsing exposes the actual excludeEnv value rather than always
setting it to false.
- Line 2585: Update the Windows report construction around
constructReportObjectComplete so it receives and preserves the selected
filename, including values passed to writeReport such as "out.json", instead of
hardcoding header.filename to null.
- Around line 2520-2524: Update the report-generation flow around writeReport
and constructReportObjectComplete to pass the validated errArg through instead
of dropping it, using the existing object validation that rejects arrays. Ensure
constructJavaScriptStack derives javascriptStack from the supplied error, then
update the relevant test to parse the emitted JSON and assert javascriptStack
reflects that error rather than only the filename.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2cbc6948-1a06-44e1-a1f2-4197eb8989cf

📥 Commits

Reviewing files that changed from the base of the PR and between 2f5e810 and 38cc556.

📒 Files selected for processing (2)
  • src/jsc/bindings/BunProcess.cpp
  • test/js/node/process/process.test.js

Comment thread src/jsc/bindings/BunProcess.cpp
Comment thread src/jsc/bindings/BunProcess.cpp
Comment thread src/jsc/bindings/BunProcess.cpp Outdated
Extract a shared constructReportJavaScriptStack used by both POSIX and
Windows report builders. When an Error is passed to getReport(err) or
writeReport(filename, err), its .stack is split into javascriptStack.message
(first line) and javascriptStack.stack (remaining frames), matching Node.
When no err is passed, the synthetic ERR_SYNTHETIC callstack is used as
before.

Also threads the resolved filename into the Windows report builder so
header.filename is populated there too, and makes getReport(err) validate
err as an Object.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 src/jsc/bindings/BunProcess.cpp:2504-2510 — The overload dispatch uses arg0.isObject(), which in JSC is true for functions and arrays, so writeReport(() => {}) and writeReport([]) silently write a report file to cwd instead of throwing ERR_INVALID_ARG_TYPE like Node does. Gating with arg0.isObject() && !arg0.isCallable() and rejecting arrays in the err validation would match Node exactly.

    Extended reasoning...

    What diverges

    Node's writeReport([filename][, err]) overload dispatch in lib/internal/process/report.js is:

    if (typeof file === 'object' && file !== null) {
      err = file;
      file = undefined;
    } else if (file !== undefined) {
      validateString(file, 'file');
    }
    if (err !== undefined) validateObject(err, 'err');

    The two relevant JS-semantics facts: typeof (() => {}) is 'function' (not 'object'), and validateObject rejects arrays by default.

    This PR's dispatch at BunProcess.cpp:2504 uses arg0.isObject(). In JSC, JSValue::isObject() checks whether the cell's JSType >= ObjectType, and JSFunction is an object subtype — so it returns true for functions. The same is true for arrays. The subsequent err validation at line 2521 also uses errArg.isObject(), which again accepts both functions and arrays.

    Step-by-step: process.report.writeReport(() => {})

    1. Node: typeof file === 'object' → 'function' === 'object' → false. Falls through to validateString(file, 'file') with a function → throws ERR_INVALID_ARG_TYPE ("The "file" argument must be of type string").
    2. Bun (this PR): arg0.isObject() → true (functions are objects in JSC). errArg = arg0, fileArg = jsUndefined(). The fileArg.isUndefined() check skips string validation. The errArg.isObject() check at line 2521 passes. file is empty, so a default report.YYYYMMDD.…json filename is generated and a report file is written to cwd. Returns the filename string.

    Step-by-step: process.report.writeReport([])

    1. Node: typeof [] === 'object' → true, so err = []. Then validateObject(err, 'err') rejects arrays → throws ERR_INVALID_ARG_TYPE ("The "err" argument must be of type object").
    2. Bun (this PR): arg0.isObject() → true, errArg = []. errArg.isObject() at line 2521 → true, no throw. Writes a report file to cwd.

    Why nothing else catches it

    There is no downstream use of errArg that would reject a function or array — after validation, errArg is currently unused (the report doesn't yet incorporate the error's stack), so any object-typed value sails through and the side effect (file write) happens.

    Impact

    Minor Node-compat divergence: passing a function or array — clearly a programmer error — writes a diagnostic file to disk instead of throwing. Nothing crashes and the API is rarely used, but it does perform an observable filesystem side effect on invalid input where Node fails fast.

    Fix

    Mirror Node's checks exactly:

    if (arg0.isObject() && !arg0.isCallable()) {   // typeof === 'object' && !== null
        errArg = arg0;
        fileArg = jsUndefined();
    } else { … }

    and for the err validation, reject arrays and callables (matching validateObject's defaults):

    if (errArg.isNull() || !errArg.isObject() || errArg.isCallable() || JSC::isArray(globalObject, errArg)) {
        return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "err"_s, "Object"_s, errArg);
    }

    (or use the existing Bun::V::validateObject helper if one exists — the codebase already has the isObject() && !isCallable() pattern in bindings.cpp).

  • 🟡 src/jsc/bindings/BunProcess.cpp:2520-2524 — The err argument is parsed and type-validated here but never passed to constructReportObjectComplete (which has no error parameter), so writeReport(new Error('x')) produces a report identical to writeReport() — in Node the error's stack populates the report's javascriptStack section. This is the same pre-existing gap as getReport(err) at line 2489, so wiring it through means extending constructReportObjectComplete and is arguably out of scope; just flagging the parsed-but-unused local.

    Extended reasoning...

    What the bug is

    Process_functionWriteReport parses the optional err argument at lines 2503–2510 (treating an object first-arg as err per Node's overload rules) and validates it at lines 2520–2524 (rejecting non-object values with ERR_INVALID_ARG_TYPE). After that, errArg is never referenced again. The report is built at line 2585 via constructReportObjectComplete(vm, zigGlobal, file), whose signature is (VM&, Zig::GlobalObject*, String fileName) — there is no error parameter to thread it through even if the caller wanted to.

    Code path

    1. User calls process.report.writeReport(new Error('marker')).
    2. arg0.isObject() is true, so errArg = arg0 and fileArg = jsUndefined().
    3. errArg passes the !errArg.isUndefined() / errArg.isObject() validation.
    4. The default filename is generated, then constructReportObjectComplete(vm, zigGlobal, file) is called with only the filename.
    5. The report JSON is written to disk with no reference to the error's message or stack.

    The result is byte-identical to what writeReport() with no arguments would produce (modulo timestamp/seq).

    Why nothing else prevents it

    The underlying report builder has never supported an error parameter — getReport(err) at line 2489 has the identical gap (it doesn't even look at callFrame->argument(0)). So this PR inherits an infrastructure limitation rather than introducing a new one; the newly-added validation just makes the unused argument more visible.

    Impact

    In Node.js, process.report.writeReport(err) populates the report's javascriptStack section with err.stack so the diagnostic report captures the error that prompted it. Here that context is silently dropped. Nothing crashes and the report file is still valid — this is an incomplete-compat gap, not a correctness bug. The PR remains a strict improvement over the previous stub, which returned the Error object verbatim without writing anything.

    Step-by-step proof

    • Line 2504: if (arg0.isObject()) { errArg = arg0; ... } — errArg bound.
    • Lines 2520–2524: if (!errArg.isUndefined()) { if (errArg.isNull() || !errArg.isObject()) return ERR::INVALID_ARG_TYPE(...); } — validated, no other use.
    • grep -n errArg in the function body after line 2524 → no hits.
    • Line 2585: constructReportObjectComplete(vm, zigGlobal, file) — three args only.
    • constructReportObjectComplete declaration (~line 2098): (VM&, Zig::GlobalObject*, const String& fileName) — no error slot.

    The new test only asserts typeof f2 === 'string' and f2 !== err; it never checks that 'marker' appears in the written JSON, so this gap is untested.

    How to fix

    Extend constructReportObjectComplete to accept an optional JSValue err and, when present, populate the javascriptStack section from err.stack / err.message (matching Node's report.cc). Then pass errArg from both writeReport and getReport. That's a larger change than this PR's scope (converting a stub into a working file writer), so it's reasonable to defer — but the validated-but-unused local is worth a comment or a follow-up.

Comment thread src/jsc/bindings/BunProcess.cpp Outdated
Comment thread src/jsc/bindings/BunProcess.cpp
The overload dispatch now matches Node's typeof check: a callable first
argument routes to validateString (so writeReport(() => {}) throws on
"file"), and err validation goes through Bun::V::validateObject which
rejects arrays and callables (so writeReport([]) and getReport([]) throw
on "err"). Adds test cases for each.

On Windows the report file is now opened with _wfopen over the UTF-16
path instead of narrow fopen on UTF-8 bytes, so non-ASCII directories
and filenames resolve correctly.
@robobun

robobun commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed the review findings in 4d289cd:

  • arg0.isObject() accepting functions/arrays: dispatch now uses arg0.isObject() && !arg0.isCallable() so a function first-arg routes to validateString and throws on "file". err validation switched to Bun::V::validateObject, which rejects arrays and callables, so writeReport([]) / getReport([]) / getReport(() => {}) all throw ERR_INVALID_ARG_TYPE like Node. Test cases added.
  • errArg validated but dropped: already addressed in 339405d (stale against the earlier commit); errArg is threaded into constructReportJavaScriptStack for both writeReport and getReport.
  • Windows narrow fopen: switched to _wfopen over fullPath.wideCharacters() on Windows.
  • header.filename for "stdout": Node v26.3.0 emits "filename":"stdout" (not null), so the current behavior and test match Node; left as-is.

Comment thread src/jsc/bindings/BunProcess.cpp Outdated
Comment thread src/jsc/bindings/BunProcess.cpp
Comment thread src/jsc/bindings/BunProcess.cpp Outdated
Comment thread src/jsc/bindings/BunProcess.cpp Outdated
…ties

writeReport now reads compact/directory/filename/excludeEnv/excludeNetwork
from process.report (via processObject) rather than thisValue, so a
detached const {writeReport} = process.report; writeReport() still honors
the config, matching Node and matching getReport in this file.

header.trigger is threaded through constructReportObjectComplete and set
to "API" for writeReport() and "GetReport" for getReport(), matching
Node.

javascriptStack.errorProperties for a user-supplied error now contains
its own-enumerable string-keyed properties (excluding name/message/stack,
values stringified), matching Node's PrintJavaScriptErrorProperties.

Tests updated to cover trigger, errorProperties, and detached calls.
@robobun

robobun commented Jul 16, 2026 •

Copy link
Copy Markdown
Collaborator Author

CI on b07f3f9: all process.report.writeReport and getReport(err) tests pass on every lane. Remaining reds are unrelated to this diff:

  • test/cli/install/bun-create.test.ts (error.NPMIsDown, external registry outage across 4 lanes; already being handled separately)
  • test/js/node/process/process.test.js on Windows x64: the pre-existing process.memoryUsage.arrayBuffers assertion flaked once and passed on retry; none of the writeReport tests failed
  • bun-install-registry.test.ts, terminal-platform-gaps.test.ts, spawn.test.ts (flaky on Windows, passed on retry)

The writeReport implementation is ready for review; verified locally on linux-x64 and windows-x64 with all 13 report tests passing and all 9 writeReport tests fail-before/pass-after.

Comment thread src/jsc/bindings/BunProcess.cpp Outdated
Comment thread src/jsc/bindings/BunProcess.cpp Outdated
constructReportJavaScriptStack now uses a TopExceptionScope and swallows
exceptions from reading err.stack and from per-property get/toString on
errorProperties iteration, matching Node's per-property TryCatch. Index
keys and Symbol-valued properties are skipped (Node skips them too), and
termination exceptions are still propagated via
clearExceptionExceptTermination.

Also guard reportObjValue with an empty-value check before .isObject()
in writeReport, matching the sibling getReport.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any bugs, but this implements a new user-facing Node.js compat API (process.report.writeReport) with ~200 lines of new C++ — file I/O via raw fopen/_wfopen, argument-overload disambiguation, per-property exception swallowing via clearExceptionExceptTermination, and cross-platform path handling — so it's worth a human look.

What was reviewed:

  • constructReportJavaScriptStack hostile-err hardening: throwing getters, Symbol values, and index keys are handled and covered by tests; termination exceptions are correctly propagated rather than cleared.
  • writeReport overload routing (arg0.isObject() && !arg0.isCallable()) against the test matrix — functions route to file and arrays to err, both rejected by validateString/validateObject as tested.
  • Windows path shares the new constructReportJavaScriptStack and threads trigger/fileName/errValue through consistently.
Extended reasoning...

Overview

This PR implements process.report.writeReport() (previously a no-op stub returning its first argument) and extends process.report.getReport() to accept an err argument, matching Node.js. It touches src/jsc/bindings/BunProcess.cpp (+~250 lines), src/jsc/bindings/BunProcessReportObjectWindows.cpp (refactored to share the new constructReportJavaScriptStack helper and thread trigger/fileName/errValue through), and adds ~260 lines of tests in test/js/node/process/process.test.js.

The new code includes: a shared constructReportJavaScriptStack that either synthesizes a callstack or extracts .stack and own enumerable properties from a user-supplied error (swallowing per-property exceptions via clearExceptionExceptTermination to match Node's best-effort semantics); full writeReport argument disambiguation ([filename][, err]); reading of compact/excludeEnv/excludeNetwork/directory/filename from the live process.report object (receiver-agnostic — detached calls still honor config); auto-generated timestamped filenames with an atomic sequence counter; special-casing "stdout"/"stderr"; and file writing via fopen/_wfopen with stderr diagnostics on failure.

Security risks

Low but non-trivial. writeReport writes to a filesystem path constructed from user-controlled process.report.directory + filename, but this is by design (Node does the same) and requires the caller to already control JS execution. The hostile-err handling deliberately swallows exceptions from user getters/toString on the error object; termination exceptions are preserved via clearExceptionExceptTermination() rather than clearException(), which is the correct pattern. No injection or auth surface.

Level of scrutiny

High. This is a new user-facing Node-compat API with substantial native implementation surface: raw stdio file I/O (rather than bun.sys wrappers — though this is C++ bindings, not Rust), manual path-separator concatenation, a DECLARE_TOP_EXCEPTION_SCOPE with several clearExceptionExceptTermination sites (the repo's guidance flags clearException generally), getIfPropertyExists empty-value handling, and Windows-specific _wfopen/localtime_s/_getpid branches. API-shape decisions (return empty string on open failure, stderr banner text, header.trigger values "GetReport" vs "API", stringifying all errorProperties) should be verified against Node's actual behavior by a human familiar with the compat surface.

Other factors

Test coverage is thorough — auto-generated filename shape, arg validation matrix (numbers/functions/arrays/null for both file and err), directory/compact/filename config, open-failure path, stdout sink, err threading into javascriptStack, hostile err objects (index keys, Symbol values, throwing getters on both properties and .stack), and detached-call receiver-agnostic config. All tests spawn subprocesses with bunEnv and drain pipes concurrently. Given the size and the number of design/compat choices embedded here, this warrants a maintainer review rather than auto-approval.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant