Skip to content

fix(sdk): recover an install the server no longer has instead of refusing its events - #62

Merged
onamfc merged 3 commits into
mainfrom
fix/sdk-event-recovers-unknown-install
Sep 23, 2026
Merged

onamfc merged 3 commits into
mainfrom
fix/sdk-event-recovers-unknown-install

Conversation

@onamfc

@onamfc onamfc commented Sep 23, 2026

Copy link
Copy Markdown
Member

Summary

POST /api/sdk/v1/event answered 404 Install event not found when the installId was not in install_events, and dropped the event. That is permanent from the device's side: the id lives in the app's storage for the life of the install, so every event it will ever send refers to an id the server cannot resolve. There is no code path back.

Any deployment that prunes analytics on a retention window reaches this the moment an install outlives the window. It is also silent — the 404 goes to a mobile client that ignores it, so it appears in edge logs and nowhere else.

Now: an unknown install is recorded again under the id the client sent, and the event is stored against it.

The recovered row claims nothing it cannot know:

  • attribution_method = 'recovered', no link_id, no click_id, no confidence_score — it can never be read as a fresh attributed install.
  • fingerprint_hash = 'recovered:<id>', a marker that cannot equal a real hash, so it cannot match a click by accident.
  • sdk_name / sdk_version from the event, so the row says which client it came from.
  • ON CONFLICT (id) DO NOTHING plus a re-read, so concurrent events for the same install cannot collide.

Creating a row from a client-supplied id adds no exposure that POST /api/sdk/v1/install does not already have: that endpoint is unauthenticated and creates an install row for anyone who calls it. The only difference is who picked the UUID, and it is still only ever their own row.

The 404 that remains (recovery itself could not complete) now carries code: "INSTALL_NOT_FOUND" and action: "reregister" so a client can tell it from a transport error and register a fresh install instead of retrying forever.

Verification

  • npx tsc --noEmit, npm run build, npx vitest run — 20 files, 397 tests pass.
  • src/routes/sdk.event.test.ts gains five cases: the install is recorded again and the event stored; no attribution is claimed; a concurrent request's row is used rather than failing; the remaining 404 carries the code and action; a known install is untouched (two queries, no recovery insert). The earlier "unknown install returns 404" case is replaced — that was the behaviour being fixed.
  • End to end against a real Postgres: first event for a missing install → 200 acknowledged, row created as {"attribution_method":"recovered","link_id":null,"click_id":null,"confidence_score":null,"fingerprint_hash":"recovered:9f1c…"}; the device's next event → 200, still one install row, two events stored; attributed count 0.
  • README documents the behaviour under the SDK endpoints.

Note for multi-tenant hosts

A recovered install is a device that already existed, not a new one. A host that counts install rows as new installs in customer-facing analytics should exclude attribution_method = 'recovered' before adopting this release, or a fleet of recovered devices will read as a spike of installs on the day they recover.

…sing its events

An event whose installId is not in install_events was answered with a 404 and
dropped. The id lives in the app's storage for the life of the install, so
that is not a transient failure: every event that device will ever send
refers to an id the server cannot resolve, and the SDK has no way back. A
deployment that prunes analytics on a retention window reaches this state the
moment an install outlives the window, which is the common case rather than
an edge one, and the failure is invisible — the 404 goes to a mobile client
that ignores it.

The install is now recorded again under the id the client sent and the event
is stored against it. The recovered row claims nothing it cannot know: marked
`recovered`, with no link, click or confidence score, so it can never be read
as a fresh attributed install, and a marker fingerprint that cannot collide
with a real one. Creating a row from a client-supplied id adds no exposure
that /api/sdk/v1/install does not already have, since that endpoint is
unauthenticated and creates install rows for any caller.

The 404 that remains — recovery itself could not complete — now carries
code INSTALL_NOT_FOUND and action reregister, so a client can tell it from a
transport error. The earlier test for the old behaviour is replaced by cases
covering recovery, the concurrent-request race, and that path.
@codecov

codecov Bot commented Sep 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@onamfc
onamfc merged commit d68dc48 into main Sep 23, 2026
14 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 23, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant