1 Foundation · 1-1

Developer Collaboration Guide & Technical PRD

A single document unifying MMP (Adjust) SDK initialization spec, deep-link routing logic, and pre-release QA scenarios. Written so this document doubles as the PRD.

Owner · Growth Eng.Status · In productionAdjust SDK ≥ v5.0.0iOS 14.5+ required
Summary

This document defines the minimum required spec that the dev team must implement to lay the attribution foundation for a new or relaunched app. Only builds that pass all three gates — Adjust SDK v5 initialization → deferred deep-link routing → release QA checklist — are allowed into LIVE campaigns. A single misconfigured line in the SDK can invalidate data across all campaigns, so every item requires code review + QA Lead double sign-off.

§1 Prerequisites

  • Adjust dashboard access: App Token (separate for iOS/Android), Environment (sandbox/production), and Default Tracker issued
  • Bundle ID / Package Name pre-registered. Android uses the applicationId, iOS uses the App Store Connect Bundle Identifier
  • StoreKit 2 + ATTrackingManager dependency and iOS 14.5+ support
  • App Links / Universal Links domain verification files hosted (.well-known/apple-app-site-association, .well-known/assetlinks.json)

§2 Implementation steps

2.1 iOS · Adjust SDK initialization (Swift, AppDelegate)

Initialization must happen after the ATT prompt response. Initializing before the prompt permanently locks IDFA to 0, forcing attribution down to a SKAdNetwork-only channel.

iOS · AppDelegate.swift
import Adjust
import AdjustSdk
import AppTrackingTransparency

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    let appToken = "abc1234xyz9"  class="c">// Production token
    let environment = ADJEnvironmentProduction

    let config = ADJConfig(appToken: appToken,
                           environment: environment,
                           allowSuppressLogLevel: false)
    config?.logLevel = ADJLogLevelInfo
    config?.needsCost = true                 class="c">// Cost data for ROAS calculation
    config?.linkMeMetadata = true            class="c">// Enable LinkMe clipboard matching
    class="c">// attConsentWaitingInterval: max seconds Adjust waits for the ATT response.
    class="c">// Unrelated to how long the ATT prompt itself is shown. 120 is recommended
    class="c">// when maximizing the ATT response rate is the priority.
    config?.attConsentWaitingInterval = 120

    class="c">// SKAN 4.0 ConversionValue update delegate
    config?.delegate = SKANDelegateHandler.shared

    Adjust.appDidLaunch(config)

    class="c">// Trigger the ATT prompt once the user has perceived the app's value
    class="c">// (e.g. after onboarding completes).
    OnboardingState.shared.onCompleted = {
        ATTrackingManager.requestTrackingAuthorization { _ in
            Adjust.requestTrackingAuthorization { status in
                class="c">// status: 0 Not Determined / 3 Authorized
                print("[ATT] status=\(status)")
            }
        }
    }
    return true
}

2.2 Android · Adjust SDK initialization (Kotlin, Application class)

Must be placed at the very top of onCreate in your Application subclass. If it initializes later than external libraries (e.g. Firebase), the first session's install referrer is dropped.

Android · App.kt
import android.app.Application
import com.adjust.sdk.Adjust
import com.adjust.sdk.AdjustConfig
import com.adjust.sdk.LogLevel

class App : Application() {
    override fun onCreate() {
        super.onCreate()

        val appToken = "abc1234xyz9"
        val environment = AdjustConfig.ENVIRONMENT_PRODUCTION

        val config = AdjustConfig(this, appToken, environment).apply {
            setLogLevel(LogLevel.INFO)
            setNeedsCost(true)
            setPreinstallTrackingEnabled(true)        class="c">// Preinstall tracking
            setSendInBackground(true)                 class="c">// Send while backgrounded
            setEventBufferingEnabled(false)           class="c">// false for real-time campaign ops
            setOnAttributionChangedListener { attr ->
                class="c">// network_name / campaign_name / adgroup_name / creative_name
                AppAnalytics.bindAttribution(attr)
            }
            setOnDeeplinkResponseListener { uri ->
                DeeplinkRouter.handle(uri)            class="c">// Deferred deep-link routing
                true
            }
        }

        Adjust.onCreate(config)
        registerActivityLifecycleCallbacks(AdjustLifecycleCallbacks())
    }
}

2.3 Deep-link routing logic (incl. deferred)

Unify three entry points into a single router: (a) Universal Link / App Link, (b) custom URI scheme, (c) Adjust deferred deep link (fires on first launch after a store visit while not yet installed). The router always normalizes to the standard scheme://host/path?params form before branching.

