firebase / firebase/firebase-ios-sdk
[FR]: Analytics: hold events logged while collection is disabled and upload them on enable (ATT-deferred consent)
- Dominant language
- C++
- Stars
- 6.7k
- Forks
- 1.8k
- Avg merge
- 2d 14h
- Merged PRs (30d)
- 72
Description
### Description
**1. Use case**
Our app must not let Firebase Analytics collect anything until the user has answered the App Tracking Transparency prompt, so we start with collection disabled and call `Analytics.setAnalyticsCollectionEnabled(true)` only once ATT resolves.
Every event our app logs in that window is dropped. That window covers the entire opening of a new user's first session — splash, onboarding, login, the first screens of our activation funnel — and it is not short: iOS only presents the ATT prompt while the app is active, we let the UI settle before requesting it, and the user then has to read and answer it.
To avoid losing that data we had to build and maintain our own event queue in front of `logEvent`. Ours is 123 lines: a lock, a bounded backlog, overflow accounting, a deliberate choice of `CLOCK_MONOTONIC_RAW` over `systemUptime` (the holds we care about happen while the device is asleep, so uptime under-reports them), a `gate_held_ms` marker written onto every replayed event, and a downstream convention in BigQuery that reconstructs `real_event_time = event_time - gate_held_ms`. Every app that gates collection on ATT has to write some version of this.
Four problems remain that no app-side queue can solve:
- **It cannot preserve event time.** `logEvent(_:parameters:)` has no timestamp parameter and the `firebase_`, `google_`, and `ga_` parameter prefixes are reserved (FIRAnalytics.h:68-73). Every replayed event is stamped at flush time. We can recover ordering in our own BigQuery queries via the marker above, but GA4's `event_timestamp`, sessionization, and the built-in funnel, path, and retention reports all use the flush instant, so the opening minutes of every new install collapse into a single point.
- **It cannot survive process death.** Our backlog is in memory. The SDK's own event store is on disk. A process that is killed during the ATT window — a background launch that never reaches active and so never prompts, an OOM, a user swipe-kill — loses everything we were holding. We cannot fix this without shipping a second on-disk event store next to the one the SDK already has.
- **It has to guess a bound.** We chose 512 events, dropping oldest on overflow and reporting the count. The SDK does not have to guess; it already manages its own store size and upload scheduling.
- **It cannot see SDK-generated events at all.** Automatic events during the window — `screen_view`, `session_start`, `user_engagement` — never pass through app code, so no app-side queue can hold them. For the ATT window they are simply absent, and that part is not recoverable by any workaround.
**2. How the Firebase SDK currently handles it**
Events logged via `Analytics.logEvent(_:parameters:)` while collection is disabled are not delivered after collection is later enabled (FirebaseAnalytics 12.17.0, SPM). This is not documented on the data-collection page, and `FIRAnalytics.h:100-104` documents only that the setting itself persists.
We are not calling this a bug — collection disabled meaning not collected is a coherent contract. We are asking for a way to opt out of the *discarding* part while keeping the *not uploading* part, because those are two different guarantees and only the second one is what consent actually requires.
**Alternatives we considered**
- **Consent Mode.** We know `Analytics.setConsent` plus the `GOOGLE_ANALYTICS_DEFAULT_ALLOW_*` Info.plist defaults are the documented way to start denied and grant later. Neither the app consent docs nor `FIRAnalytics+Consent.h` state what happens to events logged while `analytics_storage` is `denied`. If those events are retained and sent with their original timestamps on grant, that solves our problem and this request reduces to documenting it — we would take that outcome gladly.
- **Our own queue.** Described above; it is what we ship today, and the four gaps above are why it is not sufficient.
- **Logging nothing before consent.** A total loss of first-session data.
**3. How you propose the Firebase SDK should handle it**
An opt-in, default-off mode in which events passed to `logEvent(_:parameters:)` while collection is disabled are written to the local event store with their original timestamps and uploaded once collection is enabled. Gate the upload stage rather than the logging stage.
Constraints we explicitly accept, since this is a privacy question and not only an engineering one:
- **Opt-in, default off.** No change to what `setAnalyticsCollectionEnabled(false)` means for any existing caller. With the flag off, behavior is exactly what it is today.
- **`FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED` keeps drop-everything semantics.** It is a kill switch, not deferred consent, and nothing here should touch it.
- **No identifiers at hold time.** Held records carry the event name, developer-supplied parameters, and the original timestamp. No IDFA, no app instance ID; identity is applied at flush from the state in effect then.
- **Bounded and discardable.** A cap and a TTL are fine. Discard without uploading on a second `setAnalyticsCollectionEnabled(false)`, on `resetAnalyticsData()`, and if consent never arrives. Nothing leaves the device until collection is enabled.
- Automatic events during the disabled window can stay out of scope. Our own `logEvent` calls are the part we can currently lose and the part we most need.
There is precedent for exactly this inside Firebase: Crashlytics holds unsent reports on device across launches when automatic collection is disabled, specifically so an app can obtain one-time consent, then `sendUnsentReports()` or `deleteUnsentReports()`.
We would file the mirror request against `firebase-android-sdk` and link it — we are not asking for iOS-only semantics.
### API Proposal
Just keep logEvent can work even as `setAnalyticsCollectionEnable(true)`
Or, separate ads features to another SDK.
### Firebase Product(s)
Analytics
Contributor guide
Research direction
Start with FIRAnalytics.h, the collection-enabled API, and FIRAnalytics+Consent.h to compare the documented behavior for disabled collection and denied analytics storage. Trace the iOS Analytics logging and local event-store entry points, then define tests for opt-in retention, timestamp preservation, later upload, bounds, and discard cases. Done means the proposed behavior is specified and covered without changing existing default-off semantics.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- ios, objective-c, swift
- Domain
- analytics, mobile
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100