Skip to content

fix: retry failed install reports, React Navigation 7 ref types, debug-only logging, peer deps - #15

Merged
onamfc merged 10 commits into
mainfrom
fix/rn-sdk-reliability-and-types
Oct 28, 2026
Merged

onamfc merged 10 commits into
mainfrom
fix/rn-sdk-reliability-and-types

Conversation

@onamfc

@onamfc onamfc commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

Builds on #14 (README accuracy): this branch starts from docs/readme-accuracy, so its first commit (456acda) is #14's. Merge #14 first; after that, this PR shows only its own eight commits.

Fixes

A failed install report is now retried

The install-reported flag (@linkforty:first_launch) was saved before the install report was sent. When the report failed (no connection, a server error, the app killed while it was in flight), it was never sent again: installId stayed null for good and every trackEvent() was dropped with a warning.

Now the flag is saved only once the server accepts the report (a 2xx response), so each later launch reports again until one succeeds. The key keeps its name, so installs already reported by earlier versions are not reported again. A 2xx response without an installId also sets the flag, so it can't loop.

onDeferredDeepLink still gets null on a launch whose report fails, and the launch that later reports successfully calls it with that report's result.

Devices stuck on 1.9.0 or earlier recover once

For the release notes: devices whose first-launch install report failed on 1.9.0 or earlier report their install once after upgrading. These appear as installs dated at the upgrade, usually organic, because the original click is normally outside the attribution window by then.

Those versions left such a device with the first-launch flag set and no install ID, so all its events were dropped.

  • An install now counts as reported when an install ID is stored or a new marker @linkforty:install_report_accepted is set. The marker is set on any 2xx response.
  • A device with the flag but no ID reports once, and is retried if that report fails.
  • A 2xx without an installId sets the marker, so it can't loop.
  • A device with an install ID never reports again.
  • The recovery report does not call onDeferredDeepLink, whether the install is attributed, organic or the report fails. The user has been using the app, so routing them like a fresh install would be wrong. First-launch reports, and their retries, still call it.
  • A recovery report only restores the install ID. Any link the server matches comes from a fingerprint match made at upgrade time, not from the click that led to the install. So it is not saved as install data (getInstallData() stays null) and is not credited for later events. A link the user taps to open the app is credited as usual, through the deep link path.
  • The flag is still written, so a downgraded app doesn't report again.

navigationRef accepts React Navigation 7 refs

NavigationRouteLike.params was Record<string, unknown>. React Navigation 7 types route params as Readonly<object | undefined>, which doesn't assign to that type, so createNavigationContainerRef() (with no param list) failed with TS2322 as navigationRef, with strict on or off. That broke the documented setup and examples/AutoNavTracking.tsx. params is now Readonly<object | undefined>, and sanitizeScreenParams accepts any value and ignores non-objects and arrays at runtime.

Logging only with debug, API key never logged

  • DeepLinkHandler logged URLs and resolved link data with console.log/console.warn on every link and cold start, whatever debug was set to. It now logs only with debug (new optional new DeepLinkHandler({ debug }); the no-argument constructor still works).
  • init() with debug logged the whole config, including apiKey. The key is now [redacted].
  • Without debug, only failures are logged, as before: a failed install report or event, init() called twice, and an event dropped for lack of an install ID. None of these logs include link, device or secret data.

Peer dependencies

  • The react-native peer goes from >=0.64.0 to >=0.65.0. @react-native-async-storage/async-storage 2.x declares react-native: ^0.0.0-0 || >=0.65 <1.0.
  • @react-native-async-storage/async-storage (^2.2.0 || ^3.0.0) and react-native-device-info (^15.0.0) move from dependencies to peerDependencies. Apps had to install both anyway, because autolinking only links native modules listed in the app's own package.json. As regular dependencies they also allowed a second, nested copy. An app on AsyncStorage 3 (npm latest is 3.1.1) got a nested 2.x for the SDK. That 2.x JS looks up the RNCAsyncStorage native module, but 3.x registers RNAsyncStorage, so the SDK's storage was null at runtime. As a peer, the SDK uses the app's one copy.
  • AsyncStorage 3 support: 3.x's default export is its legacy storage, which reads the data 2.x wrote. It keeps getItem/setItem/removeItem but drops multiRemove. clearData() now uses removeItem, so the SDK only calls methods that exist in both versions. The test mock now has only those shared methods.

