fix(android): recover from lost init-iframe + handshake logging (2.2.2 debug) - #101
Open
JakubMrozek wants to merge 6 commits into
Open
fix(android): recover from lost init-iframe + handshake logging (2.2.2 debug)#101JakubMrozek wants to merge 6 commits into
JakubMrozek wants to merge 6 commits into
Conversation
Symptom: on Android the ad fills (frame served) but never shows, and no `viewed` impression is recorded. Root cause is the same class of bug fixed in the React Native SDK for Saylo: the Android WebView `onLoad` can fire two or more times, and a reload after the one-time `onWebViewCreated` flush re-creates an empty message queue, so an `init-iframe` posted on the reload is queued but never delivered. Without `init-iframe`, `iframeLoaded` never becomes true, `update-iframe` is never sent, the stream never starts, and the ad never shows — silently, with no error. Changes: - KontextWebview: re-run the message-queue flush on every `onLoadStop` (not just once in `onWebViewCreated`) so a re-queued `init-iframe` is delivered; expose an optional `onLoadStop` callback. - AdFormat: arm a 500ms fallback on load that, if the handshake hasn't progressed, forces `iframeLoaded` (to unblock dimension posting) and (re)sends `update-iframe` — mirroring the RN Saylo workaround. - Add `[Kontext][handshake]` info logging across the full lifecycle (active/frame URL, onLoadStop, init/show/resize/ad-done/hide/error, update-iframe normal + fallback, dimension posting) to diagnose on device. - Drop the `; null` appended to the two evaluateJavascript sources. It is a no-op on Android (undefined is coerced to null there); it only suppressed a cosmetic iOS "unsupported type" log. Restore before release if desired. - example: print ad events and use chai-dev/inlineAd for on-device testing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gcat Logger._logLocal only used developer.log(name: 'Kontext'), which surfaces in DevTools but not in `flutter run` stdout or adb logcat. Mirror it to debugPrint (kDebugMode only) so the [Kontext][handshake] trace is visible on device during debugging. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ceholder
- kSdkVersion, pubspec version, and podspec version -> 2.2.3-rc.0 so the RC
is identifiable in preload requests (sdk.version) and everywhere else.
- example: restore kPublisherToken placeholder ('PUBLISHER_TOKEN').
Not a production release — pre-release identifier for testing the Android
init-iframe fix on the customer's device.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r 2.2.3-rc.0 publish.yml previously required every published tag to be an ancestor of origin/main. Exempt pre-release tags (semver `-` suffix, e.g. v2.2.3-rc.0) so an RC can be handed to a customer for testing without merging to main. Stable releases still must be cut from main. main is untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
JakubMrozek
temporarily deployed
to
pubdev-release
July 2, 2026 19:35 — with
GitHub Actions
Inactive
….3-rc.1 The rc.0 recovery armed its update-iframe fallback in `onLoadStop`, but that event is exactly what the Android double-onLoad reload corrupts — so on the customer device 2 of 4 ads still filled-but-never-showed (frame served, no `viewed`). Rearm on mount instead, independent of any load event, and keep re-sending `update-iframe` until `show-iframe` actually arrives. - ad_format: mount-based recovery nudger (reload-proof); its timer is no longer cancelled by cancelTimers() (that runs before the ad is visible, i.e. exactly when the nudger must keep going). - kontext_webview: expose onWebViewCreated so we get the controller even if zero messages arrive. - test: reproduce the lost-init-iframe failure and assert recovery (fails on the old onLoadStop approach, passes now). Full suite 27/27. - Validated on emulator: 6/6 ads rendered, one benign recovery nudge, no regression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
JakubMrozek
had a problem deploying
to
pubdev-release
July 3, 2026 11:36 — with
GitHub Actions
Error
rc.1 stopped the recovery on `show-iframe`, but show-iframe can arrive while init-iframe was lost — leaving iframeLoaded false and the ad blank. It also armed once on mount, so a reused ad slot only ever recovered the first ad. - Recovery now keys off the true failure signal (iframeLoaded never set), not show-iframe, and stops only when actually shown (iframeLoaded && showIframe) or when a real init-iframe arrived (never nudges healthy ads). - Re-armed per [bidId] so every ad recovers, not just the first. - Regression test: show-iframe arrives but init-iframe lost -> recovery still fires. Full suite 27/27. - Validated on emulator with init-iframe dropped: 3/3 ads recovered and viewed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
JakubMrozek
temporarily deployed
to
pubdev-release
July 3, 2026 13:46 — with
GitHub Actions
Inactive
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
Customer (chai / Saylo-style app) reports: on Android, ads fill but never show on v2.2.2. Server-side we confirmed the exact signature via ClickHouse + OTel logs:
bidsrow exists (fill) ✅impressions_contentwritten (creative HTML served viaGET /api/frame/:id) ✅impressions_eventsviewed— never written ❌ (client never fires/impression/:id/view)So the creative loads in the WebView but the show/view handshake never completes — no error, no forwarded log.
Root cause
Same class of bug the RN SDK already fixed for Saylo (
sdk-react-native/src/formats/Format.tsx): the Android WebViewonLoadfires 2+ times. The SDK flushes the early-bridge message queue only once inonWebViewCreated; a reload after that re-creates an empty__kontextMsgQueue, so aninit-iframeposted on the reload is queued and never delivered. Withoutinit-iframe,iframeLoadednever becomes true →update-iframeis never sent → the stream never starts → the ad never shows.An exhaustive line-by-line review of the whole v2.2.1→v2.2.2 diff showed nothing in it mechanically breaks the Android show path (the
; nullchange is a no-op on Android), consistent with this being a latent race present in both versions that timing perturbations (e.g. the Flutter 3.38 WebView runtime) surface more often — which is why "2.2.1 works / 2.2.2 doesn't" and why it's not reliably reproducible.Changes
KontextWebview: re-run the queue flush on everyonLoadStop(delivers a re-queuedinit-iframe); expose optionalonLoadStop.AdFormat: 500ms fallback on load — if the handshake hasn't progressed, forceiframeLoadedand (re)sendupdate-iframe(≤6 tries, stops onshow-iframe). Mirrors the RN workaround.[Kontext][handshake]info logs across the lifecycle so we can see on-device exactly where it dies and whether the fallback rescues it.; nullremoved from the twoevaluateJavascriptsources — no-op on Android; only silenced a cosmetic iOS log. Restore before release if this ships to iOS.chai-dev/inlineAdfor testing.Testing
flutter analyzeclean;flutter test test/src/widgets/ad_format_test.dart→ 26/26 pass.[Kontext][handshake]trace.Base is
run-2.2.2(the 2.2.2 line) intentionally —mainis left untouched.🤖 Generated with Claude Code