feat(cli): default release mode to event - #91823
Conversation
Uploaded symbol sets, source maps and mappings are now release-independent by default, and each exception resolves its own release. Pass --release-mode symbol-set to keep binding the release to the upload. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jg6r6rFUBnowrXssZyU7yR
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
🤖 CI report✅ Trunk lane — non-backend laneThis PR is assigned to the non-backend lane. It does not run backend Python tests and may merge in parallel with PRs in other lanes. |
🦔 PostHog Review reviewed this pull requestFound 2 must fix, 4 should fix, 1 consider. Published 7 findings (view the review). |
|
PostHog Review alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏 |
| /// How the release is associated with exceptions. `event` (the default) leaves symbol sets | ||
| /// unbound; the chunks already carry the release id in their injected snippet, so the release | ||
| /// is resolved per event rather than per symbol set. `symbol-set` stamps the release id onto | ||
| /// the uploaded symbol sets instead. Also settable via `POSTHOG_RELEASE_MODE`. | ||
| #[arg( | ||
| long, | ||
| env = "POSTHOG_RELEASE_MODE", | ||
| value_enum, | ||
| default_value = "symbol-set" | ||
| default_value = "event" |
There was a problem hiding this comment.
Event uploads can keep source maps with wrong offsets
Why we think it's a valid issue
- Checked:
into_uploadincli/src/sourcemaps/source_pairs.rs, the snippet templates incli/src/sourcemaps/constant.rs,set_chunk_id/remove_chunk_idincli/src/sourcemaps/content.rs, the server's skip-or-overwrite branch inproducts/error_tracking/backend/logic/symbol_sets.py, and the event-mode hash tests incli/tests/sourcemap.rs. - Found: the payload and the hash are taken from different states of the pair.
cli/src/sourcemaps/source_pairs.rs:165-166capturessource_contentandsourcemap_contentfordatawhile the injection is still present, and:170strips it only afterwards to compute the hash. Sodatacarries the snippet variant andcontent_hashdoes not. - Found: the two variants really do shift generated columns. Both templates in
cli/src/sourcemaps/constant.rs:2and:8are single-line and end in}();with no newline, andcli/src/sourcemaps/content.rs:269-272prepends the snippet directly before the first character. The release variant replaces,with;e._posthogReleaseId=e._posthogReleaseId||"<36-char uuid>";var, roughly 80 characters, so every line-1 mapping moves by that amount. Minified bundles put most code on line 1. - Found: the equal hash is deliberate and test-pinned.
test_event_mode_content_hash_is_stable_across_release_statesatcli/tests/sourcemap.rs:474-509assertsreleaseless == with_release, and the comment atcli/src/sourcemaps/source_pairs.rs:150-156names the exact case — a chunk injected before a release resolved "carries a shorter snippet (and a differently adjusted sourcemap)". The design knowingly hashes past a map difference to avoid a falsecontent_hash_mismatch. - Found:
forcedoes not rescue it.products/error_tracking/backend/logic/symbol_sets.py:246-249takes the equal-hash branch and onlypasses, issuing no presigned URL, so the newer payload is never sent.forceis read at:250, which is reachable only when the hashes differ. Event mode always setsforce: true, and it has no effect here. - Impact: after a chunk transitions between the no-release and with-release states without its pristine content changing, the stored map keeps the old snippet's offsets while the deployed chunk has the new one. Frames then resolve about 80 generated columns away, which in minified code can be a different function. The wrong map persists until the pristine content changes, and nothing reports it.
- Impact (scope): a release-to-release change is not affected, because a UUID is always 36 characters, so the snippet length and the mappings stay identical — that case is covered by
test_event_mode_content_hash_is_stable_across_releases_for_adopted_idsatcli/tests/sourcemap.rs:620. The harm needs the length to change, so it hits projects whose earlier uploads had no resolvable release (cli/src/sourcemaps/inject.rs:98-102warns in that state) and that later gain one. Unchanged vendor chunks are the most exposed, since their pristine content survives many deploys. This PR makes event mode the default atcli/src/sourcemaps/plain/upload.rs:77, so the path now runs for every web upload.
Issue description
Event mode removes the injected snippet before it calculates content_hash. A chunk without a release and the same chunk with a release therefore have the same hash. Their uploaded source maps differ because the release snippet is longer and shifts generated columns. The server skips the second payload as identical. It then uses old column offsets for new exceptions, which can resolve frames to wrong source positions.
Suggested fix
Include the snippet layout in the event-mode hash. For example, append a marker when _posthogReleaseId is present. This makes the no-release-to-release transition overwrite the stored map. Keep the hash stable when only the release UUID changes. Add a test for both transitions.
Prompt to fix with AI (copy-paste)
## Context
@cli/src/sourcemaps/plain/upload.rs#L69-77
<issue_description>
Event mode removes the injected snippet before it calculates `content_hash`. A chunk without a release and the same chunk with a release therefore have the same hash. Their uploaded source maps differ because the release snippet is longer and shifts generated columns. The server skips the second payload as identical. It then uses old column offsets for new exceptions, which can resolve frames to wrong source positions.
</issue_description>
<issue_validation>
- **Checked:** `into_upload` in `cli/src/sourcemaps/source_pairs.rs`, the snippet templates in `cli/src/sourcemaps/constant.rs`, `set_chunk_id` / `remove_chunk_id` in `cli/src/sourcemaps/content.rs`, the server's skip-or-overwrite branch in `products/error_tracking/backend/logic/symbol_sets.py`, and the event-mode hash tests in `cli/tests/sourcemap.rs`.
- **Found:** the payload and the hash are taken from different states of the pair. `cli/src/sourcemaps/source_pairs.rs:165-166` captures `source_content` and `sourcemap_content` for `data` while the injection is still present, and `:170` strips it only afterwards to compute the hash. So `data` carries the snippet variant and `content_hash` does not.
- **Found:** the two variants really do shift generated columns. Both templates in `cli/src/sourcemaps/constant.rs:2` and `:8` are single-line and end in `}();` with no newline, and `cli/src/sourcemaps/content.rs:269-272` prepends the snippet directly before the first character. The release variant replaces `,` with `;e._posthogReleaseId=e._posthogReleaseId||"<36-char uuid>";var `, roughly 80 characters, so every line-1 mapping moves by that amount. Minified bundles put most code on line 1.
- **Found:** the equal hash is deliberate and test-pinned. `test_event_mode_content_hash_is_stable_across_release_states` at `cli/tests/sourcemap.rs:474-509` asserts `releaseless == with_release`, and the comment at `cli/src/sourcemaps/source_pairs.rs:150-156` names the exact case — a chunk injected before a release resolved "carries a shorter snippet (and a differently adjusted sourcemap)". The design knowingly hashes past a map difference to avoid a false `content_hash_mismatch`.
- **Found:** `force` does not rescue it. `products/error_tracking/backend/logic/symbol_sets.py:246-249` takes the equal-hash branch and only `pass`es, issuing no presigned URL, so the newer payload is never sent. `force` is read at `:250`, which is reachable only when the hashes differ. Event mode always sets `force: true`, and it has no effect here.
- **Impact:** after a chunk transitions between the no-release and with-release states without its pristine content changing, the stored map keeps the old snippet's offsets while the deployed chunk has the new one. Frames then resolve about 80 generated columns away, which in minified code can be a different function. The wrong map persists until the pristine content changes, and nothing reports it.
- **Impact (scope):** a release-to-release change is not affected, because a UUID is always 36 characters, so the snippet length and the mappings stay identical — that case is covered by `test_event_mode_content_hash_is_stable_across_releases_for_adopted_ids` at `cli/tests/sourcemap.rs:620`. The harm needs the length to change, so it hits projects whose earlier uploads had no resolvable release (`cli/src/sourcemaps/inject.rs:98-102` warns in that state) and that later gain one. Unchanged vendor chunks are the most exposed, since their pristine content survives many deploys. This PR makes event mode the default at `cli/src/sourcemaps/plain/upload.rs:77`, so the path now runs for every web upload.
</issue_validation>
## Task
Investigate the issue and solve it
<potential_solution>
Include the snippet layout in the event-mode hash. For example, append a marker when `_posthogReleaseId` is present. This makes the no-release-to-release transition overwrite the stored map. Keep the hash stable when only the release UUID changes. Add a test for both transitions.
</potential_solution>
hermes upload resolved --info-plist after its event mode warning, so an iOS build was told its exceptions would report no release while the run created the correct one. proguard upload never checked --build, which the server packs into the release version it keys on. Both now share one predicate on ReleaseArgs. The inject help described the web release source only, though InjectArgs also backs hermes inject, which injects no release id. The conflict help claimed a rule that holds for sourcemap uploads and not for proguard. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jg6r6rFUBnowrXssZyU7yR
Related PRs
Event mode is live today. Each build must ask for it. These PRs make it the default. The modes themselves do not change.
Problem
Changes
--release-modedefaults toeventon seven commands.sourcemap inject,sourcemap process,sourcemap upload,hermes inject,hermes clone,hermes uploadandproguard upload.--release-mode symbol-setkeeps the old behavior.eventexperimental.Review fixes
hermes uploadresolves--info-plistbefore the event mode check. The check read the raw fields, so an iOS build got a warning about a release the run creates correctly.proguard uploadwarns about a missing--build. The server packs the build into the release version it keys on.ReleaseArgs::event_coordinates_complete.hermes injectsharesInjectArgsand injects no release id.proguard uploadpasses both flags through.How did you test this code?
cargo testincli/.symbol-setopt-out. Both now includehermes inject.event_mode_needs_every_release_coordinatecovers the shared predicate, including the build that the proguard guard missed.Automatic notifications
The sampo changeset is
cli/.sampo/changesets/release-mode-event-default.md. It asks for a minor bump.Docs update
The posthog.com docs still call
symbol-setthe default. They need a separate PR.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Written with Claude Code (Opus 5). Skills invoked:
/writing-pr-descriptions.Six commands declare the flag, not four.
sourcemaps/inject.rsandproguard/upload.rscarry it too.