Skip to content

feat: route unhandled rejections through sendTelemetryError for richer error data#1940

Merged
mdesmet merged 2 commits intomasterfrom
feat/route-unhandled-rejection-through-telemetry-helper
May 8, 2026
Merged

feat: route unhandled rejections through sendTelemetryError for richer error data#1940
mdesmet merged 2 commits intomasterfrom
feat/route-unhandled-rejection-through-telemetry-helper

Conversation

@ralphstodomingo
Copy link
Copy Markdown
Contributor

@ralphstodomingo ralphstodomingo commented May 7, 2026

Summary

Production cluster unhandlederror with Channel closed (~45 unique machines on 0.61.x, mostly Win32) is opaque in App Insights even after the error.name / error.message / error.code visibility fix in #1928 — that fix only applies to events emitted via our sendTelemetryError helper. Uncaught promise rejections fire VS Code's built-in unhandlederror event from @vscode/extension-telemetry's baseTelemetrySender.sendErrorData, which only forwards name / message / stack and drops error.code.

error.code is the field that distinguishes IPC failure modes (ERR_IPC_CHANNEL_CLOSED, EPIPE, EBADF, ECONNRESET, ...). Without it, every Channel closed event looks identical in App Insights regardless of root cause — making it impossible to triage which subsystem the IPC channel was attached to.

What this PR does

Adds a process.on('unhandledRejection') listener in DBTPowerUserExtension.activate(context) that routes the rejection reason through this.telemetry.sendTelemetryError("unhandledRejection", reason). The helper already handles error_name / error_message / error_code extraction (telemetry/index.ts:128-133) plus the standard dbtIntegrationMode / instanceName / localMode envelope.

const onUnhandledRejection = (reason: unknown) => {
  try {
    this.telemetry.sendTelemetryError("unhandledRejection", reason);
  } catch {
    // Telemetry failures must never re-enter the rejection path.
  }
};
process.on("unhandledRejection", onUnhandledRejection);
context.subscriptions.push({
  dispose: () => process.off("unhandledRejection", onUnhandledRejection),
});

Three deliberate properties:

  1. Distinct event name. unhandledRejection (camelCase) vs upstream unhandlederror (lowercase) — both events stream to App Insights separately, so the new one is queryable independently and the upstream stream stays untouched. Lets us deprecate using unhandlederror later without breaking existing dashboards.
  2. Disposable cleanup. Registered against context.subscriptions so the listener unhooks on extension deactivation. Without this, handlers accumulate one-per-reactivation across the lifetime of a VS Code window.
  3. Try/catch around the telemetry call. A throwing telemetry pipeline must not re-enter the rejection path — that would surface as another unhandled rejection, caught by our handler, throwing again, infinite loop.

What this enables

The next telemetry rotation will give us error_code on cluster unhandledRejection, which means we can split "Channel closed" into:

Today these are all anonymous Error: Channel closed rows. Post-fix they become individually triageable.

Test plan

Adds src/test/suite/unhandledRejectionRouting.test.ts with four cases:

PASS src/test/suite/unhandledRejectionRouting.test.ts
  unhandledRejection routing
    ✓ forwards an uncaught Promise rejection through sendTelemetryError (3 ms)
    ✓ captures error.code via sendTelemetryError's existing extractErrorFields
    ✓ dispose unhooks the listener so handlers don't accumulate across reactivations
    ✓ a throwing telemetry call does not re-enter the rejection path
  • npx jest — 32 of 33 suites pass; only failure is the pre-existing jupyterlabServicesCompat environmental issue (missing build artifact) confirmed unrelated by stashing this PR's changes
  • No public API surface change
  • Listener registered as context.subscriptions disposable — verified by test 3 (no calls after dispose)
  • Tests drive process.emit directly because Jest's own unhandledRejection listener for failure reporting interferes with letting Promise.reject bubble naturally — emitting the event is identical from the listener's perspective

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Improvements

    • Capture unhandled promise rejections and route them to telemetry for improved diagnostics.
    • Ensure telemetry failures are safely contained so they don’t trigger additional unhandled-rejection issues.
  • Tests

    • Added tests validating routing of unhandled rejections to telemetry, preservation of error payload details, and proper listener cleanup and disposal.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 7, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d47c74bd-7675-49cf-88ab-4972dcad01c7

📥 Commits

Reviewing files that changed from the base of the PR and between 5aa0a9a and 0c4e084.

