Repository navigation
feat: register link handlers during init(), bound every request, and report how the install report ended - #16
Merged
Conversation
…report how the install report ended
init() now creates the deep link handler and the link-hosts list before its first await, so onDeepLink() may be called right after init() without awaiting it. Until now a handler registered while init() was running was built without the link hosts and then replaced by init(), which left its Linking listener live for the session: links on the workspace's other hosts were never recognized by it, and a second onDeepLink() call added a second listener, so every link was delivered twice. The handler is now the one init() created, it receives the link hosts when they arrive, and there is one listener however often onDeepLink() is called.
Every request is abandoned after requestTimeoutMs (default 10 seconds). React Native sets no connect, read or write timeout on Android, so a server that accepted the connection and never answered left init() pending for the session. An abandoned request rejects with code 'timeout', and an abandoned install report is retried on the next launch like any other failure. The device's user agent lookup is bounded too (3 seconds): on iOS it runs a WKWebView on the main thread and can fail to settle, in which case a generated `<app>/<version> iOS/<os>` user agent is sent, the form the native SDKs use, which the server matches by platform and OS version.
onDeferredDeepLink callbacks receive a second argument, { status: 'attributed' | 'organic' | 'failed', error? }, so an app can tell an organic install from a report that failed and will be retried. The callback is still called with null in both cases, because apps use it as the signal that the routing decision is in; a callback that takes only the data keeps working. The delivery now happens once, after the report's try/catch, so a callback that throws surfaces as such instead of being logged as a failed report and followed by a second, contradicting delivery.
Events tracked while init() is still running are held (up to 100) and sent with their original timestamp once init() has the install ID, instead of being dropped; if that launch's report fails they are dropped with one warning.
init() no longer rejects when storage fails (the AsyncStorage native module not linked): the failure is logged once per launch, a read counts as absent and a write is skipped, so the SDK behaves as on a first launch and keeps working for the session. The only error init() rejects with is one thrown by the app's own onDeferredDeepLink callback.
An HTTP error response is now reported as `HTTP <status>` with the body's message or error appended when the body is JSON; a non-JSON body (a proxy's error page) no longer reads as "Network error".
…erred link once initialized, query the launch URL once A second init() call, including one made while the first is still running (React 18 StrictMode runs effects twice in development), now logs a warning and returns the first call's promise. Until now the guard was the initialized flag, set only at the end, so a second call during the first replaced the handler and the link-hosts list while the first handler kept its Linking listener: two listeners, every link delivered twice. The deferred deep link is delivered by init() after the SDK is initialized, so an app callback that throws rejects init() but leaves a working SDK behind, and awaiting init() no longer waits on the sends of events held during it (sendEvent never rejects). DeepLinkHandler asks for the launch URL once per handler, so re-registering onDeepLink() (an effect whose callback identity changes) no longer delivers the launch URL again each time.
|
🎉 This PR is included in version 1.10.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
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.
What changes
onDeepLink()right afterinit(), without awaiting it, is now supported.init()creates the deep link handler and the link-hosts list before its firstawait. Previously a handler registered duringinit()was built without the link hosts and then replaced at the end ofinit(), which left itsLinkinglistener alive for the session: links on the workspace's other hosts were never recognized by it, and a secondonDeepLink()call added a second listener, so every link arrived twice. Now there is one handler, it receives the link hosts when they arrive (a link handled before then waits up to 3 s, as before), and one listener however oftenonDeepLink()is called.Every request has a deadline (
requestTimeoutMs, default 10 s). React Native sets no connect/read/write timeout on Android, so a server that accepted the connection and never answered leftinit()pending. An abandoned request rejects withcode: 'timeout'; an abandoned install report is retried on the next launch. The user agent lookup is bounded too (3 s): on iOS it runs aWKWebViewon the main thread and can fail to settle, in which case a generated<app>/<version> iOS/<os>user agent is sent, the form the native SDKs use, which the server already matches by platform and OS version.onDeferredDeepLinkcallbacks get a second argument,{ status: 'attributed' | 'organic' | 'failed', error? }, so a failed report (retried next launch) is distinguishable from an organic install. The callback is still called withnullin both cases, since apps use it as the "routing decision is in" signal; one-argument callbacks keep working. Delivery now happens once, after the report's try/catch, so a callback that throws surfaces as such rather than being logged as a failed report and followed by a secondnulldelivery.Events tracked while
init()runs are held (up to 100) and sent with their original timestamp once the install ID is known, instead of dropped. If that launch's report fails they are dropped with one warning.init()never rejects for a storage failure (AsyncStorage native module not linked). It logs the cause once per launch and behaves as on a first launch, so the SDK keeps working for the session. The only errorinit()rejects with is one thrown by the app's ownonDeferredDeepLinkcallback.HTTP errors name the status:
HTTP 502, orHTTP 400: <message>when the body is JSON. A proxy's HTML error page no longer reads as "Network error".Not changed, deliberately
No terminal state for a 4xx install response. An install that is never reported is a permanent loss (the reason for the recovery logic added in 1.9.2), and a transient server-side 400 would make that permanent for every device that launched during it. One small request per cold start is the cheaper failure. The install endpoint has no auth, so 401/403 do not occur there.
Tests
29 new or updated cases: registration during
init()with a workspace link host, the request deadline (fake timers, default and configured), the three callback statuses, a throwing callback, held events (sent / dropped), broken storage, error messages, and the user agent fallback.npm test: 118 passing.npm run buildclean.