Skip to content

feat(cli): default release mode to event - #91823

Draft
ablaszkiewicz wants to merge 2 commits into
masterfrom
feat/release-mode-event-default
Draft

feat(cli): default release mode to event#91823
ablaszkiewicz wants to merge 2 commits into
masterfrom
feat/release-mode-event-default

Conversation

@ablaszkiewicz

@ablaszkiewicz ablaszkiewicz commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

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

  • Two releases that ship the same code report their exceptions on one release.
  • The release that uploads the symbol set first takes them.

Changes

  • --release-mode defaults to event on seven commands.
  • The commands are sourcemap inject, sourcemap process, sourcemap upload, hermes inject, hermes clone, hermes upload and proguard upload.
  • --release-mode symbol-set keeps the old behavior.
  • The help text no longer calls event experimental.

Review fixes

  • hermes upload resolves --info-plist before 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 upload warns about a missing --build. The server packs the build into the release version it keys on.
  • Both commands now read one predicate, ReleaseArgs::event_coordinates_complete.
  • The inject help describes both release sources. hermes inject shares InjectArgs and injects no release id.
  • The conflict help limits its event mode rule to sourcemap uploads. proguard upload passes both flags through.

How did you test this code?

  • cargo test in cli/.
  • Two tests pin the new default. Two more cover the symbol-set opt-out. Both now include hermes inject.
  • event_mode_needs_every_release_coordinate covers the shared predicate, including the build that the proguard guard missed.
  • Not run: an upload against a real project.

Automatic notifications

  • Publish to changelog?

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-set the 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.rs and proguard/upload.rs carry it too.

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
@ablaszkiewicz ablaszkiewicz self-assigned this Aug 31, 2026
@trunk-io

trunk-io Bot commented Aug 31, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

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

@github-actions

Copy link
Copy Markdown
Contributor

🤖 CI report

Trunk lane — non-backend lane

This 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

posthog Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

🦔 PostHog Review reviewed this pull request

Found 2 must fix, 4 should fix, 1 consider.

Published 7 findings (view the review).

@posthog

posthog Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

PostHog Review alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review

Found 2 must fix, 4 should fix, 1 consider.

Comment thread cli/src/sourcemaps/hermes/upload.rs
Comment thread cli/src/sourcemaps/inject.rs
Comment thread cli/src/sourcemaps/inject.rs Outdated
Comment thread cli/src/sourcemaps/plain/upload.rs
Comment thread cli/src/proguard/upload.rs
Comment on lines +69 to +77
/// 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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Event uploads can keep source maps with wrong offsets

must_fix bug

Why we think it's a valid issue
  • 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 passes, 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 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>

Comment thread cli/src/proguard/upload.rs
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
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