Skip to content

feat(error-tracking): report the injected release id on flutter web - #554

Draft
ablaszkiewicz wants to merge 1 commit into
mainfrom
feat/web-release-id
Draft

feat(error-tracking): report the injected release id on flutter web#554
ablaszkiewicz wants to merge 1 commit into
mainfrom
feat/web-release-id

Conversation

@ablaszkiewicz

@ablaszkiewicz ablaszkiewicz commented Aug 28, 2026

Copy link
Copy Markdown

💡 Motivation and Context

A Flutter web app that uploads its sourcemaps with posthog-cli sourcemap inject --release-mode=event gets a release id written into main.dart.js. Nothing in the Dart SDK read it. The release still reached the event, because on web the plugin hands the exception to posthog-js and posthog-js reads the same global — but only posthog-js 1.409.0 and later. The page picks its own posthog-js, so an app on an older one silently loses the release its build injected.

This reads the global in Dart, next to the chunk ids the SDK already reads there.

  • release_id.dart, release_id_web.dart and release_id_io.dart mirror the existing chunk_ids.dart trio. The io stub returns null: no chunk carries a release id outside the web build, and Apple and Android exceptions resolve their release from the app metadata the native SDK reports.
  • DartExceptionProcessor.processException puts it on the payload as $release_id, before user properties, so a caller can still override it.
  • An absent, empty or non-string global leaves the property off. A malformed value reaches the server as a release that resolves to nothing, which is worse than sending no release.

Nothing changes for an app that does not run --release-mode=event, and nothing changes on Apple or Android.

💚 How did you test it?

Two example apps in error-tracking-examples, the same Flutter web app twice, differing only in the release mode their upload runs in. Each was run twice: a first release, then the same code again under a new version. Both built against this branch through a path dependency, and both reported into a local PostHog.

All four exceptions symbolicated the full Dart call chain and resolved a release.

Flutter web (legacy)

First release

posthog-cli, flutter-web-legacy@5.0.0
WARN posthog_cli::api::releases: release flutter-web-legacy@5.0.0 not found
INFO posthog_cli::api::releases: Release flutter-web-legacy@5.0.0 created successfully! 01a047ec-a14d-0000-0040-a7619a2252d6
INFO posthog_cli::sourcemaps::inject: injecting selection: ["build/web"]
INFO posthog_cli::sourcemaps::source_pairs: found 1 pairs
INFO posthog_cli::sourcemaps::inject: injecting done
INFO posthog_cli::sourcemaps::plain::upload: Found 1 chunks to upload
INFO posthog_cli::api::symbol_sets: Server returned 1 upload keys (0 skipped as already present)
INFO posthog_cli::api::symbol_sets: Upload summary: 1 chunk(s) uploaded, 0 skipped (0 already present, 0 too large)
Issue with readable stack trace Release is associated App the SDK reported
stack release app

The app page reads injected $release_id: none, which is what legacy mode looks like. The release on the event came from the symbol set the upload bound to it.

Second release, same code

posthog-cli, flutter-web-legacy@6.0.0
WARN posthog_cli::api::releases: release flutter-web-legacy@6.0.0 not found
INFO posthog_cli::api::releases: Release flutter-web-legacy@6.0.0 created successfully! 01a047ee-5f8d-0000-4e44-b784779574a6
INFO posthog_cli::sourcemaps::inject: injecting selection: ["build/web"]
INFO posthog_cli::sourcemaps::source_pairs: found 1 pairs
INFO posthog_cli::sourcemaps::inject: injecting done
INFO posthog_cli::sourcemaps::plain::upload: Found 1 chunks to upload
INFO posthog_cli::api::symbol_sets: Server returned 1 upload keys (0 skipped as already present)
INFO posthog_cli::api::symbol_sets: Upload summary: 1 chunk(s) uploaded, 0 skipped (0 already present, 0 too large)

Legacy mints a fresh random chunk id on every build, so an unchanged 2.4 MB sourcemap uploads again as a second symbol set.

Flutter web (event mode)

First release

posthog-cli, flutter-web-releaseless@5.0.0
WARN posthog_cli::api::releases: release flutter-web-releaseless@5.0.0 not found
INFO posthog_cli::api::releases: Release flutter-web-releaseless@5.0.0 created successfully! 01a047f1-da22-0000-d921-a557644b1768
INFO posthog_cli::sourcemaps::inject: injecting selection: ["build/web"]
INFO posthog_cli::sourcemaps::source_pairs: found 1 pairs
INFO posthog_cli::sourcemaps::inject: injecting done
INFO posthog_cli::sourcemaps::plain::upload: Found 1 chunks to upload
INFO posthog_cli::api::symbol_sets: Server returned 1 upload keys (0 skipped as already present)
INFO posthog_cli::api::symbol_sets: Upload summary: 1 chunk(s) uploaded, 0 skipped (0 already present, 0 too large)
Release is associated App the SDK reported
release app

The app page prints the id it read out of the chunk, and the event carries the same one.

Second release, same code

