Privacy & consent
GDPR, CCPA, ATT, advertising consent. What the SDK does, what you have to do, and the APIs that make it easy.
What Reflect collects, by default
- Install UUID (random GUID, in
PlayerPrefs); Android ID (SSAID) - Device taxonomy: model, manufacturer/brand, type (phone/tablet/tv), OS version + build, CPU, screen size/format/density, RAM, UI mode
- App bundle id, version (+ version code), install source / store, first install + last update time, system-app flag
- Locale, timezone; network type, carrier name + MCC/MNC
- Engagement: session count & length, app foreground state; environment (production/sandbox)
- Fraud signals: emulator, root/jailbreak, VPN, mock-location detection
- Public IP + country (server-side, from CF headers)
What requires explicit consent
All advertising/device identifiers are gated by RequireAdvertisingConsent and scrubbed server-side if the user later denies consent.
- GAID (Android advertising id) + its source/retry — gated by
RequireAdvertisingConsent+ the user’s system "Limit Ad Tracking" setting. - IDFA / IDFV (iOS) — gated by Apple’s ATT prompt; only after the user taps Allow.
- Google App Set ID (Android 12+) — privacy-friendly per-developer id used for matching as GAID availability declines.
- Amazon Fire ID (Fire OS devices only).
- Push token (FCM/APNS) — set via
SetPushTokenfor re-engagement; treated as a quasi-identifier and dropped on consent denial. - External device id — your own cross-system id set via
SetExternalDeviceId; dropped on consent denial. - China only, OFF by default:
IMEI/MEIDandOAID— collected only when you explicitly enableCollectImei/CollectOaidfor China distribution (require extra permissions / the China OAID SDK). Leave off unless you have a lawful basis.
Advertising consent (GDPR / EEA / UK)
If you’re distributing in regions requiring opt-in for cross-app tracking, set:
new ReflectConfig {
RequireAdvertisingConsent = true,
...
}The SDK will NOT collect GAID / IDFA until you call:
ReflectSDK.SetAdvertisingConsent(granted: true);
// or
ReflectSDK.SetAdvertisingConsent(granted: false); // re-collects without ad idsWire this to your CMP (consent management platform) callback. Reflect doesn’t ship a CMP UI — every studio has different brand requirements and we don’t want to lock you into ours.
iOS App Tracking Transparency
Apple requires the ATT prompt before any cross-app tracking on iOS 14.5+. Reflect supports two modes:
// 1) Auto-prompt on first launch
new ReflectConfig {
AutoRequestIosTracking = true,
}
// 2) Manual — call when you have a contextual moment in your UX
ReflectSDK.RequestIosTracking(status => {
Debug.Log("ATT result: " + status);
// status is one of: NotDetermined, Restricted, Denied, Authorized, Unavailable
});The build post-processor injects NSUserTrackingUsageDescription into Info.plist automatically. Edit the wording in Editor/ReflectBuildPostProcessor.cs.
Android — AD_ID permission & Play Console Data Safety
The com.google.android.gms.permission.AD_ID permission (required on Android 13+ to read the GAID) is declared in the SDK’s Plugins/Android/AndroidManifest.xml and merges into your app automatically — you don’t add it. For COPPA / kids apps, remove that line and set RequireAdvertisingConsent = true.
You must still complete two Google Play Console declarations:
- App content → Advertising ID: declare that the app uses an advertising ID (purpose: analytics / attribution). Declaring “no” while the
AD_IDpermission is present triggers a Play review warning. - Data safety: declare collection of Device or other IDs — GAID, Android ID, App Set ID, and (if enabled) Fire ID / IMEI / OAID, plus the push token; App activity (events, sessions); and Purchase history if you call
TrackPurchase. Declare the collection purpose as analytics / attribution and whether data is shared with third parties (mirror yourSetThirdPartySharingsetting).
GDPR / CCPA right-to-be-forgotten
Reflect supports three operations per install_uuid: delete (irreversible wipe), opt-out (drop future events, keep history), and export (downloadable JSON snapshot). Tenant operators submit them from the admin console at Settings → Privacy, or programmatically via the worker:
# Tenant operator triggers from their backend on user request:
POST https://api.reflect.cloud/admin/gdpr/delete
Cookie: reflect_session=<operator session>
{ "install_uuid": "8c1f2a4d-..." }What "delete" actually does:
- Cascade
DELETEacross every install-keyed D1 table:events,attributions,attribution_credits,clicks(joined via attribution),postbacks_log,webhook_deliveries(joined via event_id),ad_revenue_events,fraud_anomaly_scores,reengagement_attributions,touch_log,consent_records. - Sweep all R2 audit blobs tagged with this install_uuid (via the per-install index written at ingest time).
- Insert the install into
data_opt_outsso future events drop at the queue boundary — no D1 write, no attribution, no postback. KV-cached, sub-ms hot-path check. - Log a row in
gdpr_requestswithrows_affected+ the operator email + timestamps. Idempotent: re-submitting a completed delete is a no-op.
SDK-side wipe
The SDK exposes ReflectSDK.DeleteUserData(callback) which:
- Locally: wipes
PlayerPrefs/AsyncStorage/shared_preferences(install_uuid + first-launch flag), drops the offline event queue, clears global properties + user_id. - Server-side: POSTs
/admin/gdpr/deletewith the install_uuid. Returns synchronously with the deletion summary.
The local wipe completes synchronously — even if the server call fails, the user's device is clean. The server-side cascade typically finishes in <500ms because the install is already indexed.
What's NOT deleted by the cascade (legitimate-interest retention):
audit_logentries about the deletion itself (kept for legal record of the operation).- Daily DB backup blobs in R2 (whole-DB disaster-recovery snapshots — separate retention policy).
- Aggregated analytics (cohort retention rollups, hourly counters) — derived from deleted source rows but not directly identifying after aggregation.
Privacy Manifest (iOS) — required by App Store
Apple requires every SDK on the App Store to ship a PrivacyInfo.xcprivacy declaring the data it collects and the required-reason-codes for any restricted API it uses. All three Reflect SDKs (Unity, React Native, Flutter) ship one; you don't need to author it manually.
Where it lives in each SDK
| SDK | Manifest path | Packaging |
|---|---|---|
| Unity | Plugins/iOS/PrivacyInfo.xcprivacy | Build post-processor copies it into the Xcode project automatically |
| React Native | ios/PrivacyInfo.xcprivacy | resource_bundles entry in reflect-react-native.podspec — picked up by pod install |
| Flutter | ios/Classes/PrivacyInfo.xcprivacy | resource_bundles entry in reflect_sdk.podspec — picked up by Flutter's iOS build |
What it declares
The manifest tells Apple that the SDK collects four kinds of data, two of which are linked to user identity:
| Data type | Linked | Tracking | Purposes |
|---|---|---|---|
DeviceID (IDFA / IDFV / install_uuid) | yes | yes | Analytics, Third-Party Advertising |
ProductInteraction (events) | yes | no | Analytics |
PurchaseHistory (revenue, subscriptions) | yes | no | Analytics |
CoarseLocation (server-inferred from IP) | yes | no | Analytics |
Plus two required-reason-code declarations for restricted Apple APIs the SDK uses internally:
| API category | Reason | Why we use it |
|---|---|---|
UserDefaults | CA92.1 | Persist install_uuid + event-queue cursor |
FileTimestamp | C617.1 | Read / write timestamps on the on-disk event buffer |
What YOU still need to do
- Add an app-level
PrivacyInfo.xcprivacyin your iOS project root declaring categories you collect outside of Reflect (email, name, photos, etc.). Apple validates the app manifest in addition to every SDK's manifest. - Add Reflect's attribution endpoint hostname to
NSPrivacyTrackingDomainsin the SDK's manifest. The shipped manifest has an empty array because the hostname is tenant-configurable:
The build post-processor in Unity does this automatically when you configure<key>NSPrivacyTrackingDomains</key> <array> <string>api.reflect.cloud</string> <!-- or your custom domain --> </array>BaseUrl; for React Native and Flutter you edit the manifest file directly afterpod installthe first time, or via a post-install hook. - If your app does NOT do server-side IP geo-lookup, you can remove the
CoarseLocationblock from the SDK manifest before shipping.
Anonymized mode
Not implemented as a single config flag (it’d be a leaky abstraction). Compose:
RequireAdvertisingConsent = true+ don’t callSetAdvertisingConsent(true)— drops GAID/IDFA.- Don’t call
SetUserId— keeps user anonymous. - Don’t pass PII as event properties (email, name) — server-side validation truncates strings to 1024 chars but doesn’t scrub semantic PII.
BaseUrl). No fingerprinting beyond what’s needed for install attribution.