Technical
6 min read

How to Get Full Attribution Coverage Between Adjust and Firebase

A one-time SDK setup for game studios: cross-write Adjust ADID, Firebase user_pseudo_id, and your own user_id so installs join cleanly across both systems. AppsFlyer, Singular, and Branch follow the same pattern.

Sabri Karagonen

Data & Product

How to Get Full Attribution Coverage Between Adjust and Firebase

A game studio I worked with could only match 60% of their Firebase installs to an Adjust record. Forty percent of new users were ghosts. They had no idea where those users came from.

The fix was three lines of SDK code. Coverage went to 98% within a week of the next app release.

If you wire this up once during your Adjust integration, you never have to think about it again. The rest of this post is what to put in those three lines.

Why the gap exists

Adjust and Firebase run in the same app but generate different IDs:

  • Firebase stamps every install with a user_pseudo_id. Anonymous, persistent, and the primary key in BigQuery's events_* table.
  • Adjust stamps every install with an adid. The primary key on the Adjust side.
  • Your backend has its own user_id once the user logs in.

If you do not explicitly tell each SDK about the others, you cannot join them later.

Most of the 40% gap was iOS users who declined ATT. Without an IDFA, Adjust falls back to probabilistic matching, and a chunk of those users never get attributed. The same thing happens on Android when users reset their GAID. Those installs end up in Firebase with no campaign attached.

The three mappings

Here are the three SDK calls the studio added in their next iOS and Android release:

1. Send Adjust ADID to Firebase

When the Adjust SDK initializes, grab the adid and write it to Firebase as a user property.

// Android, Kotlin
Adjust.getAdid { adid ->
    if (adid != null) {
        FirebaseAnalytics.getInstance(context)
            .setUserProperty("adjust_adid", adid)
    }
}
// iOS, Swift
Adjust.adid { adid in
    if let adid = adid {
        Analytics.setUserProperty(adid, forName: "adjust_adid")
    }
}

Now every Firebase event for that user carries the Adjust ID. In BigQuery, you can join events_* to the Adjust raw export on user_properties.adjust_adid = adid.

2. Send Firebase user_pseudo_id to Adjust

Firebase exposes its anonymous ID through getAppInstanceId(). Send it to Adjust as a session callback parameter or as a partner parameter.

FirebaseAnalytics.getInstance(context).appInstanceId
    .addOnSuccessListener { id ->
        Adjust.addGlobalCallbackParameter("firebase_pseudo_id", id)
    }

This gives you a second join key, useful when the ADID-side write fails or arrives late.

3. Send your own user_id to Adjust on login

The moment a user logs in or creates an account, push your internal user_id to Adjust as a callback parameter, and set it as a Firebase user property at the same time.

Adjust.addGlobalCallbackParameter("user_id", currentUser.id)
FirebaseAnalytics.getInstance(context).setUserId(currentUser.id)

Now your authenticated users are joinable by user_id across your warehouse, CRM, backend events, Firebase, and Adjust.

AppsFlyer, Singular, Branch

Same idea on AppsFlyer, Singular, and Branch, only the method names change.

  • AppsFlyer: read appsflyer_id via AppsFlyerLib.getAppsFlyerUID(), write to Firebase. Send your customer_user_id with setCustomerUserId() and the Firebase pseudo ID via setAdditionalData().
  • Singular: read Singular.getSingularDeviceId(), write to Firebase. Send your user_id with Singular.setCustomUserId(). Pass firebase_pseudo_id as a global property.
  • Branch: read the install ID from Branch.getInstance().getFirstReferringParams(), write to Firebase. Call setIdentity(userId) on login.

If you only remember one thing: every ID your stack generates should be readable from every other system.

"Isn't pushing IDs around a privacy issue?" Not if your consent flow is set up correctly.

  • IDFA is gated by ATT. If the user declines, you don't have an IDFA to push anywhere.
  • Adjust ADID is not a device ID. It's Adjust's own identifier derived from IDFV plus signals. Sharing it with your own Firebase project is fine.
  • Firebase user_pseudo_id is anonymous, Google treats it as non-PII.
  • Your own user_id: if it's an email, hash it before sending. If it's a UUID, you're fine.

As long as your privacy policy mentions you use an MMP and analytics provider, and you respect ATT and GDPR, this is exactly what those vendors expect. If you're in the EU and want to be safe, run it past your DPO.

What to do Monday

Open the ticket for your next Adjust release and add three SDK calls:

  1. After Adjust.onCreate(), fetch the ADID and write it as a Firebase user property.
  2. After FirebaseAnalytics initializes, fetch the appInstanceId and pass it as an Adjust global callback parameter.
  3. In your login handler, push the authenticated user_id to both SDKs.

Ship, wait two weeks, then count how many Firebase installs have a matching adid in your Adjust raw export. If you were near 60%, you'll be near 95%.

Half a day of SDK work, and you can finally tell where your users came from.