Marketing + ATT Pipeline (iOS)
End-to-end skill for iOS marketing channel attribution under Apple's App Tracking Transparency. Merges audit framework (ads-apple), AdAttributionKit reference (adattributionkit), and SDK consent gating (managing-mobile-app-consent), plus anonymized production incident learnings.
When To Use This Skill
Activate this skill when ANY of the following is true:
- New iOS project integrating a marketing SDK (AppsFlyer, Adjust, Branch, Singular, Meta, Firebase Analytics, AdMob).
- Existing iOS app showing attribution loss after iOS 14.5: organic install surge, MMP dashboard "Organic" share rising, Meta AEM events missing, conversion value postbacks empty.
- ATT prompt timing question (when to show, what wait interval, app-launch race conditions).
- SKAdNetwork ↔ AdAttributionKit migration or dual-attribution setup.
- Pre-launch checklist for an iOS app that runs paid UA.
- Audit request: "is our iOS attribution stack healthy?"
If the user mentions any keyword from the description's keyword list, this skill should fire.
Workflow / Operating Sequence
For any task, follow WORKFLOW.md — it defines the standard 5-stage flow (CONTEXT → ROUTE → DIAGNOSE → ACT → VERIFY) and which reference files to consume in what order. Read it FIRST before diving into the decision tree below.
Decision Tree (Entry Point)
What is the user asking?
│
├─ "Install / event attribution missing or going Organic"
│ └─→ references/att-timing-and-events.md
│ Cover: SDK init order, attConsentWaitingInterval semantics,
│ trackEvent vs install payload gap, capture-protection silent-defer,
│ Adjust v5 enableFirstSessionDelay, Facebook AppEvents denied path
│
├─ "Meta AEM not visible / ATE verification error / Meta + AppsFlyer setup"
│ └─→ references/meta-aem-troubleshoot.md
│ Cover: domain verification, app status prerequisites, CUID role,
│ ATE postback debug, RN/Expo gotchas, AEM auto-aggregation (post Jun 2025)
│
├─ "TikTok ad attribution / SKAN ownership / TikTok SDK init"
│ └─→ references/tiktok-integration.md
│ Cover: TikTokBusinessSDK init, disableSKAdNetworkSupport(),
│ ATT delay controls, SKAN ownership decision matrix,
│ hybrid MMP+SDK setup, Events API S2S
│
├─ "ATT prompt design / consent flow / SDK gating"
│ └─→ references/consent-gating.md
│ Cover: ATT 4-state matrix, pre-permission prompt pattern,
│ NSUserTrackingUsageDescription wording, per-SDK init pattern
│ (AppsFlyer, AdMob, Firebase, Crashlytics, Meta), Android parity
│
├─ "AdAttributionKit / SKAdNetwork conversion values / postbacks"
│ └─→ references/adattributionkit-and-skan.md
│ Cover: 3 conversion windows, tier 0-3 postback granularity,
│ fine vs coarse values, lockPostback timing, AttributionCopyEndpoint,
│ dual attribution since Apr 2025, Apple Ads AAK + AdServices API
│
├─ "Audit our Apple Ads + MMP setup" / "ASA health check"
│ └─→ references/apple-ads-audit.md (uses ads-apple framework)
│ Cover: campaign structure, bid health, CPP, MMP integration check,
│ AdAttributionKit dual attribution, ASA Health Score 0-100
│
├─ "Pre-launch / new project setup"
│ └─→ references/pre-launch-checklist.md
│ Cover: Info.plist keys, ATT description, SKAdNetworkItems,
│ PrivacyInfo.xcprivacy, AdNetworkIdentifiers, AASA file,
│ MMP DevKey/AppID config, conversion value mapping, test postbacks
│
└─ "I don't know what's wrong, attribution feels off"
└─→ scripts/diagnose.sh + references/symptom-to-cause.md
Cover: symptom → likely cause → which reference to read next
Core Concepts (Read Before Diving)
ATT 4-State Behavior Matrix
| State | Meaning | IDFA | MMP Action | Marketing SDK Action |
|---|---|---|---|---|
.notDetermined | User not asked yet | All zeros | Configure SDK wait/delay before start; do not send first session before ATT response or timeout | Init in deferred mode; queue unmanaged events |
.authorized | User allowed | Real IDFA | Send full payload with IDFA | Enable user-level tracking |
.denied | User declined | All zeros | Send aggregate-only via SKAN/AAK | Continue only consent-allowed limited/aggregate events; no IDFA/user-level tracking |
.restricted | MDM/parental block | All zeros | Do NOT prompt (requestTrackingAuthorization will fail silently); use SKAN | SKAN-only mode |
Critical: .notDetermined is the dangerous state. If your MMP SDK fires start() here without a wait interval, install payload goes out with zero IDFA → MMP can never re-attribute it later → permanent "Organic" misclassification.
The Three Layers of iOS Attribution (post iOS 14.5)
┌─────────────────────────────────────────────────────────┐
│ Layer 1: Deterministic (IDFA-based) │
│ Requires ATT .authorized │
│ ~25-40% global opt-in (varies by category) │
│ → Full user-level events to AppsFlyer/Adjust/Branch │
├─────────────────────────────────────────────────────────┤
│ Layer 2: SKAdNetwork (SKAN v3/v4) + AdAttributionKit │
│ Privacy-preserving, no user-level data │
│ Apple-mediated postbacks 24-48h delay (1st window) │
│ Conversion values: fine 0-63 OR coarse low/medium/high │
│ → MMP receives postback, maps CV → revenue/funnel step │
├─────────────────────────────────────────────────────────┤
│ Layer 3: Probabilistic / modeled (MMP-side) │
│ Statistical fingerprinting where allowed by Apple │
│ Modeled conversions for non-consented users │
└─────────────────────────────────────────────────────────┘
A healthy stack uses all three simultaneously. If only one is wired, you're losing 30-70% of attribution coverage.
Two Most Common Production Failures
-
The ATT wait/delay scope trap. MMP wait APIs are SDK-specific. AppsFlyer queues the launch event and consecutive in-app events during
waitForATTUserAuthorization; AdjustattConsentWaitingIntervaldelays first-session activity and first-session delay queues recorded packages until released. The trap is assuming those SDK queues cover every pipe: SDK-external events, backend/CAPI sends, Meta/TikTok direct SDK events, and events emitted before wait config still need explicit ATT/privacy gating. Fix inreferences/att-timing-and-events.md. -
Silent Consent-Denied Paths. Marketing SDKs often have a code path where
requestTrackingAuthorizationdenied/notDetermined results in early-return without logging. Looks like "everything works" until you check the dashboard and see a 50% drop in events. Fix inreferences/consent-gating.md.
Channel-Specific Quick Reference
| Channel | iOS Attribution Mechanism | ATT Dependency | Key SDK |
|---|---|---|---|
| Apple Ads (ASA) | AdServices token + AAK/SKAN where supported | Not IDFA-dependent; ATT can affect payload detail/modeling | None or MMP SDK |
| Google App Campaigns | SKAdNetwork + Firebase events | Required for user-level/ad-personalization links | Firebase + Google Ads SDK |
| Meta App Install | AEM + SKAN + SDK/MMP/CAPI app events | ATT authorized enables IDFA-level SDK signals; AEM covers limited users after setup | Meta SDK, MMP S2S, or CAPI |
| TikTok App Install | SKAN + SDK/S2S events | Required for IDFA/user-level signals; limited events still need privacy gating | TikTok SDK or MMP |
| Snap App Install | SKAN | Required | Snap SDK or MMP |
| Branch / Adjust / AppsFlyer / Singular (MMP) | Aggregator across all above + IDFA | Required for user-level | Their SDK |
Output Pattern
When invoked, the skill should:
- Identify the sub-problem using the decision tree above.
- Load the relevant reference file(s) from
references/. - Apply the diagnosis or pattern from that reference.
- Reference the exact code call site (file:line) in the user's project when fixing.
- For audits: produce a scored