Android · DeeplinkRouter.kt
object DeeplinkRouter {
    fun handle(uri: Uri): Boolean {
        val path = uri.pathSegments.firstOrNull() ?: return openHome()
        return when (path) {
            "product" -> openProduct(uri.lastPathSegment ?: return openHome())
            "promo"   -> openPromo(uri.getQueryParameter("code"))
            "invite"  -> openReferral(uri.getQueryParameter("ref"))
            else      -> openHome()
        }.also {
            Analytics.track("deeplink_opened", mapOf(
                "deeplink_path"   to uri.path.orEmpty(),
                "deeplink_source" to (uri.getQueryParameter("utm_source") ?: "direct"),
                "is_deferred"     to (DeferredFlag.consume())
            ))
        }
    }
}

2.4 Standard URL schema

Network tracker URLs must enforce the schema below on the Adjust Custom URL. All macros use each network's own placeholders — no ad-hoc additions.

Adjust Click URL Template
https://app.adjust.com/{tracker_token}
  ?campaign={campaign_name}
  &adgroup={adgroup_name}
  &creative={creative_name}
  &idfa={idfa}            // Auto-zeroed on iOS 14.5+
  &gps_adid={gps_adid}    // Google Play Services ADID
  &cost_type=cpc
  &cost_amount={cost}
  &cost_currency=USD
  &deeplink=myapp%3A%2F%2Fproduct%2F{product_id}
  &fallback=https%3A%2F%2Fwww.example.com%2Fproduct%2F{product_id}

§3 QA scenarios (release gate)

All 12 scenarios below must PASS before a LIVE build is approved. One failure = build rejected.

IDScenarioExpectedSeverityTool
QA-01New install → first session firesinstall + session_start each fire onceBlockerAdjust Testing Console
QA-02ATT prompt Authorized caseIDFA non-empty, attribution reaches sandboxBlockerCharles Proxy
QA-03ATT Denied caseIDFA=00000...0, SKAN-only channel works correctlyBlockerConsole
QA-04Universal Link cold startDeeplinkRouter.handle called exactly onceMajorSafari → app switch
QA-05Deferred deep link (not installed → installed)onDeeplinkResponse fires on first launchBlockerTest Console + Test Device
QA-06Custom URI scheme (external browser)Router branches correctlyMajoradb shell am start
QA-07Background ↔ foreground transitionNo duplicate session firesMajorAdjust Logs
QA-08Airplane mode → back onlineEvents queue then send exactly onceMajorCharles
QA-09SKAN postback (Apple Test Mode)coarse value [low|medium|high] mapped correctlyBlockerxcrun simctl + Apple Console
QA-10Cost data received (UAC/AAP)cost_amount shows up in the Adjust dashboardMinorAdjust > Cost Data
QA-11in_app_purchase revenue eventrevenue + currency correctBlockerStoreKit Test
QA-12Uninstall + ReinstallReattribution classified correctlyMajorAdjust > Reattribution

§4 Troubleshooting / pitfalls

!
install event fires twice

Happens when Adjust.appDidLaunch is called from both Application and AppDelegate. Consolidate to a single call in the Application class; ActivityLifecycleCallbacks can still be registered, but keep SDK initialization separate from it.

!
iOS attribution shows up as organic only

1) Check whether the ATT prompt was called after SDK initialization. 2) If attConsentWaitingInterval is too short, installs get permanently locked to noIDFA before the user responds — set it to 120 seconds to give a proper response window. 3) Verify the LinkMe domain matches the click URL used by each network.

!
Deferred deep link doesn't fire on first launch

Android: the Play Install Referrer API dependency may be missing — add com.android.installreferrer:installreferrer:2.2. iOS: verify that Universal Links' apple-app-site-association is served over HTTPS with Content-Type=application/json.

i
Using a production token in sandbox by mistake

Separate environments at build time via BuildConfig.DEBUG or #if DEBUG. Using a sandbox token in production still delivers raw events to Adjust, but attribution never fires.

§5 Handoff checklist

Before QA Lead → Growth PM handoff, confirm:

  • Download 1 day of raw export from the Adjust dashboard and verify debug traffic is separated
  • SKAN postback receiving endpoint registered (skadnetwork.adjust.com) and partner re-direct is ON
  • user_id key consistency confirmed with 2nd-party analytics tools (Firebase / Amplitude, etc.)
  • Adjust → BigQuery / S3 export pipeline sync confirmed