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.
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 ConnectBundle 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.
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 app039;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.
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.
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.
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.
| ID | Scenario | Expected | Severity | Tool |
|---|---|---|---|---|
| QA-01 | New install → first session fires | install + session_start each fire once | Blocker | Adjust Testing Console |
| QA-02 | ATT prompt Authorized case | IDFA non-empty, attribution reaches sandbox | Blocker | Charles Proxy |
| QA-03 | ATT Denied case | IDFA=00000...0, SKAN-only channel works correctly | Blocker | Console |
| QA-04 | Universal Link cold start | DeeplinkRouter.handle called exactly once | Major | Safari → app switch |
| QA-05 | Deferred deep link (not installed → installed) | onDeeplinkResponse fires on first launch | Blocker | Test Console + Test Device |
| QA-06 | Custom URI scheme (external browser) | Router branches correctly | Major | adb shell am start |
| QA-07 | Background ↔ foreground transition | No duplicate session fires | Major | Adjust Logs |
| QA-08 | Airplane mode → back online | Events queue then send exactly once | Major | Charles |
| QA-09 | SKAN postback (Apple Test Mode) | coarse value [low|medium|high] mapped correctly | Blocker | xcrun simctl + Apple Console |
| QA-10 | Cost data received (UAC/AAP) | cost_amount shows up in the Adjust dashboard | Minor | Adjust > Cost Data |
| QA-11 | in_app_purchase revenue event | revenue + currency correct | Blocker | StoreKit Test |
| QA-12 | Uninstall + Reinstall | Reattribution classified correctly | Major | Adjust > Reattribution |
§4 Troubleshooting / pitfalls
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.
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.
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.
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