posthog-cli, flutter-web-releaseless@6.0.0
WARN posthog_cli::api::releases: release flutter-web-releaseless@6.0.0 not found
INFO posthog_cli::api::releases: Release flutter-web-releaseless@6.0.0 created successfully! 01a047f4-ee05-0000-8df6-3d3592b4255c
INFO posthog_cli::sourcemaps::inject: injecting selection: ["build/web"]
INFO posthog_cli::sourcemaps::source_pairs: found 1 pairs
INFO posthog_cli::sourcemaps::inject: injecting done
INFO posthog_cli::sourcemaps::plain::upload: Found 1 chunks to upload
INFO posthog_cli::api::symbol_sets: Server returned 0 upload keys (1 skipped as already present)
INFO posthog_cli::api::symbol_sets: Upload summary: 0 chunk(s) uploaded, 1 skipped (1 already present, 0 too large)

1 skipped (1 already present) is the case the two examples exist to contrast. The chunk id comes from the content, so the sourcemap is unchanged and the stored symbol set is reused. The new release's id goes into main.dart.js, and the exception reports flutter-web-releaseless@6.0.0 off that one symbol set.

Issue with readable stack trace Release is associated App the SDK reported
stack release app

Both variants and all four releases land on one issue, each event carrying its own:

releases

Deferred loading

flutter build web emits one sourcemapped chunk. An app that uses deferred loading gets one more per deferred library. A third build with import 'heavy.dart' deferred as heavy produced main.dart.js and main.dart.js_1.part.js, the CLI found and injected both, and every frame carried its own chunk id:

fn=aa6.$0     file=main.dart.js             chunk=43e34280-56ef-5e2f-9462-42a959136196
fn=Object.aEI file=main.dart.js_1.part.js   chunk=5ddac51e-6b0a-5fc5-bec4-6a09ac09c349
fn=Object.h   file=main.dart.js             chunk=43e34280-56ef-5e2f-9462-42a959136196

Both frames symbolicated, and the event resolved one release for the whole stack.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

Docs are unticked on purpose: --release-mode is not documented on posthog.com for any platform yet, so a Flutter-only page would be the first. Happy to write it in a follow-up once the general docs land.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Written by Claude Opus 5 in Claude Code. Skills invoked: posthog-repos, asd-ste100, writing-pr-descriptions.

The task started as "add event release mode support for Flutter", on the assumption the SDK needed the same work the other SDKs got. It did not. Reading posthog_flutter on main showed the web handler already routes through posthog-js's captureException, and running it end to end confirmed the release reaches the event today. The change that survived is the narrower one: read the global in Dart so the release does not depend on which posthog-js the page loaded.

Rejected along the way: reading the release id in the web method-channel handler instead of the processor, which would have missed the fallback path the handler keeps for old posthog-js builds.

The two example apps live in PostHog/error-tracking-examples on main, as flutter-web-legacy and flutter-web-releaseless.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ablaszkiewicz ablaszkiewicz self-assigned this Aug 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

posthog-flutter Compliance Report

Date: 2026-08-28 10:54:25 UTC
Duration: 96923ms

✅ All Tests Passed!

45/45 tests passed


Capture Tests

29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields 148ms
Format Validation.Event Has Uuid 128ms
Format Validation.Event Has Lib Properties 119ms
Format Validation.Distinct Id Is String 122ms
Format Validation.Token Is Present 124ms
Format Validation.Custom Properties Preserved 126ms
Format Validation.Event Has Timestamp 124ms
Retry Behavior.Retries On 503 5334ms
Retry Behavior.Does Not Retry On 400 2119ms
Retry Behavior.Does Not Retry On 401 2119ms
Retry Behavior.Respects Retry After Header 8122ms
Retry Behavior.Implements Backoff 15447ms
Retry Behavior.Retries On 500 5226ms
Retry Behavior.Retries On 502 5225ms
Retry Behavior.Retries On 504 5226ms
Retry Behavior.Max Retries Respected 15446ms
Deduplication.Generates Unique Uuids 125ms
Deduplication.Preserves Uuid On Retry 5225ms
Deduplication.Preserves Uuid And Timestamp On Retry 10336ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 5234ms
Deduplication.No Duplicate Events In Batch 124ms
Deduplication.Different Events Have Different Uuids 118ms
Compression.Sends Gzip When Enabled 116ms
Batch Format.Uses Proper Batch Structure 113ms
Batch Format.Flush With No Events Sends Nothing 109ms
Batch Format.Multiple Events Batched Together 125ms
Error Handling.Does Not Retry On 403 2117ms
Error Handling.Does Not Retry On 413 2118ms
Error Handling.Retries On 408 5227ms

Feature_Flags Tests

16/16 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id 14ms
Request Payload.Flags Request Uses V2 Query Param 10ms
Request Payload.Flags Request Hits Flags Path Not Decide 10ms
Request Payload.Flags Request Omits Authorization Header 9ms
Request Payload.Token In Flags Body Matches Init 10ms
Request Payload.Groups Round Trip 10ms
Request Payload.Groups Default To Empty Object 10ms
Request Payload.Person Properties Distinct Id Auto Populated When Caller Omits It 11ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False 10ms
Request Payload.Disable Geoip Omitted Defaults To False 10ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key 9ms
Request Lifecycle.No Flags Request On Init Alone 5ms
Request Lifecycle.No Flags Request On Normal Capture 115ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests 17ms
Request Lifecycle.Mock Response Value Is Returned To Caller 10ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event 117ms

@posthog

posthog Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

🦔 PostHog Review reviewed this pull request

Nothing worth raising this time, so here's a calming picture instead:

A panda relaxing and waving

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