Skip to content

feat(flutter): emit native frames for obfuscated AOT errors - #677

Merged
hpouillot merged 3 commits into
mainfrom
feat/flutter-aot-symbolication
Oct 28, 2026
Merged

hpouillot merged 3 commits into
mainfrom
feat/flutter-aot-symbolication

Conversation

@hpouillot

@hpouillot hpouillot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Flutter developers using obfuscated Android and iOS builds cannot inspect readable Dart frames in error tracking (request #433).

  • The SDK emits native frames and debug images. PostHog resolves the reported AOT addresses with uploaded symbols.
  • Ordinary Dart traces and malformed AOT metadata keep their fallback; events without native frames omit $debug_images.
  • Deferred loading-unit frames stay raw rather than resolving against the wrong symbol set.
  • AOT frames honor inAppByDefault. Cause stacks contribute their referenced debug images.
  • The private cause helpers require an image accumulator, which stays empty without native frames. This does not add a required SDK option.
  • The companion PostHog PR scopes optional app-source uploads and tests Flutter artifacts in Cymbal.

Android AOT event-to-UI matching is verified in an isolated local project; iOS AOT runtime validation and coordinated release remain pending. The change does not remap obfuscated exception type names or change the app UI directly.

💚 How did you test it?

  • Ran flutter test --no-pub, make checkFormatDart, make analyzeDart, and pnpm exec changeset status.
  • Processor tests catch incorrect debug-ID conversion, dropped AOT addresses, deferred-unit misattribution, and loss of ordinary-trace fallback.
  • A method-channel test catches lost debug metadata during event delivery.
  • Invented obfuscated and non-obfuscated Android releases sent exceptions to an isolated local project. The issue UI showed the Dart function, app line, and uploaded source context.
  • An async AOT trace resolved both app frames around its synthetic suspension frame. Missing or mismatched debug images left the invented events intact.
  • An unsigned iOS device release build produced Mach-O symbols. Not checked: iOS live capture; the iOS simulator cannot run release-mode AOT.
  • No symbols were uploaded to a remote project.

Flutter AOT app frame and source context

📝 Checklist

  • All commits are signed (see Commit signing)
  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed. (Flutter onboarding remains unchanged until the paired feature is verified and ready to release.)
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file. (A minor changeset was added directly and validated with pnpm exec changeset status.)

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: pi, gpt-6-sol

  • Session: Cockpit f4faad3e-b1d7-4e72-90f1-eab640e344b9 (no public session link available). Tools: read, edit, write, bash, codemode.
  • Skills: /pr, /writing-pr-descriptions, /writing-tests, /writing-code-comments, /writing-user-facing-copy, /announcing-behavior-changes.
  • A public SDK spec guided debug-image fields. Invented Android and iOS builds supplied the test IDs. Searches for open obfuscation PRs found no duplicate.

@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 · Standard · reviewed 9358e8c: 3 Should fix.

This is the only automatic review while this pull request is a draft. Comment @posthog review to review new changes sooner. Otherwise, a review of the new changes runs when you mark it ready for review.

See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.

processedFrame = {
'platform': 'native',
'lang': 'dart',
'instruction_addr': '0x${instructionAddress.toRadixString(16)}',

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.

P2 · Compensate for the resolver's call-site adjustment

Dart AOT traces already report call addresses, but Cymbal's RawNativeFrame::resolve_impl subtracts one again. For future-listener frames, Dart reports the closure entry point, so this extra subtraction places the lookup before the closure and resolves the wrong function or no symbol. Bias instruction_addr by +1 to match the resolver.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. Dart AOT reports the program counter directly. The companion server change in PostHog/posthog#124249 now skips the return-address adjustment for Dart frames, so the SDK keeps the actual instruction address. A regression test resolves a function-entry address from an invented Flutter artifact.

Comment on lines +377 to +384
if (instructionAddress != null &&
imageBase != null &&
instructionAddress >= imageBase) {
processedFrame = {
'platform': 'native',
'lang': 'dart',
'instruction_addr': '0x${instructionAddress.toRadixString(16)}',
'image_addr': aotImage!.imageAddr,

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.

P2 · Associate deferred frames with their own loading-unit images

Android deferred-component traces contain loading_unit headers and unit N frames. This branch assigns deferred addresses above the root base to the root image, making Cymbal query the wrong symbol set. The SDK contract required by AGENTS.md:11 requires matching images; select each unit's build ID/base or preserve those frames as raw.

'lang': 'dart',
'instruction_addr': '0x${instructionAddress.toRadixString(16)}',
'image_addr': aotImage!.imageAddr,
'in_app': true,

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.

P2 · Honor the configured default for AOT frame classification

When inAppByDefault is false, every recognized AOT frame still receives in_app: true. The ordinary-frame path honors this setting for unknown origins, so enabling split debug information changes classification and makes external frames participate as application code. Use inAppByDefault for native frames whose package cannot be determined.

@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

posthog-flutter Compliance Report

Date: 2026-10-28 10:32:18 UTC
Duration: 96917ms

✅ All Tests Passed!

45/45 tests passed


Capture Tests

✅ 29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 147ms
Format Validation.Event Has Uuid ✅ 121ms
Format Validation.Event Has Lib Properties ✅ 117ms
Format Validation.Distinct Id Is String ✅ 115ms
Format Validation.Token Is Present ✅ 117ms
Format Validation.Custom Properties Preserved ✅ 118ms
Format Validation.Event Has Timestamp ✅ 117ms
Retry Behavior.Retries On 503 ✅ 5332ms
Retry Behavior.Does Not Retry On 400 ✅ 2118ms
Retry Behavior.Does Not Retry On 401 ✅ 2118ms
Retry Behavior.Respects Retry After Header ✅ 8126ms
Retry Behavior.Implements Backoff ✅ 15439ms
Retry Behavior.Retries On 500 ✅ 5227ms
Retry Behavior.Retries On 502 ✅ 5227ms
Retry Behavior.Retries On 504 ✅ 5225ms
Retry Behavior.Max Retries Respected ✅ 15445ms
Deduplication.Generates Unique Uuids ✅ 125ms
Deduplication.Preserves Uuid On Retry ✅ 5223ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 10334ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 5230ms
Deduplication.No Duplicate Events In Batch ✅ 125ms
Deduplication.Different Events Have Different Uuids ✅ 117ms
Compression.Sends Gzip When Enabled ✅ 115ms
Batch Format.Uses Proper Batch Structure ✅ 114ms
Batch Format.Flush With No Events Sends Nothing ✅ 109ms
Batch Format.Multiple Events Batched Together ✅ 123ms
Error Handling.Does Not Retry On 403 ✅ 2115ms
Error Handling.Does Not Retry On 413 ✅ 2117ms
Error Handling.Retries On 408 ✅ 5226ms

Feature_Flags Tests

✅ 16/16 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 13ms
Request Payload.Flags Request Uses V2 Query Param ✅ 10ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 9ms
Request Payload.Flags Request Omits Authorization Header ✅ 10ms
Request Payload.Token In Flags Body Matches Init ✅ 9ms
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 ✅ 10ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 9ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 9ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 10ms
Request Lifecycle.No Flags Request On Init Alone ✅ 4ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 113ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 16ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 12ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 115ms

@hpouillot
hpouillot marked this pull request as ready for review October 27, 2026 17:56
@hpouillot
hpouillot requested a review from a team as a code owner October 27, 2026 17:56

@dustinbyrne dustinbyrne 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.

No actionable defects found in the AOT frame conversion, fallback handling, cause-image collection, or native forwarding path. Coordinate the companion backend correction and complete running-build/symbol verification before release.

AI-assisted review.

)
: <Map<String, dynamic>>[];
: null;
final frames = parsedTrace?.frames ?? <Map<String, dynamic>>[];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

always use Object or Object? instead of dynamic so we can be explicit about null safety

@hpouillot hpouillot Oct 28, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated the new frame, image, and cause maps to Object? in 5dde65a. The existing processException return type stays unchanged for compatibility.

List<String>? inAppIncludes,
List<String>? inAppExcludes,
bool inAppByDefault = true,
required List<Map<String, dynamic>> debugImages,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

i'd avoid required since its not needed if strip debug symbols compiler flag isnt used

@hpouillot hpouillot Oct 28, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

required here applies only to a private helper's image accumulator. processException passes an empty list even when no AOT metadata exists, and it adds $debug_images to the event only if that list is nonempty. No SDK caller needs to provide debug images. I kept this internal argument required so cause frames can add images to the same list.

List<String>? inAppIncludes,
List<String>? inAppExcludes,
bool inAppByDefault = true,
required List<Map<String, dynamic>> debugImages,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@hpouillot hpouillot Oct 28, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same for this recursive private helper: it always receives the internal accumulator, which can be empty. Events without native AOT frames omit $debug_images; required does not make it a public option or an event field.

@hpouillot
hpouillot merged commit cabe336 into main Oct 28, 2026
35 checks passed
@hpouillot
hpouillot deleted the feat/flutter-aot-symbolication branch October 28, 2026 12:55
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.

3 participants