Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions cli/.sampo/changesets/release-mode-event-native.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
cargo/posthog-cli: minor
---

Add `--release-mode` to `symbol-sets upload` (also `POSTHOG_RELEASE_MODE`). `symbol-set`, the default, 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 needs no `--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+); the release itself is named with `posthog-cli release resolve`, whose id you pass to the app. Event mode applies to newly created symbol sets: a symbol set already bound to a release keeps that binding — event mode does not detach it — so existing builds keep resolving.
22 changes: 22 additions & 0 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,28 @@ The CLI resolves exact Xcode build-setting references such as `$(APP_VERSION)` a
Missing, unresolved, or compound values fall back to `PRODUCT_BUNDLE_IDENTIFIER`, `MARKETING_VERSION`, and `CURRENT_PROJECT_VERSION`.
Explicit `--release-name`, `--release-version`, and `--build` values take precedence.

### Reporting the release from a native app at runtime

A web build injects `$release_id` into its bundle. A compiled binary has no bundle, so it reports the release from an environment variable instead.

Upload the debug symbols with `--release-mode=event`, so newly created symbol sets upload **release-independent** — bound to no release. (An already-bound symbol set keeps its release: event mode does not detach existing bindings, so older builds keep resolving.) Then resolve the release, put its id in `POSTHOG_RELEASE_ID`, and run the app with that variable set:

```bash
# Symbols, release-independent — no --release-name/--release-version needed here.
posthog-cli symbol-sets upload --directory target/release --release-mode=event

