AppsFlyer: Fix - retry failed init instead of silently dropping every event - #2383
Draft
JohnathanWhite wants to merge 1 commit into
Draft
JohnathanWhite wants to merge 1 commit into
JohnathanWhite wants to merge 1 commit into
Conversation
… event A single failed AppsFlyer init was permanent for the process lifetime, and every conversion event after it was dropped in silence. The chain: - AppsFlyerWrapper.init() sets `status = 'failed'` when initSdk throws, and swallows the error rather than rethrowing. - init() is called from exactly one place, analytics.effects.ts:93, inside Analytics.initialize(), which is guarded by `_isInitialized`. Because the AppsFlyer failure never propagates, `_isInitialized` is still set to true, so init() is never called again. - track() returned early on `!isReady()`. With status stuck at 'failed', that is every subsequent event, for the rest of the process. - The failure was reported with logManager.error(), which only writes a local log line and a Sentry breadcrumb -- not captureError -- so it produced no Sentry issue and no metric. Nothing could have alerted on it. - Braze and Mixpanel have no equivalent gate, so they kept receiving the same events. Any comparison between Mixpanel and AppsFlyer therefore shows AppsFlyer short by however many users hit a failed init. That last point matches the reported symptom: an audit of Crypto Buys for the week of 6/22 found roughly half the logged transactions missing from AppsFlyer while Mixpanel had them all. Changes: - Extract the init body into a module-private `doInit()` so it can be driven from more than the public init(). This is what makes the diff large -- the block moved out of the object literal and de-indented; the logic inside is unchanged apart from the two items below. - Add `ensureReady()`, used by track(): it awaits an in-flight init, and retries a previously failed one rather than dropping the event. Retries are throttled to once per 5 minutes so a device that genuinely cannot reach AppsFlyer does not re-attempt initSdk on every conversion. - Report init failure with logManager.captureError() so it becomes a visible Sentry issue with a countable population. Deliberately not changed: getId() still returns early when not ready, since callers treat a missing ID as acceptable. No event queue is introduced -- a retry means the event that triggered it is sent once init succeeds, and buffering conversions across a cold start is a larger change than this.
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.
Investigating the report that ~half of Crypto Buy conversions never reach AppsFlyer (week of 6/22: 6,400 buys / $1.65M in Mixpanel vs 3,006 / $761K in AppsFlyer).
What I found
A single failed AppsFlyer init is permanent for the process, and every conversion event after it is dropped in silence.
AppsFlyerWrapper.init()setsstatus = 'failed'wheninitSdkthrows, and swallows the error instead of rethrowing.init()is called from exactly one place —analytics.effects.ts:93, insideAnalytics.initialize(), which is guarded by_isInitialized. Because the AppsFlyer failure never propagates,_isInitializedis set totrueanyway, soinit()is never called again. Nothing readsgetStatus()either.track()returned early on!isReady(). With status stuck at'failed', that is every subsequent event for the rest of the process.logManager.error(), which only writes a local line and a Sentry breadcrumb — notcaptureError— so it produced no Sentry issue and no metric. Nothing could have alerted on this.Analytics.trackfan-out.Point 5 is why the two systems diverge at all: the fan-out in
analytics.effects.tsis symmetric, so any Mixpanel-vs-AppsFlyer gap has to come from a drop inside the AppsFlyer wrapper. This is the only silent one.What this changes
doInit()so it can be driven from more than the publicinit(). This is why the diff is large — the block moved out of the object literal and de-indented. The logic inside is unchanged apart from the two items below.ensureReady(), used bytrack(): awaits an in-flight init, and retries a previously failed one rather than dropping the event. Throttled to one retry per 5 minutes so a device that genuinely cannot reach AppsFlyer does not re-attemptinitSdkon every conversion.logManager.captureError(), so it becomes a visible Sentry issue with a countable population.What I could not verify
I can't prove this accounts for the full ~50%. Two limits:
401 ... credentials have expired), so I could not check the live event data.captureErrorchange is what makes that measurable.Also worth flagging for whoever owns the dashboard: no analytics event in the app sends a
partnerproperty. All sevenPurchased Buy Cryptocall sites sendexchange, always hardcoded per-provider and never unset (scan.effects.ts×6,MoonpayBuyEmbeddedCheckout.tsx:587). The onlypartner:keys in the codebase arelastPurchaseDatain redux (BuyAndSellRoot.tsx:2265) and a Simplex URL payload (simplex-utils.ts:419) — neither reaches analytics. So a Mixpanel breakdown bypartnershowing "(not set)" for 3,326 buys may be measuring a property the app never sends, which would make the partner correlation an artifact of the segmentation rather than a signal. Worth confirming before treating "partner is unset" as the cause.Verification this needs
Left as a draft because the mechanism is proven from code but the magnitude is not:
captureErrorchange and watch for an[AppsFlyer] init failedissue in Sentry — that gives the affected population for the first time.*.appsflyersdk.comat the DNS layer, which a real slice of users already do) and confirm a later Crypto Buy now arrives in AppsFlyer.