Mobile SDKsIdentity & Device IDs

Identity & Device IDs

The Datafly mobile SDKs use a multi-layered identity model to track users across sessions, app reinstalls, and the transition from anonymous to known user.

Bridging identities

Two envelope fields let you tell the server that two identities are the same person, or that an event has already been reported elsewhere.

setPreviousId — app-to-web handoff and logout

reset() mints a fresh anonymous ID. The SDK automatically carries the previous one on the next event, so the server merges the two identities instead of treating a logout/login cycle as two different people.

Call it explicitly when you know the other surface’s ID — for example, when your app opens a web view that already has a _dfid:

Datafly.shared.setPreviousId("the-web-anonymous-id")
Datafly.setPreviousId("the-web-anonymous-id")
Datafly.setPreviousId('the-web-anonymous-id');

The ID is sent once, on the next event. Repeating it every event would force a redundant identity lookup server-side.

event_id — deduplicating against an existing pixel

If you run a vendor SDK alongside Signal (a Meta SDK reporting the same purchase, say), pass a shared event_id so the vendor deduplicates rather than counting the conversion twice. Use the same value you pass to the vendor’s own SDK — typically your order ID.

Datafly.shared.track("purchase", properties: [
    "event_id": orderId,
    "revenue": 99.99,
])
Datafly.track("purchase", mapOf(
    "event_id" to orderId,
    "revenue" to 99.99,
))

The SDK lifts event_id (or eventId) out of your properties into the event envelope, so it never reaches a vendor as an ordinary property. Meta CAPI and TikTok Events API both receive it as their native event_id. If you omit it, the delivery layer falls back to the SDK’s own message ID, which deduplicates Signal’s own retries but cannot match a vendor SDK’s events.

Collected IDs — identifiers another SDK owns

Some identifiers cannot be generated on-device because a different SDK owns them. The Firebase app instance ID is the case that matters: the GA4 Measurement Protocol requires app_instance_id for app properties, and only the Firebase SDK can produce it.

Read it from Firebase and hand it over:

Analytics.appInstanceID { id in
    if let id { Datafly.shared.setCollectedId("firebase_app_instance_id", value: id) }
}
FirebaseAnalytics.getInstance(context).appInstanceId
    .addOnSuccessListener { Datafly.setCollectedId("firebase_app_instance_id", it) }
Datafly.setCollectedId('firebase_app_instance_id', appInstanceId);

Blueprints read these as collected_ids.<key>. Values are persisted, survive a reset() (they describe the app install, not the person) and are sent once per change — the server stores them against the canonical identity, so resending on every event would be waste.

⚠️

Without this, no mobile install or in-app conversion can reach a GA4 app property or Firebase. Datafly cannot generate the value for you.

Identity layers

LayerWhat it isWho sets itPersistence
Anonymous IDUUID v4 generated on first launchSDK (client)iOS: Keychain / Android: SharedPreferences
Device IDIDFV (iOS) or ANDROID_ID (Android)OS (automatic)System-managed
IDFA / AAIDAdvertising identifierApp (after consent)System-managed
User IDYour user’s database IDApp (via identify())Memory (cleared on reset())
Canonical IDUnified server-side identityServer (automatic)Server-side

How it works

1. App install (new user)

On first launch, the SDK:

  1. Generates a new anonymous ID (UUID v4)
  2. Collects the device ID (IDFV on iOS, ANDROID_ID on Android)
  3. Self-generates vendor tracking IDs (GA4 client_id, Meta fbp, TikTok ttp, Pinterest _pin_unauth)
  4. Starts session 1

The first event sent to the server creates a canonical identity that links all these signals:

canonical:org123:uuid-canonical-1
  ├── link:dfid:uuid-anonymous-1      (anonymous ID)
  ├── link:idfv:uuid-vendor-1         (device ID)
  └── vendor_ids: { ga_client_id, fbp, ttp, pin_unauth }

2. User logs in

When the app calls identify("user-123"):

canonical:org123:uuid-canonical-1
  ├── link:dfid:uuid-anonymous-1
  ├── link:idfv:uuid-vendor-1
  ├── link:user_id:user-123           (now linked)
  └── vendor_ids: { ga_client_id, fbp, ttp, pin_unauth }

