feat(node): POC runtime metrics autocapture behind a feature flag - #4537
Draft
jonmcwest wants to merge 2 commits into
Draft
feat(node): POC runtime metrics autocapture behind a feature flag#4537jonmcwest wants to merge 2 commits into
jonmcwest wants to merge 2 commits into
Conversation
Samples low-level Node runtime metrics (CPU time and utilization, memory and heap limit, event loop delay and utilization, GC pauses, uptime, active handles) into the existing `posthog.metrics` client on an interval, so a backend gets metrics from installing the SDK alone. Gated three ways: `enableMetricsAutocapture: true`/`false` decides explicitly, and when unset the `metrics-sdk-autocapture` feature flag decides. The flag is evaluated locally only (against the definitions the poller already caches) with `sendFeatureFlagEvents: false`, so the gate costs no request and no billable event, and re-evaluating it every 30s makes it a remote kill switch. The Node-only sampler lives in `runtime.node.ts` and is injected by the Node entrypoint, so the edge build stays free of `node:` built-ins. Generated-By: PostHog Desktop Task-Id: 09bd3be2-75b5-4371-8de3-127a2a874a4d
Contributor
posthog-node Compliance ReportDate: 2026-08-17 08:24:38 UTC ✅ All Tests Passed!111/111 tests passed Capture_V1 Tests✅ 94/94 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
Contributor
Contributor
|
Size Change: +28.7 kB (+0.15%) Total Size: 19.7 MB 📦 View Changed
ℹ️ View Unchanged
|
The `check:public-api` job failed because the rolling reference snapshot didn't include the two new `PostHogOptions` fields. `RuntimeMetricsSampler` was also surfacing in the public reference (with no properties, since it's a methods-only interface) because the client's protected factory returns it. It's an SDK-internal seam rather than something users implement, so it's marked `@internal` and the redundant re-export from the extension barrel is gone. Generated-By: PostHog Desktop Task-Id: 09bd3be2-75b5-4371-8de3-127a2a874a4d
Contributor
|
This PR hasn't seen activity in a week! Should it be merged, closed, or further worked on? If you want to keep it open, post a comment or remove the |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Backend SDK users get nothing for free on the metrics side today:
posthog.metricsexists, but every series has to be hand-instrumented. Meanwhile a Node process already knows its CPU time, memory, event loop delay and GC pauses — the same argument autocapture makes for events applies here. This is a proof of concept for "install the backend SDK, get baseline runtime metrics".Changes
A
MetricsAutocaptureextension that samples low-level Node runtime metrics into the existingposthog.metricsclient every 10s (configurable). Series emitted, all with closed-enum attributes only:process.cpu.timestate=user|systemprocess.cpu.utilizationprocess.memory.usagetype=rss|heap_used|heap_total|external|array_buffersprocess.memory.heap_limitprocess.event_loop.delaystat=mean|p50|p90|p99|maxprocess.event_loop.utilizationprocess.gc.durationkind=minor|major|incremental|weak_callbacksprocess.uptimeprocess.active_resourcesGating.
enableMetricsAutocapture: true/falsedecides explicitly. Left unset, themetrics-sdk-autocapturefeature flag decides — evaluated locally only against the definitions the poller already caches, withsendFeatureFlagEvents: false. So the gate costs no/flagsrequest and no billable$feature_flag_calledevent, and re-evaluating it every 30s makes it a remote kill switch. The trade-off is that it needssecretKeyand a locally-evaluable flag; without local evaluation it stays closed rather than adding an unasked-for request to every client. The flag is evaluated against a random per-process ID so a percentage rollout buckets processes, with$lib,$lib_version,service_nameandenvironmentas person properties for targeting.Node-only APIs (
node:perf_hooks,node:os,node:v8,node:fs) live inruntime.node.ts, injected by the Node entrypoint via an overridable factory — the same pattern asPostHogContextandgzip.node.ts— so the edge build stays free ofnode:built-ins (verified against the builtdist/entrypoints/index.edge.js). Sampling stops before the metrics flush on shutdown, and the event loop monitor and GC observer are torn down with it.Worth arguing about before this goes further
service.instance.idfixes the reading and multiplies series by fleet size — that's the cardinality/cost trade to settle first.secretKey. There is no separate "metrics enabled" switch in the stack, so an enabled gate means new recurring egress and new billable ingestion without a customer-side opt-in. Fine for dogfooding on our own projects; not obviously fine as a general rollout lever.process.gc.durationuses the default histogram bounds, which start at 5ms, so sub-ms scavenges all land in the first bucket — count/sum/max carry the signal, bucket percentiles don't.maxSeriesPerFlushbudget (default 1000).Release info Sub-libraries affected
Libraries affected
Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted) — requested by Jon McCallum in a Slack thread started by Tim Glaser about SDKs autocapturing metrics and traces the way we autocapture events. Please set the assignee to the requester; I don't have their GitHub handle.
Built with the PostHog Slack app (Claude Code). Notable decisions along the way:
evaluateFlags()+ a synthetic distinct ID, which turned out to fire a$feature_flag_calledevent per poll (billing the user for the SDK's own decision) and to issue a/flagsrequest from every client constructor — that broke ~100 existing tests asserting exact fetch counts, which was the tell. Switched togetFeatureFlagResult(..., { onlyEvaluateLocally: true, sendFeatureFlagEvents: false }): zero requests, zero events, and the client-levelenableLocalEvaluationcondition decides whether the gate can run at all. There's a regression test for "no/batch/call from the gate".os.cpus().lengthas the utilization denominator (wrong by 10–100x under a cgroup CPU limit — now reads the cgroup quota, with a pureparseCgroupCpuQuotacovered by tests);monitorEventLoopDelayreporting ~10ms on an idle process because Node records the full tick interval (now reports the excess over the resolution); a sampler cursor that could desync on a mid-sample throw and silently halve utilization forever; a failedsampler.start()leaking an enabled event loop monitor once per gate poll; hostname/pid-derived gate IDs hashing a whole fleet into one bucket; and sampling continuing to do full work while the client is opted out.Created with PostHog from a Slack thread