📒 Files selected for processing (2)
  • src/dbtPowerUserExtension.ts
  • src/test/suite/unhandledRejectionRouting.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/dbtPowerUserExtension.ts
  • src/test/suite/unhandledRejectionRouting.test.ts

Walkthrough

Adds a process 'unhandledRejection' listener during extension activation that forwards the rejection reason to TelemetryService.sendTelemetryError("catchAllError") (telemetry errors are caught) and unregisters the listener on disposal. Tests verify routing, error property access, cleanup, and resilience when telemetry throws.

Changes

Unhandled Rejection Telemetry Routing

Layer / File(s) Summary
Extension Activation Implementation
src/dbtPowerUserExtension.ts
Registers process.on('unhandledRejection') listener in activate that forwards rejection reason to TelemetryService.sendTelemetryError("catchAllError", reason) wrapped in try/catch, and registers listener removal via context.subscriptions.
Test Helpers & Setup
src/test/suite/unhandledRejectionRouting.test.ts
Defines test helper attachUnhandledRejectionRouting and test afterEach cleanup to dispose tracked subscriptions and reset context.
Telemetry Routing Verification
src/test/suite/unhandledRejectionRouting.test.ts
Tests that emitting unhandledRejection invokes sendTelemetryError exactly once with event name "catchAllError" and the same error object.
Error Metadata Preservation
src/test/suite/unhandledRejectionRouting.test.ts
Verifies that error properties (e.g., error.code) remain accessible on the error argument passed to sendTelemetryError.
Listener Cleanup
src/test/suite/unhandledRejectionRouting.test.ts
Confirms that disposing the registered handler stops subsequent unhandledRejection emissions from triggering telemetry.
Error Handling Resilience
src/test/suite/unhandledRejectionRouting.test.ts
Ensures that if sendTelemetryError throws, process.emit("unhandledRejection", ...) does not throw while telemetry is still invoked once.

Sequence Diagram(s)

sequenceDiagram
  participant Process
  participant Extension
  participant Telemetry

  Process->>Extension: emit 'unhandledRejection' (reason)
  Extension->>Telemetry: sendTelemetryError("catchAllError", reason)
  Telemetry-->>Extension: returns or throws (caught)
  Extension->>Process: process.off(handler) on dispose
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • mdesmet
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: routing unhandled rejections through sendTelemetryError to capture richer error data including error_code.
Description check ✅ Passed The description is comprehensive and well-structured, covering the problem, solution, implementation details, test plan, and rationale. However, it deviates from the template by combining sections and omitting the explicit checklist.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/route-unhandled-rejection-through-telemetry-helper

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

Bundle Size Report

darwin-arm64: 74.2 MB
Category Size Compressed Files
Webview JS bundles 36.3 MB 12.3 MB 346
Native: altimate-core 35.1 MB 14.0 MB 1
Media assets 29.6 MB 25.8 MB 91
Native: zeromq 20.5 MB 8.2 MB 15
Webview images 15.3 MB 12.2 MB 18
Extension backend (JS) 2.7 MB 0.6 MB 1
Python packages 2.0 MB 0.5 MB 95
Native: other node_modules 1.0 MB 0.2 MB 139
Webview CSS 0.8 MB 0.1 MB 2
Webview other 0.5 MB 0.1 MB 5
Other 0.1 MB 26 KB 15
Total 143.9 MB 74.0 MB 728
linux-x64: 75.9 MB
Category Size Compressed Files
Native: altimate-core 41.8 MB 15.1 MB 1
Webview JS bundles 36.3 MB 12.3 MB 346
Media assets 29.6 MB 25.8 MB 91
Native: zeromq 21.9 MB 8.7 MB 16
Webview images 15.3 MB 12.2 MB 18
Extension backend (JS) 2.7 MB 0.6 MB 1
Python packages 2.0 MB 0.5 MB 95
Native: other node_modules 1.0 MB 0.2 MB 139
Webview CSS 0.8 MB 0.1 MB 2
Webview other 0.5 MB 0.1 MB 5
Other 0.1 MB 26 KB 15
Total 152.0 MB 75.7 MB 729
win32-x64: 76.8 MB
Category Size Compressed Files
Native: altimate-core 50.3 MB 16.2 MB 1
Webview JS bundles 36.3 MB 12.3 MB 346
Media assets 29.6 MB 25.8 MB 91
Native: zeromq 20.0 MB 8.1 MB 15
Webview images 15.3 MB 12.2 MB 18
Extension backend (JS) 2.7 MB 0.6 MB 1
Native: other node_modules 2.3 MB 0.7 MB 147
Python packages 2.0 MB 0.5 MB 95
Webview CSS 0.8 MB 0.1 MB 2
Webview other 0.5 MB 0.1 MB 5
Other 0.1 MB 26 KB 15
Total 159.8 MB 76.7 MB 736