# The release is named here, and its id goes to the app. Assign first, then export: `export
# X=$(cmd)` takes export's own exit status, so a failing release resolve would slip through and
# launch the app with an empty id.
RELEASE_ID=$(posthog-cli release resolve --release-name my-app --release-version 1.4.0)
export POSTHOG_RELEASE_ID=$RELEASE_ID
./my-app
```

The SDK reads `POSTHOG_RELEASE_ID` at runtime and reports it as `$release_id` on each exception, so the server resolves that exception's release by a direct id lookup. Because the id is the key, the release name and version do not have to match anything the app reports, and one symbol set serves every release of an unchanged binary. posthog-rs reads this variable in 0.26+ (PostHog/posthog-rs#239).

This is the same `--release-mode=event` as for a distributed binary, minus the binary injection: the release still rides the event, but the SDK reads the id from the environment rather than from bytes patched into the build.

## Skipping uploads (dry run)

Pass `--dry-run` before the subcommand (`posthog-cli --dry-run hermes upload ...`), or set `POSTHOG_CLI_DRY_RUN=true`, to turn the upload commands — `sourcemap`, `dsym`, `hermes`, and `proguard` — into a no-op.
Expand Down
110 changes: 87 additions & 23 deletions cli/src/debug_symbols/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use crate::{
symbol_sets::{dedup_uploads_by_chunk_id, SymbolSetUpload, MAX_FILE_SIZE},
},
debug_symbols::{discover, package_dsym_bundles, report_problems},
sourcemaps::args::{pack_version, ReleaseArgs, UploadConflictArgs},
sourcemaps::args::{pack_version, ReleaseArgs, ReleaseMode, UploadConflictArgs},
utils::git::get_git_info,
};

Expand All @@ -36,6 +36,23 @@ pub struct Args {
/// Implies --force unless --skip-on-conflict is set.
#[arg(long, default_value_t = false)]
pub include_source: bool,

/// How the release is associated with exceptions. `symbol-set`, the default, resolves a release
/// (from the flags above or git) and binds it to every uploaded symbol set, so an exception
/// takes the release of the symbol sets its frames resolved against. EXPERIMENTAL `event`
/// instead uploads new symbol sets release-independent (an already-bound symbol set keeps its
/// release): each exception carries the release as `$release_id`, which the SDK reports from
/// `POSTHOG_RELEASE_ID` (posthog-rs 0.26+). So one
/// symbol set serves every release of an unchanged binary, and the release flags are not needed
/// here — name the release with `posthog-cli release resolve` instead. Also settable via
/// `POSTHOG_RELEASE_MODE`.
#[arg(
long,
env = "POSTHOG_RELEASE_MODE",
value_enum,
default_value = "symbol-set"
)]
pub release_mode: ReleaseMode,
Comment thread
ablaszkiewicz marked this conversation as resolved.
}

pub fn upload(args: &Args) -> Result<()> {
Expand All @@ -44,8 +61,8 @@ pub fn upload(args: &Args) -> Result<()> {
release,
conflict,
include_source,
release_mode,
} = args;
let release_args = release.resolve_info_plist()?;

let directory = directory.canonicalize().map_err(|e| {
anyhow!(
Expand Down Expand Up @@ -95,27 +112,44 @@ pub fn upload(args: &Args) -> Result<()> {
);
}

// Now that there's something to upload, set up the release (explicit flags
// win, git info is metadata/fallback) and stamp it on every set.
let mut release_builder = ReleaseBuilder::default();
if let Ok(Some(git_info)) = get_git_info(Some(directory.clone())) {
release_builder.with_git(git_info);
}
if let Some(ref release_name) = release_args.name {
release_builder.with_name(release_name);
}
if let Some(version) = pack_version(&release_args.version, &release_args.build) {
release_builder.with_version(&version);
}
match release_mode {
// Resolve a release (explicit flags win, git info is metadata/fallback) and stamp it on
// every set, so an exception takes the release of the symbol sets it resolves against.
ReleaseMode::SymbolSet => {
// Only this mode reads release metadata, so resolve the Info.plist here rather than up
// front — an event-mode upload never uses it and must not abort on a bad --info-plist.
let release_args = release.resolve_info_plist()?;
let mut release_builder = ReleaseBuilder::default();
if let Ok(Some(git_info)) = get_git_info(Some(directory.clone())) {
release_builder.with_git(git_info);
}
if let Some(ref release_name) = release_args.name {
release_builder.with_name(release_name);
}
if let Some(version) = pack_version(&release_args.version, &release_args.build) {
release_builder.with_version(&version);
}

let created_release = release_builder
.can_create()
.then(|| release_builder.fetch_or_create())
.transpose()?;
if let Some(release) = created_release {
let release_id = release.id.to_string();
for upload in &mut uploads {
upload.release_id = Some(release_id.clone());
let created_release = release_builder
.can_create()
.then(|| release_builder.fetch_or_create())
.transpose()?;
if let Some(release) = created_release {
let release_id = release.id.to_string();
for upload in &mut uploads {
upload.release_id = Some(release_id.clone());
}
}
}
// Upload the symbol sets release-independent (bound to no release). The release rides the
// event instead: the SDK reports it as `$release_id` (from `POSTHOG_RELEASE_ID`), and the
// server resolves each exception by that id. One symbol set then serves every release of an
// unchanged binary, so there is nothing to resolve or bind here.
ReleaseMode::Event => {
info!(
"--release-mode=event: uploading symbol sets release-independent; the release is \
carried on each exception as $release_id (POSTHOG_RELEASE_ID)"
);
Comment thread
ablaszkiewicz marked this conversation as resolved.
}
}

Expand All @@ -126,7 +160,7 @@ pub fn upload(args: &Args) -> Result<()> {
let (_summary, upload_result) = api::symbol_sets::upload_with_retry(
uploads,
10,
release_args.skip_release_on_fail,
release.skip_release_on_fail,
effective_force,
conflict.skip_on_conflict,
);
Expand Down Expand Up @@ -159,6 +193,36 @@ fn merge_uploads_prefer_dsym(
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;

#[derive(Parser)]
struct SymbolSetsCli {
#[command(subcommand)]
command: crate::download::SymbolSetsSubcommand,
}

fn parse_upload(extra: &[&str]) -> Args {
let mut argv = vec!["symbol-sets", "upload", "--directory", "target/release"];
argv.extend_from_slice(extra);
match SymbolSetsCli::parse_from(argv).command {
crate::download::SymbolSetsSubcommand::Upload(args) => args,
_ => panic!("expected the upload subcommand"),
}
}

#[test]
fn defaults_to_binding_the_release_to_the_symbol_sets() {
// Every existing caller omits the flag and must keep binding symbol sets to their release.
assert_eq!(parse_upload(&[]).release_mode, ReleaseMode::SymbolSet);
}

#[test]
fn accepts_event_release_mode() {
assert_eq!(
parse_upload(&["--release-mode", "event"]).release_mode,
ReleaseMode::Event
);
}

#[test]
fn merge_uploads_prefers_dsym_over_matching_macho() {
Expand Down
7 changes: 3 additions & 4 deletions cli/src/sourcemaps/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ impl FileSelectionArgs {
pub enum ReleaseMode {
/// Bind the release to the uploaded symbol sets (the previous behavior)
SymbolSet,
/// EXPERIMENTAL: resolve the release per event from an id injected into each chunk
/// EXPERIMENTAL: resolve the release per event, keeping the uploaded chunks release-independent
Event,
}

Expand Down Expand Up @@ -135,13 +135,12 @@ pub struct ReleaseArgs {

#[derive(clap::Args, Clone, Default)]
pub struct UploadConflictArgs {
/// Allow overwriting an existing symbol set whose content has changed. Always on with
/// `--release-mode=event`. [default: false]
/// Allow overwriting an existing symbol set whose content has changed. [default: false]
#[arg(long, default_value_t = false, conflicts_with = "skip_on_conflict")]
pub force: bool,

/// Skip symbol sets that already exist with different content instead of failing.
/// Existing symbol sets are left unchanged. Ignored with `--release-mode=event`. [default: false]
/// Existing symbol sets are left unchanged. [default: false]
#[arg(long, default_value_t = false, conflicts_with = "force")]
pub skip_on_conflict: bool,
}
Expand Down
Loading