Deep link listener

  • Each onDeepLink() call added another Linking 'url' listener, so after a second call every link reached the callback twice. The handler now keeps the subscription from Linking.addEventListener and adds it once. Calling onDeepLink() again still replaces the callback, as before: only the newest one is called, once per link.
  • DeepLinkHandler.cleanup() called Linking.removeAllListeners('url'), which also removed the app's own 'url' listeners. It now calls .remove() on its own subscription only.

clearData()

It now also removes the saved link hosts and fetches them again.

Public-repo hygiene

Removed ticket IDs, references to private notes, and internal product wording from source comments and JSDoc. Removed ticket IDs and a private-repository link from past CHANGELOG entries. The hosted service is now described generically in the README and llms.txt.

Not changed

  • deviceModel and appVersion are collected by FingerprintCollector.collect(), but only generateQueryParams() uses them (it's public, and nothing inside the SDK calls it). They're not sent on install or resolve. Removing them would change public output, and sending them would change what install matching gets, so this PR leaves them as they are.
  • clearData() keeps the Linking listener: the SDK stays initialized after clearData() (init() can't run again), so removing the listener would stop deep links until a restart.

Verification

  • npm test: 103 passed. There are 24 new tests: install retry after a network error and after a 503, no re-report once reported or after a 2xx without an id, deferred delivery on the retried launch, no logging without debug, API key redaction, clearData() removing link hosts, sanitizeScreenParams with React Navigation-typed and non-object params, one listener after repeated onDeepLink()/initialize(), and cleanup() leaving the app's own listener, and recovery of a stuck device (reports once, retries after a failure, a 2xx without installId doesn't loop, a device with an install ID never re-reports, no deferred callback on a recovery report, and a recovery report saving neither link data nor a credited link). Against the previous source, 18 of the new tests fail. The device-with-an-install-ID test passes on any version; it guards against re-reporting.
  • npm run build passes. The built dist/ has no ticket IDs or internal references.
  • I type-checked examples/AutoNavTracking.tsx plus typed and untyped refs against @react-navigation/native 7.1.33 and react-native 0.83.2:
    • Before the fix: TS2322 with strict on and off.
    • After the fix: clean with strict on.
    • With strict off, one unrelated React Navigation error remains: Stack.Navigator asks for an id prop.
  • I type-checked src/ against @react-native-async-storage/async-storage 3.1.1's type definitions: no errors.
  • I have not run this on a device or simulator. AsyncStorage 3 runtime behaviour comes from reading its source (legacy storage over the old data), not from an app run.

Release

semantic-release: the fix and docs commits make this a patch release (1.9.1). Please don't publish by hand.

smith-909 and others added 10 commits October 27, 2026 15:57
…s.txt

- Require React Native 0.65+, the floor set by AsyncStorage 2.x
- Install AsyncStorage 2.x and device-info 15.x in the app itself, since
  autolinking only links an app's own dependencies and AsyncStorage 3.x
  registers a different native module name
- Add the AppDelegate / SceneDelegate step that forwards links to
  RCTLinkingManager, and launchMode="singleTask" on Android
- Explain that the server serves the AASA and assetlinks.json files
- Use https://api.linkforty.com for the hosted service, mark appToken as
  required there, apiKey as only needed for createLink()
- Await init() before onDeepLink()
- Document trackRevenue, setExternalUserId, externalUserId, deduplicated,
  the lack of an offline event queue, and the data the SDK sends
- Note what differs on self-hosted Core (createLink templateId, link hosts)
…onRef

NavigationRouteLike.params was Record<string, unknown>, which React
Navigation 7's Readonly<object | undefined> route params don't assign to,
so createNavigationContainerRef() (without a param list) failed to
type-check as navigationRef (TS2322, also with strict off), including the
documented setup and examples/AutoNavTracking.tsx. Params are now typed
Readonly<object | undefined>, and sanitizeScreenParams accepts any value
and ignores non-objects and arrays at runtime.

Also documents apiKey, appToken and debug without internal references,
and drops references to private planning notes from comments.
The install-reported flag was saved before the install report was sent,
so a report that failed (no connection, server error, app killed during
it) was never sent again: the install ID stayed null and every
trackEvent() was dropped. The flag is now saved only once the server
accepts the report, so each later launch reports again until one
succeeds. The storage key is unchanged, so installs already reported are
not reported again.

Logging:
- DeepLinkHandler logged every URL and resolved link data on every link
  and cold start regardless of debug; it now logs only with debug.
- init() with debug logged the whole config including the API key; the
  key is now redacted (and the navigation ref is not dumped).
- Without debug, only failures are logged: a failed install report or
  event, init() called twice, an event dropped for lack of an install ID.

clearData() now also removes the saved link hosts and fetches them again,
and uses removeItem rather than multiRemove.
… AsyncStorage 3

React Native autolinking only links native modules listed in the app's
own package.json, so apps must install both anyway. As regular
dependencies they also let a package manager install a second, nested
copy: an app on AsyncStorage 3 got a nested 2.x for the SDK, whose JS
looks up the RNCAsyncStorage native module while 3.x registers
RNAsyncStorage, so the SDK's storage was null at runtime.

As peers the SDK shares the app's copy. AsyncStorage ^2.2.0 || ^3.0.0:
the SDK only uses getItem, setItem and removeItem, which 3.x's default
(legacy) storage keeps, reading the data 2.x wrote.
react-native-device-info stays ^15.0.0.

The react-native peer floor rises from 0.64 to 0.65, the floor
AsyncStorage 2.x declares.
README and llms.txt: the install report is retried on later launches,
what is logged with and without debug, the native modules as peer
dependencies (AsyncStorage 2.x or 3.x), what clearData() removes, and
that untyped React Navigation 7 refs type-check. Refers to the hosted
service generically and removes ticket IDs and a private repository link
from past CHANGELOG entries.
Each onDeepLink() call added another 'url' listener, so after a second
call every link reached the callback twice. The handler now keeps the
subscription from Linking.addEventListener and adds it only once;
calling onDeepLink() again still replaces the callback, as before.

DeepLinkHandler.cleanup() called Linking.removeAllListeners('url'),
which also removed the app's own 'url' listeners. It now removes only
its own subscription.
…led on 1.9.0 or earlier

Versions up to 1.9.0 set the first-launch flag before sending the install
report, so a failed report left the device with the flag set, no install
ID, and every event dropped. Such a device now reports its install once
after upgrading. It appears as an install dated at the upgrade, usually
organic.

An install now counts as reported when an install ID is stored or a new
marker (@LinkForty:install_report_accepted) is set. The marker is set on
any 2xx response, so a response without an install ID can't trigger a
report on every launch. Failed reports are still retried, and devices
that already have an install ID never report again. The first-launch
flag is still written, so a downgraded app doesn't report again.
…y report

A device recovering from a first-launch report that failed on 1.9.0 or
earlier belongs to a user who has been using the app, so routing them
like a fresh install would be wrong. The recovery report no longer calls
onDeferredDeepLink, whether the install is attributed, organic or the
report fails. First-launch reports, and their retries, still call it.
When a device upgraded from 1.9.0 or earlier reports the install it
failed to report, any link the server matches comes from a fingerprint
match made at upgrade time, not from the click that led to the install.
The recovery report therefore no longer saves that link as install data
(getInstallData() stays null) and no longer makes it the link credited
for later events, whether the install is attributed or organic. It
stores only the install ID and the reported markers. A link tap that
opens the app still sets the credited link through the deep link path.
…y-and-types

# Conflicts:
#	README.md
#	llms.txt
@onamfc
onamfc merged commit b39eeed into main Oct 28, 2026
1 check passed
github-actions Bot pushed a commit that referenced this pull request Oct 28, 2026
## <small>1.9.2 (2026-10-28)</small>

* fix: retry failed install reports, React Navigation 7 ref types, debug-only logging, peer deps (#15) ([b39eeed](b39eeed)), closes [#15](#15)
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.9.2 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants