Add metrics capability spec - #30
Draft
dustinbyrne wants to merge 1 commit into
Draft
Conversation
Backfill a platform-agnostic `metrics` capability spec from the two shipped implementations (posthog-js `@posthog/core` engine + browser/node/react-native hosts, and the posthog-python mirror). Adds the canonical spec plus the archived OpenSpec change, and lists the pipeline in README + project.md. Operational divergences between the two implementations are named on their requirements for human decision rather than silently resolved. Generated-By: PostHog Code Task-Id: 4c6ba7cb-0447-44de-8918-d09a3d744b1e
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.
Why
The SDK metrics pipeline shipped in two SDKs but was never specified —
openspec/specs/hadlogs,traces,capture, etc., but nometrics. This backfills the platform-agnostic contract so the next SDK to add metrics has a target and the two existing implementations can't drift silently on the wire.What
Adds a new
metricscapability (statsd-style counters, gauges, histograms → OTLP/HTTP metrics atPOST {host}/i/v1/metrics), following thelogs/tracesproduct-pipeline precedent:openspec/specs/metrics/spec.md— canonical spec (24 requirements, 62 scenarios)openspec/changes/archive/2026-08-06-add-metrics/— the archived OpenSpec change (proposal / design / tasks / delta)project.md— capability listed as aproduct-scope pipelineopenspec validate --specs --strictpasses (60/60). No acceptance.featurefiles — matching thelogs/tracesprecedent, product pipelines don't get them (those dirs cover public/private SDK API behavior).Treated as one paired capability, not separate ports
Per the source, this is one engine with host surfaces plus one deliberate mirror, not three-plus independent ports:
@posthog/coreis the reference implementation (packages/core/src/metrics/{index,metrics-utils,types,config}.ts,posthog-core-stateless.ts_sendMetricsBatch, shared types inpackages/types/src/capture-metric.ts).posthog-node(src/client.ts,client.metrics),posthog-browser(src/posthog-metrics.ts,METRICS_ENDPOINT = '/i/v1/metrics'), andposthog-react-nativeare hosts of that one engine.posthog/metrics_capture.py,PostHogMetrics; testsposthog/test/test_metrics.py) is a documented mirror — its module docstring states it "Mirrors the posthog-js core implementation so every SDK speaks the same wire shape."Implementation files the spec was derived from
posthog-js:
packages/core/src/metrics/index.ts,metrics-utils.ts,types.ts,config.ts;packages/core/src/posthog-core-stateless.ts;packages/core/src/logs/logs-utils.ts(shared AnyValue encoder);packages/types/src/capture-metric.ts;packages/browser/src/posthog-metrics.ts;packages/node/src/client.ts.posthog-python:
posthog/metrics_capture.py,posthog/client.py,posthog/test/test_metrics.py.Divergences flagged for human decision (not silently resolved)
The two implementations agree on the wire shape but diverge on operational policy. Each is called out on its requirement:
interval × min(2^(failures-1), 64)) and drops the buffered window after 8 consecutive failed flushes. The posthog-js metrics engine does neither — it re-arms at the base interval and delegates transport backoff to the host request layer (fetchWithRetry/ browser retry queue), with no engine-level drop budget. The spec names Python's policy canonical and flags the JS gap as the open decision.Content-Encoding: gzip; JS core does too but allows a raw-JSON fallback when compression is disabled; the browser host signals gzip via a?compression=query param. Same server outcome, three code paths.isDisabled || optedOut; Python gates onclient.disabledand honourssend=False.beforeSendreturn validation. Python re-validates the returned mapping and re-checks the metric type (a hook can change it); JS trusts its typing.flushIntervalMs(ms) vs snake_caseflush_interval(seconds); Python adds runtime config hardening.Both agree on: endpoint +
?token=, one data point per series per flush window, delta temporality, the OTel-default histogram bounds, series-key canonicalization, the OTLP AnyValue rules (NaN/Infinity → proto3 strings, integral floats →intValue), and dropping the window on 413 (no batch-splitting, unlike logs).Not done
Draft, per request — no SDK code touched, no other spec modified, not merged. Follow-ups (resolve the retry divergence with the client-libraries team;
add-metrics-go/-rs; golden wire fixtures) are listed in the change'stasks.md.Created with PostHog Code