Skip to content

Add metrics capability spec - #30

Draft
dustinbyrne wants to merge 1 commit into
mainfrom
posthog-code/add-metrics-spec
Draft

Add metrics capability spec#30
dustinbyrne wants to merge 1 commit into
mainfrom
posthog-code/add-metrics-spec

Conversation

@dustinbyrne

Copy link
Copy Markdown
Contributor

Why

The SDK metrics pipeline shipped in two SDKs but was never specified — openspec/specs/ had logs, traces, capture, etc., but no metrics. 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 metrics capability (statsd-style counters, gauges, histograms → OTLP/HTTP metrics at POST {host}/i/v1/metrics), following the logs/traces product-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)
  • README + project.md — capability listed as a product-scope pipeline

openspec validate --specs --strict passes (60/60). No acceptance .feature files — matching the logs/traces precedent, 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-js @posthog/core is the reference implementation (packages/core/src/metrics/{index,metrics-utils,types,config}.ts, posthog-core-stateless.ts _sendMetricsBatch, shared types in packages/types/src/capture-metric.ts). posthog-node (src/client.ts, client.metrics), posthog-browser (src/posthog-metrics.ts, METRICS_ENDPOINT = '/i/v1/metrics'), and posthog-react-native are hosts of that one engine.
  • posthog-python (posthog/metrics_capture.py, PostHogMetrics; tests posthog/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."
  • Not implemented in posthog-go or posthog-rs — stated explicitly in the spec as their target.

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:

  1. Retry backoff + drop budget (the one that matters). posthog-python re-arms the retry timer with capped exponential backoff (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.
  2. gzip signalling. Python always gzips + 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.
  3. Opt-out gate. JS engine gates on isDisabled || optedOut; Python gates on client.disabled and honours send=False.
  4. beforeSend return validation. Python re-validates the returned mapping and re-checks the metric type (a hook can change it); JS trusts its typing.
  5. Config surface. camelCase flushIntervalMs (ms) vs snake_case flush_interval (seconds); Python adds runtime config hardening.
  6. Fork safety. Python resets its window on fork; no JS equivalent.

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's tasks.md.


Created with PostHog Code

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

1 participant