@ralphstodomingo ralphstodomingo self-assigned this May 7, 2026
@ralphstodomingo ralphstodomingo marked this pull request as ready for review May 7, 2026 19:16
@ralphstodomingo ralphstodomingo requested a review from mdesmet May 7, 2026 19:16
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/test/suite/unhandledRejectionRouting.test.ts`:
- Around line 45-123: Tests leak process 'unhandledRejection' listeners because
each test disposes context.subscriptions at the end of the test body, which
won't run if an assertion fails; move the cleanup into a shared afterEach so it
always runs: add an afterEach that iterates over context.subscriptions (or
stores/cleans attachments returned by attachUnhandledRejectionRouting) and calls
dispose() (or removes the process listener) to unhook the listener created by
attachUnhandledRejectionRouting; reference the existing
FakeContext/context.subscriptions and attachUnhandledRejectionRouting to locate
where to perform this guaranteed teardown.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 065e8916-dcf3-4c78-ba25-e9a1c5186e8e

📥 Commits

Reviewing files that changed from the base of the PR and between 68ef665 and 6b7f7bc.

📒 Files selected for processing (2)
  • src/dbtPowerUserExtension.ts
  • src/test/suite/unhandledRejectionRouting.test.ts

Comment thread src/test/suite/unhandledRejectionRouting.test.ts
…r error data

Production cluster `unhandlederror` with `Channel closed` (~45 unique
machines on 0.61.x, mostly Win32) was opaque even after the
`error.name`/`error.message`/`error.code` visibility fix in #1928 — that
fix only applies to events emitted via our `sendTelemetryError` helper.
Uncaught promise rejections fire VS Code's built-in `unhandlederror`
event from `@vscode/extension-telemetry`'s
`baseTelemetrySender.sendErrorData`, which only forwards `name` /
`message` / `stack` and **drops `error.code`**. That's the field that
distinguishes IPC failure modes (`ERR_IPC_CHANNEL_CLOSED`, `EPIPE`,
`EBADF`, `ECONNRESET`, ...) — without it, every `Channel closed` event
looks identical in App Insights regardless of root cause.

Add a `process.on('unhandledRejection')` listener in
`DBTPowerUserExtension.activate(context)` that routes the rejection
reason through `this.telemetry.sendTelemetryError("unhandledRejection",
reason)`. The helper already handles `error_name` / `error_message` /
`error_code` extraction (`telemetry/index.ts:128-133`) plus the standard
`dbtIntegrationMode` / `instanceName` / `localMode` envelope. The new
event has a distinct name (`unhandledRejection`, camelCase) from VS
Code's upstream `unhandlederror` (lowercase) so the two streams stay
queryable separately in App Insights — both will fire for each uncaught
rejection going forward, but the new one is enriched.

Listener is registered as a `context.subscriptions` disposable so it
unhooks on extension deactivation, preventing handler accumulation
across reload cycles. Telemetry failures inside the listener are
swallowed (try/catch) so a broken telemetry pipeline can't recursively
re-enter the rejection path.

Adds `src/test/suite/unhandledRejectionRouting.test.ts` with four cases
covering (1) the routing happy path, (2) `error.code` propagation,
(3) dispose unhooks the listener, and (4) a throwing telemetry stub
doesn't re-enter the rejection path. Tests drive `process.emit` directly
because Jest registers its own `unhandledRejection` listener for failure
reporting, which interferes with letting a real `Promise.reject` bubble
naturally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ralphstodomingo ralphstodomingo force-pushed the feat/route-unhandled-rejection-through-telemetry-helper branch from 6b7f7bc to 5aa0a9a Compare May 8, 2026 02:58
Comment thread src/dbtPowerUserExtension.ts Outdated
Address review feedback from @mdesmet on PR #1940. Aligns with the
project-wide `-Error` suffix convention for `sendTelemetryError`'s
first argument (e.g. `installDepsError`, `compileNodePythonError`,
`extensionActivationError`). Updates the matching assertions in
`unhandledRejectionRouting.test.ts`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dev-punia-altimate
Copy link
Copy Markdown
Contributor

✅ Tests — All Passed

cc @ralphstodomingo

@ralphstodomingo ralphstodomingo requested a review from mdesmet May 8, 2026 08:21
@mdesmet mdesmet merged commit 166800c into master May 8, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants