feat: report $release_id from POSTHOG_RELEASE_ID - #239
Conversation
Report a `$release_id` on every event when `POSTHOG_RELEASE_ID` is set in the environment. This is the native, deploy-time counterpart to injecting `$release_id` into a web bundle: a build tool runs `posthog-cli release resolve` to create the release and print its id, launches the app with that id in `POSTHOG_RELEASE_ID`, and the SDK stamps it on every event, so the server resolves the exception's release by a direct id lookup — no release name or version has to match anything the app reports. The value is read once (cached), an unset or blank value changes nothing, and it is set in `apply_capture_defaults` before before_send, so a hook can still drop the property. No binary patching and no code signing, unlike the marker-injection alternative. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
posthog-rs-v0 Compliance ReportDate: 2026-08-27 15:21:15 UTC ✅ All Tests Passed!46/46 tests passed Capture Tests✅ 29/29 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
posthog-rs-v1 Compliance ReportDate: 2026-08-27 15:21:26 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
|
Scope the injected `$release_id` to `$exception` events. That is the only event where the server resolves a release from `$release_id`, so a pageview or a custom event does not need it. The insertion is split into a small `apply_release_id` helper gated on the event name, so the rule is unit-tested without the process-global `POSTHOG_RELEASE_ID` read. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add `--release-mode` to `symbol-sets upload` (env `POSTHOG_RELEASE_MODE`). The default `symbol-set` mode keeps binding the release to every uploaded symbol set. `event` uploads the symbol sets release-independent — bound to no release — so one symbol set serves every release of an unchanged binary, and the upload no longer needs `--release-name`/`--release-version`. In event mode the release rides the event as `$release_id`, which the SDK reports from `POSTHOG_RELEASE_ID` (posthog-rs 0.26+, PostHog/posthog-rs#239); the release is named with `posthog-cli release resolve`, whose id you pass to the app. No binary patching and no code signing, unlike the injected `--release-mode=event` variant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The runtime release-id section claimed posthog-rs and "other native SDKs" read POSTHOG_RELEASE_ID, with no version gate. Only posthog-rs 0.26+ reads it (PostHog/posthog-rs#239), and no other native SDK does today. State the version gate the CLI help text and changeset already carry, and drop the unverified multi-SDK claim. Generated-By: PostHog Desktop Task-Id: 81d53914-e1ab-4ecf-9740-da990f29d7dd
|
Reviews (1): Last reviewed commit: "feat: report $release_id only on $except..." | Re-trigger Greptile |
| pub(crate) fn release_id() -> Option<&'static str> { | ||
| static CACHE: OnceLock<Option<String>> = OnceLock::new(); | ||
| CACHE | ||
| .get_or_init(|| normalize(std::env::var(RELEASE_ID_ENV).ok())) | ||
| .as_deref() | ||
| } |
There was a problem hiding this comment.
should this be a proc macro? if we want to bake the env var into the binary at build time, this won't work
| use std::sync::OnceLock; | ||
|
|
||
| /// The environment variable the release id is read from. | ||
| const RELEASE_ID_ENV: &str = "POSTHOG_RELEASE_ID"; |
There was a problem hiding this comment.
just a thought - we could fall back to attempting to derive a release id from the git HEAD (git rev-parse --verify 'HEAD^{commit}')
expecting cases where git is not found or we're not running in a .git repo
…llback
Report $release_id from an explicit `release_id` client option in addition to
the POSTHOG_RELEASE_ID environment variable, so the release can be baked into
the binary at build time.
- New `ClientOptionsBuilder::release_id(...)`. Set it to
`option_env!("POSTHOG_RELEASE_ID")` to bake the id in at build time, so a
shipped binary self-identifies with nothing to set at runtime.
- The runtime env var remains the fallback when the option is unset, so a deploy
can still supply the release without a rebuild.
- Precedence: an explicit option wins over the environment
(release_env::resolve_release_id, unit-tested).
- Resolved once per capture into CaptureDefaults; still stamped only on
$exception events; a before_send hook can still drop it.
Updates the public-API snapshot and the changeset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…he env
resolve_release_id now trims the explicit option and treats a blank string as
unset, matching the environment path. This makes the ergonomic build-time
pattern `option_env!("POSTHOG_RELEASE_ID").unwrap_or_default()` safe: a build
that never set the variable passes an empty string, which now falls back to the
runtime POSTHOG_RELEASE_ID instead of stamping an empty $release_id.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
i think we should expose the apps metadata as config, so users would know what to set eg app name, version, build and then we infer the release id in the backend, or even a human readable release identifier eg release = 'myapp@v1.0.0+1' this PR only exposes release_id which is a UUID that requires the posthog-cli in advance |
|
Going to close this as I decided there is no point in migrating rust to the new release system. It always builds one chunk. Migration makes setup more complicated and gives no benefit |

Problem
A compiled Rust binary carries no release. A JavaScript build injects
$release_idinto its bundle, and a mobile app reads its version from the OS, but a Rust binary has neither.Change
Report a
$release_idon$exceptionevents, from either an explicitrelease_idclient option or thePOSTHOG_RELEASE_IDenvironment variable.posthog-cli release resolvecreates the release and prints its id; that id reaches the SDK one of two ways:release_idoption — typicallyoption_env!("POSTHOG_RELEASE_ID")— so the id is compiled into the binary. A shipped binary then self-identifies with nothing set at runtime. This survives code signing (the id is in the linked binary before you sign) and needs no deploy-time configuration.POSTHOG_RELEASE_IDfrom the environment at launch, so a deploy can supply the release without a rebuild.Precedence: an explicit option wins over the environment.
The server resolves each exception's release by a direct id lookup, so no release name or version has to match anything the app reports. Only
$exceptionevents carry the property — that is the only event the server resolves a release from. The value is resolved once per capture; an unset or blank value changes nothing; the property is set beforebefore_send, so a hook can still drop it. No binary patching and no code signing.Trade-off. Baking a different id per release changes the binary, so its debug id changes and each release gets its own symbol set (the runtime-env path keeps one binary and one symbol set across releases). Both are supported; pick per deployment — known-at-build-time favours baking, assigned-after-build favours the env var.
Contract: the release id is the UUID that
posthog-cli release resolveprints. The CLI side is PostHog/posthog#89834:symbol-sets upload --release-mode=eventuploads the symbols release-independent, andrelease resolve(already onmaster) names the release and prints its id.This is a second, lighter alternative to the marker-injection approach in #237. The two are independent branches off
main; pick one.How did you test this code?
Automated (
cargo test): unit tests for the value normalization (unset → none, blank → none, whitespace trimmed), the source precedence (explicit option beats the environment; environment used when the option is unset; none when neither is set), the event-name gate (a$release_idlands on an$exception, not on a$pageview, and not at all when unset), and that an explicitrelease_idoption reaches the resolved capture defaults.cargo fmt --checkandcargo clippyclean; the public-API snapshot is updated for the new setter.End to end against a local PostHog stack, run by the agent (Claude). The
rust-release-envexample — a plain posthog-rs app with no release code of its own — is built against this branch../runresolves the release, builds withPOSTHOG_RELEASE_IDset sooption_env!bakes the id into the binary, uploads the symbols release-independent, and runs the binary with the runtime env unset — so the reported id can only have come from the build. It is shipped twice (1.0.0, 2.0.0) to show the release tracks the build.posthog-cli — release resolve, then a release-independent
symbol-sets uploadof the baked buildthe app, run with
POSTHOG_RELEASE_IDunset — the id is already compiled inRead back server-side (a dev-login session over the local API): one issue holds both builds, each
$exception($lib = posthog-rs) carries its baked$release_id, cymbal resolved it into a$exception_release, and the in-app frames symbolicate to source off the uploaded symbol set:$release_idThe binary was run with nothing in its environment — the release id it reports was compiled in at build time. The same code shipped as 2.0.0 is a different baked id, resolving to a different release.
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Authored by Claude in Claude Code, directed by @ablaszkiewicz (DRI). This is the SDK half of a second, env-variable-based approach to native release tracking, offered alongside the marker-injection pair (#237 + PostHog/posthog#89117) for comparison. The
release_idclient option (build-time baking, env var as fallback) was added in response to review feedback on this PR. The end-to-end verification above was run by the agent against a local PostHog dev stack (ingestion + cymbal), with screenshots uploaded viahogli pr:upload-image. The example data is invented (rust-release-envdemo binary) and draws on no customer material.🤖 Generated with Claude Code