Mobile Measurement Partner (MMP)
The MMP plugin attributes app installs, re-engagements, and deep links to the marketing channels that drove them. It also forwards the MMP-assigned user id to PlayOps Analytics so purchase events carry attribution context.
The current implementation is powered by AppsFlyer. All vendor-specific setup is handled by the plugin — your code only deals with the vendor-neutral surface described below.
Requirements
1. External Dependency Manager for Unity (EDM4U)
The MMP plugin pulls Android (Maven) and iOS (CocoaPods) native libraries through Google’s External Dependency Manager. Without it, Android and iOS builds will fail at the linker stage with missing AppsFlyer symbols.
EDM4U is distributed via OpenUPM. Add the scoped registry and the package to your Packages/manifest.json:
{ "scopedRegistries": [ { "name": "OpenUPM", "url": "https://package.openupm.com", "scopes": ["com.google.external-dependency-manager"] } ], "dependencies": { "com.google.external-dependency-manager": "1.2.187" }}After installing EDM4U, run Assets → External Dependency Manager → Android Resolver → Force Resolve at least once so the Android dependencies are written to Assets/Plugins/Android/.
2. iOS App Tracking Transparency (ATT) permission
On iOS 14.5 and later, the IDFA — the identifier AppsFlyer uses to match clicks to installs — is gated behind the App Tracking Transparency prompt. Without ATT authorization, every install reports as Organic.
Install the App Tracking Transparency plugin. It owns the Info.plist injection (NSUserTrackingUsageDescription) and the system prompt, exposed as playOps.AppTracking().
Trigger the prompt from your boot flow before calling Mmp().Start():
playOps.AppTracking().RequestAuthorization(_ =>{ playOps.Mmp().Start();});The MMP plugin tells AppsFlyer to wait up to 300 seconds for ATT authorization before sending its first attribution request, so you have time to show the prompt in your boot flow.
3. Consent
By default the plugin applies a Tactile-defensible consent payload at boot — see Advanced: overriding consent for what it covers, what it leaves unset, and how to override it from your own Consent Management Platform. Review this before shipping if your game ships its own privacy prompt or terms-of-service flow.
Install
The Tactile scoped registry must already be configured. Add the plugin and the AppsFlyer SDK to Packages/manifest.json dependencies:
"com.tactilegames.playops-plugins.mmp": "1.0.0"Pick the AppsFlyer SDK version that matches your game’s Unity IAP integration:
| Your Unity IAP version | AppsFlyer SDK version | Google Play Billing |
|---|---|---|
| Unity IAP 5.x (or no IAP integration) | 6.17.91 | Billing Library 8 |
| Unity IAP 4.x | 6.17.90 | Billing Library 7 |
"com.tactilegames.appsflyer-sdk": "6.17.91"Get your credentials
You need two values before you can configure the plugin: a dev key and an iOS App Store id.
If you already operate the underlying MMP for your title, use the dev key and iOS app id from your existing account — no extra setup is needed on Tactile’s side.
If you do not have an MMP account yet, request both values from your Tactile integration contact — the same person who onboards you to PlayOps. Tactile will provision the account on your behalf so you do not need to set anything up with the provider directly.
Configure
Build a PlayOpsMmpSettings with your AppsFlyer Dev Key and iOS App Store id, then pass it to the PlayOpsSDK constructor.
using PlayOps;using PlayOps.Analytics;using PlayOps.Mmp;
var mmpSettings = new PlayOpsMmpSettings( devKey: "YOUR_APPSFLYER_DEV_KEY", iosAppId: "id123456789");
var playOps = new PlayOpsSDK( new PlayOpsSettings(cloudUrl: "https://…", gameSecret: "…"), new AnalyticsSettings(appId: "yourgame"), mmpSettings);
playOps.Initialize();PlayOpsSDK.Initialize() configures the MMP provider but does not start sending attribution requests yet. Sending starts when you call playOps.Mmp().Start() — see Start the MMP.
Start the MMP
Mmp().Start() is the call that actually sends the first attribution request.
playOps.Mmp().Start();Call Start after PlayOpsSDK.Initialize() and after any iOS App Tracking Transparency prompt your boot flow runs. The plugin deliberately defers the provider’s first attribution request to this call so the request carries the final IDFA / consent state — calling Start earlier would fire that request before the device has supplied either.
If you push consent through UpdateConsent at boot, do that before Start as well.
Idempotent — calling Start more than once is a no-op after the first call.
Log custom events
Send custom in-game events to the MMP provider. Pick stable, snake_case names that map to a step in your funnel:
playOps.Mmp().LogEvent("level_completed_5");Pass extra parameters as a Dictionary<string, string>:
playOps.Mmp().LogEvent("chest_opened", new Dictionary<string, string>{ { "chestId", "1" }});Event values are sent as strings — serialize numbers and booleans at the call site. Events logged before Start are buffered by the provider and flushed once Start is called.
Subscribe to attribution events
Subscribe before calling Start so the very first callback from the MMP provider is delivered.
var mmp = playOps.Mmp();
mmp.AttributionReceived += data => Debug.Log($"Install attribution: media_source={data["media_source"]}");
mmp.AppOpenAttributionReceived += data => Debug.Log($"Re-engagement: media_source={data["media_source"]}");
mmp.DeepLinkReceived += data => Debug.Log($"Deep link: {data["deep_link_value"]}");| Event | Fires when |
|---|---|
AttributionReceived | First session after install. Carries the provider’s full conversion dictionary. |
AppOpenAttributionReceived | App is reopened via an attributed link after install. |
DeepLinkReceived | Direct or deferred deep link arrives on launch or resume. |
The payload is the provider’s raw key-value map — vendor-specific keys are not normalized, so you can route by whatever key your marketing team needs.
Read the MMP user id
string mmpUserId = playOps.Mmp().GetUserId();Returns an empty string until the provider has finished its first attribution callback (typically a few seconds after Start).
Advanced: overriding consent
You can skip this section if your game does not have its own privacy prompt. The plugin already sends a sensible default and attribution works out of the box.
If your game runs its own privacy prompt — through Google’s User Messaging Platform, OneTrust, Didomi, Usercentrics, or a custom UI — you probably end up with four booleans describing what the user agreed to. Pass them through MmpConsent:
| Field | What it controls |
|---|---|
IsUserSubjectToGdpr | Set true for European players, false for everyone else. Leave null if you do not detect the region. |
HasConsentForDataUsage | true if the user agreed to attribution / analytics tracking. Without this, installs report as Organic. |
HasConsentForAdsPersonalization | true if the user agreed to personalized ads. Controls whether their data is shared with ad networks. |
HasConsentForAdStorage | true if the user agreed to ad-related cookies / storage. Required for Google Ads measurement to work on European devices. |
Push the values at boot:
var mmpSettings = new PlayOpsMmpSettings( devKey: "YOUR_DEV_KEY", iosAppId: "id123456789", consent: new MmpConsent( isUserSubjectToGdpr: true, hasConsentForDataUsage: true, hasConsentForAdsPersonalization: false, hasConsentForAdStorage: false));Or update them later when the player changes their preferences in your settings screen:
playOps.Mmp().UpdateConsent(new MmpConsent( isUserSubjectToGdpr: true, hasConsentForDataUsage: true, hasConsentForAdsPersonalization: true, hasConsentForAdStorage: true));Using a TCF-compliant CMP
If your privacy prompt writes a standard “TCF” consent string to the device (most popular CMPs do), set enableTcfDataCollection: true and the provider picks the values up automatically. You only need to pass HasConsentForAdStorage manually — the TCF string does not carry that one.
var mmpSettings = new PlayOpsMmpSettings( devKey: "YOUR_DEV_KEY", iosAppId: "id123456789", enableTcfDataCollection: true, consent: new MmpConsent(hasConsentForAdStorage: true));