# Adapting the plan for a mobile app

A web release replaces every client within minutes. A mobile release does not: old versions keep running for months, devices queue events offline, and device clocks are wrong. The contract needs three extra decisions: which release sends which version, how queued events keep their identity, and which clock reports use.

## Contents

- Release versions
- Offline queues
- Occurrence time and receipt time
- Destination cut-offs for late events
- Workflows and late events
- Mobile acceptance cases

## Release versions

- Add to every mobile contract: `introduced_in` (first app version per platform that sends this contract version) and, when replaced, `superseded_in`.
- Send `app_version` and `build` on every event. Segment's mobile libraries collect `context.app.version` and `build` automatically where possible; check that your path keeps them through to each destination.
- Assume old versions keep sending the old definition until the minimum supported version is raised. Analysts filter or split by `app_version` across a definition change.
- Never reuse an event name for a new meaning on mobile. Old clients will keep emitting the old meaning under the same name, mixing both in every chart.
- For breaking changes, agree the cut-over as "app version X and later" plus a date after which the share of events from older versions is small enough to ignore. Watch that share; do not assume a date.
- When an enforcement tool validates versions (Segment Protocols `context.protocols.event_version`), keep every version that live app versions still send in the plan until those versions are retired.

## Offline queues

- Generate `event_id`, `message_id` and `occurred_at` when the action happens and store them with the queued event. A queue that regenerates them at flush time defeats deduplication and moves the event to the wrong day.
- SDKs batch by design. Mixpanel's Android SDK, for example, sends batches every 60 seconds while the app runs, flushes early when a batch reaches 50 events, and also sends when the app goes to the background. Check the current flush and storage rules of the SDK you use, including what happens to the queue when the app is killed or storage is full.
- Server-confirmed facts (purchase, subscription, account created) should still come from the backend. The app event for the same moment is an intent or UI event with its own name. When it describes the same occurrence, it carries the backend `event_id` as a property, with a reconciliation rule, and its own `message_id` derived from its own name and emitter. Sharing the backend's transport key would make Amplitude drop one of the two when both carry the same `device_id`, and a CDP drop whichever arrives second.
- In-app purchases follow `ecommerce-and-billing.md`: the store or billing backend is the source of truth, and GA4 `transaction_id` deduplication does not cover app streams.

## Occurrence time and receipt time

- Keep both: `occurred_at` from the device and `received_at` from the collector. Report on occurrence time; monitor delay with the difference.
- Device clocks drift and can be set by the user. Destinations correct in different ways:
  - Segment computes `timestamp = receivedAt - (sentAt - originalTimestamp)`, using the device's own send time to cancel the clock offset.
  - Amplitude sets `event_time = client_event_time + (server_received_time - client_upload_time)` when that gap is 60 seconds or more; otherwise `event_time` equals the device time.
  - Write which corrected field the warehouse uses as `occurred_at`, so every report agrees.
- Flag anomalies: `occurred_at` after `received_at`, or `occurred_at` before the app version's release date. Count them; do not silently clamp them.

## Destination cut-offs for late events

Checked 2026-10-17; fetch live before relying on them:

- GA4 ignores events that arrive more than 2 calendar days plus today after they were triggered.
- GA4 Measurement Protocol accepts backdating up to 72 hours (older events are clamped or rejected depending on `validation_behavior`). Segment's GA4 Cloud destination accepts an Event Timestamp up to 3 calendar days back in the property's time zone.
- GA4 `transaction_id` deduplication does not work on app streams, so a duplicated in-app purchase event counts twice in GA4.
- GA4 app streams allow 500 distinct event names per app user, and app reserved names differ from web.
- Mixpanel's `/track` endpoint, which the mobile SDK pages describe their requests going through, does not ingest events with timestamps older than 5 days. Replaying older events through `/import` needs `$insert_id` on every event, 36 bytes or less of letters, digits and `-`; the queued `message_id` fits.
- Amplitude: no age cut-off is stated on the HTTP V2 page; `insert_id` deduplication covers only 7 days.
- Segment drops a repeated `messageId` for at least 24 hours and RudderStack for 7 days. A queue retried after those windows can create duplicates that only the warehouse rule removes.

Decision rule: if the plan's events can realistically sit in a queue longer than a destination's cut-off (field apps, travel apps, poor connectivity markets), the canonical record must come from a path without that cut-off (backend or warehouse), and the destination's counts are documented as partial.

## Workflows and late events

- Reports can restate when late events arrive. Messages and CRM actions cannot be unsent.
- Give every workflow a maximum lateness: "the setup reminder ignores `report_generated` events received more than 2 hours after `occurred_at`" or "a congratulations message is not sent for events older than 24 hours". Put it in the contract's `late_arrival_policy`.

## Mobile acceptance cases

Add to the standard catalogue in `acceptance-tests.md`:

- Airplane mode: perform the action offline, reconnect after 10 minutes. Expect one event with the original `event_id`, `message_id` and `occurred_at`, and `received_at` about 10 minutes later.
- App killed before flush: perform the action, force-quit, reopen. Expect the event to arrive once, or document that the SDK loses it.
- Long offline: hold the queue past the strictest destination cut-off (test device with the clock unchanged). Expect the canonical path to keep the event and the destination behaviour to match the documented cut-off.
- Wrong device clock: set the clock back one day. Expect the corrected occurrence time used by the warehouse to be within the documented tolerance.
- Old and new app versions side by side after a breaking change: expect both definitions distinguishable by `app_version` and the metric to follow the documented cut-over.
- Reinstall and login: expect the same `user_id` and a new device or anonymous ID, linked after login.
