Repository navigation
feat: self-instrument with the ClickStack browser and Node SDKs - #3575
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 8739f80 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Duplicate code analysisNo new duplicate code introduced by this PR. jscpd compared this branch against |
E2E Test Results✅ All tests passed • 463 passed • 1 skipped • 1389s
Tests ran across 4 shards in parallel. |
🔴 Tier 4 — CriticalTouches authentication, tenancy data models, the public API or shipped database config — or substantially changes the query rendering engine, background tasks, the OTel pipeline, image build, or release CI. Why this tier:
Review process: Deep review from a domain expert. Synchronous walkthrough may be required. Stats
|
| ), | ||
| processors: [ | ||
| new BatchLogRecordProcessor({ | ||
| exporter: new OTLPLogExporter({ |
There was a problem hiding this comment.
🟠 major — Usage-stats exporter still sends the operator's OTLP headers to in-otel.hyperdx.io
OTLPLogExporter merges OTEL_EXPORTER_OTLP_HEADERS and OTEL_EXPORTER_OTLP_LOGS_HEADERS into the explicit headers. The explicit authorization replaces only that one key. Any other header the operator set for their own collector (for example x-honeycomb-team=<secret>, x-scope-orgid, or a vendor API-key header) is still sent to HyperDX's usage endpoint. The old SDK did the same, but this PR adds a dedicated exporter precisely to keep this path isolated. Fix it by not using the env-aware exporter: serialize the record with @opentelemetry/otlp-transformer (JsonLogsSerializer) and POST it with only the usage-stats key. The test mocks OTLPLogExporter, so it can't see the env merge. Add a test with OTEL_EXPORTER_OTLP_HEADERS set that checks the outgoing request headers.
Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond
| // process.exit() skips the SDK's exit-time flush, so a one-shot task would | ||
| // drop its own span and duration metric. Before init shutdown() resolves at | ||
| // once. | ||
| const exitAfterShutdown = (code: number) => { |
There was a problem hiding this comment.
🔵 minor — exitAfterShutdown isn't run-once or time-bounded, and the in-app cron keeps ticking while it waits
Before, process.exit ran right away. Now an uncaughtException or unhandledRejection leaves the process running until shutdown() settles, which can be up to the exporter/BSP timeouts against a black-holed collector. Meanwhile the CronJob started at line 80 can fire more ticks, and every further exception or rejection starts another shutdown(). A late rejection can also race exitAfterShutdown(0) from the one-shot path. Fix: guard with a module-level exiting flag, stop the cron job, and Promise.race the shutdown against a short timeout (for example 5 s) before calling process.exit.
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
| // forceExit calls process.exit(), which skips the SDK's own | ||
| // exit-time flush. Before init (no key set) this resolves at | ||
| // once. | ||
| await shutdownTelemetry(); |
There was a problem hiding this comment.
🔵 minor — SDK shutdown runs once per HTTP server, so the first server to finish stops telemetry while the other is still draining
gracefulShutdown is registered separately for appServer and opampServer (line 75), and each onShutdown now awaits shutdownTelemetry(). Whichever finishes first shuts down the SDK while the other server may still be draining requests (up to the 10 s timeout). Those requests' spans and logs are dropped, along with that server's own 'Server gracefully shut down...' line. Fix: shut telemetry down once, after both servers' shutdowns settle, for example with a shared promise built from Promise.allSettled over both, or a run-once wrapper.
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
PR Review
2 finding(s): 🔴 0 critical · 🟠 0 major · 🔵 2 minor 1 posted as inline comment(s) on the changed lines. 1 listed below. Findings outside the changed lines1 minor
Severity is the reviewer's own estimate and is used for ordering, not filtering. No finding blocks merge automatically; a maintainer decides. |
Deep Review✅ No critical issues found. This SDK-migration PR ( 🟡 P2 — recommended
🔵 P3 nitpicks (8)
Reviewers (13): correctness, security, adversarial, reliability, testing, maintainability, kieran-typescript, frontend-races, project-standards, performance, previous-comments, learnings-researcher, agent-native. Testing gaps:
Pre-existing (not introduced, not counted): the hardcoded usage-stats ingestion key in No finding blocks merge automatically. A maintainer will expect P0/P1 findings in code this PR changes to be fixed; P2/P3 are your call -- fix or reply. Never fix findings about surrounding code here; reply instead. Do not widen the PR. How to respond |
| // telemetry (it hides the browser key in /api/config) never got Next server | ||
| // telemetry either, because production builds did not load | ||
| // instrumentation.ts before. | ||
| if (!HDX_EXPORTER_ENABLED) { |
There was a problem hiding this comment.
🔵 minor — HDX_EXPORTER_ENABLED=false stops only the Next server, so the API and alert task keep exporting
The shared decideSelfTelemetry is described as the one place that decides whether the SDK starts, but the HDX_EXPORTER_ENABLED check sits outside it, in the Next-only path. In the single-container hyperdx/all-in-one image (docker/hyperdx/entry.prod.sh), setting HDX_EXPORTER_ENABLED=false together with HYPERDX_API_KEY turns off browser and Next telemetry, but packages/api/src/telemetry.ts still starts the SDK for the API and check-alerts. Fix: add an HDX_EXPORTER_ENABLED field to SelfTelemetryEnv and return { start: false } for it inside decideSelfTelemetry, so both entry points share one gate. If the API is meant to ignore the flag, say that explicitly in the changeset.
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
Since ClickHouse#3575 the API installs systeminformation through @opentelemetry/instrumentation-host-metrics, not @opentelemetry/host-metrics. That package asks for ^5.31.6, but the old ^5.24.0 pin held it at 5.30.7.
## Summary The root `resolutions` block pins `systeminformation` to `^5.24.0`, and `yarn.lock` resolves it to 5.30.7. `@hyperdx/api` installs it through `@clickhouse/clickstack-node` → `@opentelemetry/auto-instrumentations-node` → `@opentelemetry/instrumentation-host-metrics` 0.6.0 (since #3575), so it is in the API image. That package asks for `^5.31.6`, but the old pin holds it at 5.30.7. `npm audit` reports 7 advisories for 5.30.7, 5 of them high. Two of them are the command injections in `networkInterfaces()` on Linux from the issue: GHSA-5xpp-75jx-m839 (fixed in 5.31.7) and GHSA-hvx9-hwr7-wjj9 (fixed in 5.31.6). - The resolution is now `^5.33.19`, the newest 5.x release. It fixes all 7 advisories. The last fix (GHSA-3f7j-7jwc-fxfj, Windows) is in 5.33.16. - `yarn.lock` changes only the `systeminformation` entry, from 5.30.7 to 5.33.19. - A changeset bumps `@hyperdx/api` at `patch`. Things to be aware of: - I did not find a path from HyperDX code to the vulnerable functions. `@opentelemetry/auto-instrumentations-node` leaves host metrics off by default (`defaultExcludedInstrumentations`). `packages/api/src/telemetry.ts` turns them on only when `NODE_ENV=development` and an API key is set, and the Docker images set `NODE_ENV=production`. An operator can still turn them on with `OTEL_NODE_ENABLED_INSTRUMENTATIONS`. The instrumentation calls `networkStats()` with no argument. On Linux, that reads only the default interface (`ip route`, then `/sys/class/net/<iface>/...`) and does not call `networkInterfaces()`. It calls it only on Windows, or for the `'*'` argument. I measured this on Linux (see Tests). So this change removes a vulnerable version from the image and from scanner reports. I could not show an exploit through HyperDX. - #1803 raised this resolution to `^5.31.0` in March. It was stacked on #1740 and merged into the #1740 branch after #1740 had landed, so the bump never reached `main`. - 5.33.19 was published on 2026-10-30, so it passes the repo's 7-day `npmMinimalAgeGate`. - The systeminformation changelog from 5.30.7 to 5.33.19 lists fixes and hardening, and no API removals. ### Tests `yarn install` with the repo's Yarn (4.13.0) changes only the `systeminformation` entry in `yarn.lock`. `yarn why systeminformation` shows `5.30.7 (via npm:^5.24.0)` on `main` and `5.33.19 (via npm:^5.33.19)` on this branch, both under `@opentelemetry/instrumentation-host-metrics@npm:0.6.0` (before #3575: `@opentelemetry/host-metrics@npm:0.35.5`). `node_modules` has one copy, 5.33.19. The API starts with the new version. On this branch after #3575, I started `packages/api/src/index.ts` with `NODE_ENV=development` and the `src/telemetry.ts` preload (the `yarn dev` command, without nodemon and dotenv), a dummy `HYPERDX_API_KEY`, and the OTLP exports sent to a local HTTP listener. The first metrics export had `system.cpu.time`, `system.memory.usage`, `system.network.io` and `system.network.errors`. The log had no uncaught errors or unhandled rejections. Before #3575 I ran the same check with `@opentelemetry/host-metrics` 0.35.5, with the same result. For the before/after checks I used two small installs with the pre-#3575 `yarn.lock` versions of `@opentelemetry/host-metrics` (0.35.5), `@opentelemetry/sdk-metrics` (1.30.1) and `@opentelemetry/api` (1.9.0). The `systeminformation` call in `@opentelemetry/instrumentation-host-metrics` 0.6.0 (`build/src/stats/si.js`) is the same code as in `host-metrics` 0.35.5, so these results apply to the current dependent too. An npm override sets `systeminformation` to 5.30.7 (`main`) or 5.33.19 (this branch). The Linux runs use `node:22.23.4-alpine`, the base of the API image, in Docker (arm64). `HostMetrics` runs as in `packages/api/src/index.ts`, with an in-memory reader, and is collected twice. | Check | `main` (5.30.7) | This branch (5.33.19) | | :-- | :-- | :-- | | `npm audit`, `systeminformation` advisories | 7 (5 high, 1 moderate, 1 low) | 0 | | Linux: `networkInterfaces()` with `source /tmp/x;touch${IFS}/tmp/pwned-interfaces` in `/etc/network/interfaces` (GHSA-5xpp-75jx-m839) | runs `touch` | does not run it | | Linux: `HostMetrics` with the same file | 10 metrics, 0 errors, `system.network.io` for `eth0`, `touch` not run | same | | macOS: `HostMetrics` | 10 metrics, 0 errors | same | | Result fields of `networkInterfaces()` and `networkStats()` (Linux and macOS) | | same as `main` | I also logged every child process. On Linux, on both versions, `HostMetrics` runs only `ip route 2> /dev/null | grep default` and `cat /sys/class/net/eth0/...`. Prettier passes on `package.json` and the changeset. Not run: Windows, the Docker image build, ESLint, `tsc` and `knip` (no source file or workspace dependency changed). ### How to reproduce **Starting point:** a clone of this repo. Use the repo's Yarn (4.13.0). **On `main`:** 1. Run `yarn why systeminformation`. It shows `systeminformation@npm:5.30.7 (via npm:^5.24.0)`. 5.30.7 is in the vulnerable range of GHSA-5xpp-75jx-m839 (`<= 5.31.6`) and GHSA-hvx9-hwr7-wjj9 (`>= 4.17.0, <= 5.31.5`). 2. To see the injection itself: in a `node:22-alpine` container with `systeminformation@5.30.7` installed, write `source /tmp/x;touch${IFS}/tmp/pwned-interfaces` to `/etc/network/interfaces`, then run `node -e "require('systeminformation/lib/network').networkInterfaces()"`. `/tmp/pwned-interfaces` is created. **On this branch:** 1. Run `yarn why systeminformation`. It shows `systeminformation@npm:5.33.19 (via npm:^5.33.19)`. 2. Repeat step 2 with `systeminformation@5.33.19`. `/tmp/pwned-interfaces` is not created. ### How to test on Vercel preview N/A — non-UI change. ### References - Closes #3306 - Related: #1740 (added the resolution), #1803 (its `^5.31.0` bump did not reach `main`)
Closes HDX-5951
Summary
HyperDX now instruments itself with the new ClickStack SDKs (
@clickhouse/clickstack-browser,@clickhouse/clickstack-node), the same path users take when they migrate from@hyperdx/browserand@hyperdx/node-opentelemetry. Existing env vars keep working:HYPERDX_API_KEY, per-signal browser endpoints andNEXT_PUBLIC_OTEL_RESOURCE_ATTRIBUTES.Changes
packages/api/src/telemetry.ts,bin/hyperdx,utils/logger.ts,tasks/usageStats.ts): a-rpreload replaces the old one and still starts nothing without a key. Pino and Express come from the bundled instrumentations. Usage stats get their own exporter, so HyperDX's ingestion key never reaches them.packages/app/instrumentation-client.ts,AppNav):setUserandsetAttributes({ teamName })replacesetGlobalAttributes. Custom actions staycustom-actionspans for the Sessions page, and logout starts a new session.packages/app/instrumentation.ts, Dockerfiles): moved to the package root so production builds load it, which they never did before. It reports as<service>-nextand skips static assets.HDX_EXPORTER_ENABLED=falseturns it off.packages/common-utils/src/selfTelemetry.ts): one tested helper decides whether the API and the Next server start the SDK.Screenshots
Example self-instrumented data on ClickStack SDK