All pre-login events are now attributed to the identified user.

3. App reinstall

When the app is reinstalled, a new anonymous ID is generated. The server recovers the identity through device recognition:

New anonymous ID: uuid-anonymous-2
Device recognition hash matches → same device
Server links: dfid:uuid-anonymous-2 → canonical:uuid-canonical-1

Result: same canonical identity, continuous user history

Device recognition

The server computes a SHA-256 hash from multiple device signals to identify returning devices:

SignalWeightSource
IP address25Request headers
User agent25SDK-generated (includes device model, OS version)
Screen resolution15Device info
Accept-Language15Request headers
Timezone10Device locale
Locale10Device locale

If the hash matches a previously seen device (confidence threshold met), the server links the new anonymous ID to the existing canonical identity.

Device recognition includes a collision cap (default 15 distinct anonymous IDs per hash). If exceeded — for example on a corporate network where many devices share the same IP and user agent — the hash is marked unreliable and device recognition is disabled for that fingerprint.

Identity correction

After processing device recognition, the server may return a corrected_anonymous_id in the batch response. The SDK automatically updates its stored anonymous ID to match:

SDK sends event with anonymousId: "uuid-anonymous-2"
Server recognises device → returns corrected_anonymous_id: "uuid-anonymous-1"
SDK updates Keychain/SharedPreferences with corrected ID
Subsequent events use the corrected ID

Platform-specific persistence

iOS

DataStorageSurvives reinstallSurvives factory reset
Anonymous IDKeychain (kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly)YesNo
Anonymous ID backupUserDefaultsNoNo
IDFVSystemYes (same vendor)No
Vendor IDsUserDefaultsNoNo
Session numberUserDefaultsNoNo

The Keychain recovery chain: Keychain → UserDefaults backup → generate new.

Android

DataStorageSurvives reinstallSurvives factory reset
Anonymous IDSharedPreferencesNoNo
ANDROID_IDSystem (per-signing-key since Android 8.0)YesNo
Vendor IDsSharedPreferencesNoNo
Session numberSharedPreferencesNoNo
⚠️

Android has weaker client-side persistence than iOS. On app reinstall, a new anonymous ID is generated and vendor IDs are regenerated. The server-side device recognition system compensates by linking the new anonymous ID back to the existing canonical identity.

Vendor ID generation

On first launch, the SDK self-generates tracking IDs that match the format each vendor expects:

VendorID nameFormatExample
GA4ga_client_id{random}.{timestamp}7234856102.1712498234
Metafbpfb.1.{timestamp_ms}.{random}fb.1.1712498234567.8765432109
TikTokttp27-char base36abcdefghijklmnopqrstuvwxyz1
Pinterestpin_unauthUUID v46f4f9e2a-1b3c-4d5e-8f9a-0b1c2d3e4f5a

These are sent once on the first event, then cached server-side. This means you don’t need to embed Meta SDK, GA4 SDK, etc. in your app — Datafly generates compatible IDs natively.

Advertising IDs (IDFA / AAID)

Advertising IDs require explicit user consent and must be set by the app after permission is granted:

iOS (IDFA)

import AppTrackingTransparency
import AdSupport
 
ATTrackingManager.requestTrackingAuthorization { status in
    if status == .authorized {
        let idfa = ASIdentifierManager.shared().advertisingIdentifier.uuidString
        Datafly.shared.setAdvertisingId(idfa, enabled: true)
    }
}

Android (AAID)

// Using Google Play Services (separate dependency)
val adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context)
if (!adInfo.isLimitAdTrackingEnabled) {
    Datafly.setAdvertisingId(adInfo.id, enabled = true)
}
⚠️

Never call setAdvertisingId() without first obtaining user consent. On iOS, this requires the App Tracking Transparency (ATT) prompt. On Android, respect the user’s “Opt out of Ads Personalization” setting.

Logout / reset

Call reset() when the user logs out:

Datafly.shared.reset()

This:

  1. Generates a new anonymous ID
  2. Clears user ID and traits
  3. Clears group ID and traits
  4. Regenerates vendor IDs
  5. Does not clear the event queue (pending events are still delivered)