Skip to content

feat: keep events on the device until the server has them, and store each once - #14

Merged
onamfc merged 2 commits into
mainfrom
feat/durable-events
Oct 21, 2026
Merged

onamfc merged 2 commits into
mainfrom
feat/durable-events

Conversation

@onamfc

@onamfc onamfc commented Oct 21, 2026

Copy link
Copy Markdown
Member

Summary

Events are now kept on the device until the server has them, and each carries an id the server uses to store it once.

  • clientEventId. Every event gets a UUID when it is tracked (trackEvent, trackRevenue, trackScreenView), sent as clientEventId and kept through every retry.
  • Stored queue. The queue (up to 100 events) is kept in SharedPreferences and restored on initialize(). clearData() and clearEventQueue() clear it.
  • Store before send. Each event is stored before it is sent and removed only after the server answers, so an app closed mid-send keeps it; the resend carries the same clientEventId.
  • When queued events go out: when initialize() completes, when the app returns to the foreground, after the next event that sends, and on flushEvents(). Flushes run one at a time, so concurrent triggers send each event once.
  • No new permission. The foreground is detected from Application.ActivityLifecycleCallbacks (started-activity count 0 → 1); the network connection is not watched, and no dependency is added.

Behavior change

trackEvent, trackRevenue and trackScreenView return normally when the event can't be sent now (no connection, or a 5xx) and is queued. They used to throw NetworkError / InvalidResponse while queueing it. They still throw for invalid input, when not initialized, and when the server rejects the event (4xx); a rejected event is not kept. Listed under Changed in the CHANGELOG.

Server

Storing a resent event once needs a server whose event endpoint recognizes clientEventId (@linkforty/core with client event ID support). An older server ignores the field; events are then delivered at least once, and a resend after a lost reply can be stored twice.

Tests

  • Unit: 181 passed (171 before). New DurableEventsTest (9): id format and the same id on retry; different ids per event; queue restored by a new tracker with integers and nested values intact; stored before sent; a 4xx throws and is not kept; clearing clears storage; four concurrent flushes send three events exactly once; an unreadable stored queue is discarded; the id survives storage. EventTrackerTest: returns normally on a 5xx and offline.
  • Without the change (only EventTracker.kt reverted, since the tests use the new storage API): 8 of those tests fail.
  • Instrumented (API 34 emulator): 17 passed, including eventQueuedOnServerErrorsSurvivesARelaunchAndIsSentOnceWithTheSameId: three 503s, trackEvent returns normally, then after a relaunch the event is sent once with the same id.
  • Lint: clean (no new findings).
  • E2E (e2e/run.mjs, emulator + Chrome, local server with the new Core): 76 passed, 0 failed. New scenarios in the shared harness: an event tracked offline survives the app closing and is stored once; an event whose reply is lost is stored once when it is sent again; queued events are sent when the app returns to the foreground, without a new launch.

Also: API.md no longer says to register callbacks again after a failed initialize() (they're kept since 1.4.0).

…th a clientEventId

Each event carries a clientEventId generated when it is tracked and kept
through retries, so a server that recognizes it stores an event once. The
event queue is stored in SharedPreferences: an event is stored before it
is sent and removed once the server has it, so unsent events survive the
app being closed. Queued events are sent when initialize() completes, when
the app returns to the foreground (activity lifecycle callbacks; no new
permission), after the next event that sends, and on flushEvents().
Flushes run one at a time.

trackEvent, trackRevenue and trackScreenView return normally when the
event is queued for retry (no connection, or a 5xx); they still throw for
invalid input, when not initialized, and for a 4xx rejection.
…e, and the foreground sends the queue

Shared harness: the proxy can drop event requests or cut the connection
after the server replies, and three scenarios check that an offline event
is stored once after the app is closed and reopened, that an event whose
reply was lost is stored once when resent, and that queued events go out
when the app returns to the foreground. The Android device sends the app
home and brings the running task back. API.md: a failed initialize()
keeps registered callbacks.
@onamfc
onamfc merged commit 6d19850 into main Oct 21, 2026
7 checks passed
@onamfc onamfc mentioned this pull request Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants