AppsFlyer¶
Herald's AppsFlyer module sends attribution and conversion events to AppsFlyer. AppsFlyer only gets
the events you choose: your conversions, under AppsFlyer's predefined names such as af_purchase
and af_complete_registration. It sends events only, because AppsFlyer keeps no
user attributes.
| SDK | Package |
|---|---|
| Android | io.github.mkhytarmkhoian:herald-appsflyer |
| Flutter | herald_appsflyer, over appsflyer_sdk |
| iOS | herald-ios-appsflyer, module HeraldAppsFlyer, over AppsFlyer's iOS SDK |
| React Native | @herald-analytics/appsflyer, over react-native-appsflyer |
Setup¶
Your app initialises AppsFlyer, with the dev key and the conversion and deep-link listeners: in
Application.onCreate on Android, in main() on Flutter, in the app delegate on iOS, in the app's
entry file on React Native. On iOS, Herald uses the shared AppsFlyerLib.shared(), so its classes
take no AppsFlyer object; on React Native they take AppsFlyer from react-native-appsflyer. AppsFlyer 7 also ignores start() until
a session-ready listener is registered:
class App : Application() {
override fun onCreate() {
super.onCreate()
// AppsFlyer requires init in Application.onCreate, and version 7 ignores start()
// until a session-ready listener is registered.
AppsFlyerLib.getInstance()
.init("YOUR_DEV_KEY", null, this)
.registerSessionReadyListener { /* deep-link data is ready */ }
}
}
AppsFlyerLib.shared().initialize(devKey: "YOUR_DEV_KEY", appId: "YOUR_APPLE_APP_ID")
// AppsFlyer 7 sends a session only on start(), once per foreground. While Herald keeps the SDK
// stopped, before consent, it sends nothing.
AppsFlyerLib.shared().registerSessionReadyListener {
AppsFlyerLib.shared().start()
}
final appsFlyer = AppsFlyerSdk.instance;
await appsFlyer.init(devKey: 'YOUR_DEV_KEY', appId: 'YOUR_APPLE_APP_ID');
// AppsFlyer 7 sends a session only on start(), once per foreground. While Herald keeps the SDK
// stopped, before consent, it sends nothing.
await appsFlyer.registerSessionReadyListener(() async {
await appsFlyer.start();
});
Then build the provider. It has no properties, because AppsFlyer keeps no user attributes, and
no generic factory at the end, so only the conversions a factory handles reach AppsFlyer:
val appsFlyer = AppsFlyerLib.getInstance() // initialised in Application.onCreate
val tracker = AppsFlyerAnalyticsTrackerService(
// Your conversions and revenue. Nothing at the end, so other events aren't sent.
CompositeAppsFlyerEventTrackerFactory(featureFactories),
)
val service = AppsFlyerAnalyticsService(appsFlyer, application)
val provider = Herald.Provider(
name = "appsflyer",
events = tracker, // no properties: AppsFlyer keeps no user attributes
identity = service,
lifecycle = service,
consent = service,
)
let tracker = AppsFlyerAnalyticsTrackerService(
// Your conversions and revenue. Nothing at the end, so other events aren't sent.
eventTrackerFactory: CompositeAppsFlyerEventTrackerFactory(featureFactories)
)
let service = AppsFlyerAnalyticsService() // uses AppsFlyerLib.shared(), set up at start-up
let provider = HeraldProvider(
name: "appsflyer",
events: tracker, // no properties: AppsFlyer keeps no user attributes
identity: service,
lifecycle: service,
consent: service
)
final appsFlyer = AppsFlyerSdk.instance; // initialised at start-up
final tracker = AppsFlyerAnalyticsTrackerService(
// Your conversions and revenue. Nothing at the end, so other events aren't sent.
eventTrackerFactory: CompositeAppsFlyerEventTrackerFactory(featureFactories),
);
final service = AppsFlyerAnalyticsService(appsFlyer);
final provider = HeraldProvider(
name: 'appsflyer',
events: tracker, // no properties: AppsFlyer keeps no user attributes
identity: service,
lifecycle: service,
consent: service,
);
const tracker = new AppsFlyerAnalyticsTrackerService({
// Your conversions and revenue. Nothing at the end, so other events aren't sent.
eventTrackerFactory: new CompositeAppsFlyerEventTrackerFactory(featureFactories),
});
const service = new AppsFlyerAnalyticsService(AppsFlyer); // initialised at start-up
const provider = new HeraldProvider({
name: 'appsflyer',
events: tracker, // no properties: AppsFlyer keeps no user attributes
identity: service,
lifecycle: service,
consent: service,
});
What reaches AppsFlyer¶
| Herald | AppsFlyer |
|---|---|
| an event a factory handles | logEvent(context, name, values), with each value as its own JSON type |
an AppsFlyerPurchaseEvent |
af_purchase with af_revenue, af_currency and optional content, quantity and order id; a parameter with one of those keys is refused and reported |
an AppsFlyerSubscribeEvent |
af_subscribe with af_revenue and af_currency, refusing those keys as parameters the same way |
an AppsFlyerAdRevenueEvent |
logAdRevenue(AFAdRevenueData, parameters) |
identify / reset |
setCustomerUserId(userId) / setCustomerUserId(null) |
start |
stop(true): stays silent |
setEnabled(true) / setEnabled(false) |
stop(false) then start() / stop(true) |
| Herald | AppsFlyer |
|---|---|
| an event a factory handles | logEvent(name, withValues:), with each value as its own type |
an AppsFlyerPurchaseEvent |
af_purchase with af_revenue, af_currency and optional content, quantity and order id; a parameter with one of those keys is refused and reported |
an AppsFlyerSubscribeEvent |
af_subscribe with af_revenue and af_currency, refusing those keys as parameters the same way |
an AppsFlyerAdRevenueEvent |
logAdRevenue(AFAdRevenueData, additionalParameters:) |
identify / reset |
customerUserID = userId / customerUserID = nil |
start |
isStopped = true: stays silent |
setEnabled(true) / setEnabled(false) |
isStopped = false then start() / isStopped = true |
| Herald | AppsFlyer |
|---|---|
| an event a factory handles | logEvent(name, eventValues:), with each value as its own JSON type |
an AppsFlyerPurchaseEvent |
af_purchase with af_revenue, af_currency and optional content, quantity and order id; a parameter with one of those keys is refused and reported |
an AppsFlyerSubscribeEvent |
af_subscribe with af_revenue and af_currency, refusing those keys as parameters the same way |
an AppsFlyerAdRevenueEvent |
logAdRevenue(...), with the parameters as additional parameters |
identify / reset |
setCustomerUserId(userId) / nothing: the plugin can't clear the id, and AppsFlyer forgets it at the next cold start |
start |
stop(true): stays silent |
setEnabled(true) / setEnabled(false) |
stop(false) then start() / stop(true) |
| Herald | AppsFlyer |
|---|---|
| an event a factory handles | logEvent({ eventName, eventValues }), with each value as its own JSON type |
an AppsFlyerPurchaseEvent |
af_purchase with af_revenue, af_currency and optional content, quantity and order id; a parameter with one of those keys is refused and reported |
an AppsFlyerSubscribeEvent |
af_subscribe with af_revenue and af_currency, refusing those keys as parameters the same way |
an AppsFlyerAdRevenueEvent |
logAdRevenue(...), with the parameters as additional parameters |
identify / reset |
setCustomerUserId({ customerId: userId }) / nothing: the package can't clear the id, and AppsFlyer forgets it at the next cold start |
start |
stop({ shouldStop: true }): stays silent |
setEnabled(true) / setEnabled(false) |
stop({ shouldStop: false }) then start() / stop({ shouldStop: true }) |
Your conversions¶
Most AppsFlyer events are your own conversions under AppsFlyer's predefined names. Write a tracker
and a factory per event in the feature that owns it, as the
factory chain describes. On Android, AFInAppEventType and
AFInAppEventParameterName hold the names.
For revenue, Herald's AppsFlyer module has its own types in AppsFlyer's terms, because AppsFlyer
counts revenue only under af_revenue. Your factory maps your event to one and hands it to the matching tracker:
| Type | Tracker | Tracker on Flutter, iOS and React Native |
|---|---|---|
AppsFlyerPurchaseEvent |
PurchaseEventTracker |
PurchaseAppsFlyerEventTracker |
AppsFlyerSubscribeEvent |
SubscribeEventTracker |
SubscribeAppsFlyerEventTracker |
AppsFlyerAdRevenueEvent |
AdRevenueEventTracker |
AdRevenueAppsFlyerEventTracker |
See Revenue.
Consent¶
AppsFlyer starts on consent, not at start-up. start() only stops the SDK, so a fresh install
sends nothing, not even the install. setEnabled(true) resumes it and calls start(), in the
order AppsFlyer requires after a stop. Don't call start() yourself.
AppsFlyer doesn't persist the stopped state, and start() applies it every launch, so re-apply
the stored decision after start-up. DMA consent (setConsentData) and anonymizeUser are set on
AppsFlyerLib, or AppsFlyerSdk on Flutter and AppsFlyer on React Native, directly.
Identity¶
AppsFlyer 7 doesn't persist the customer user id between launches. Call identify on every cold
start for a signed-in user, before re-applying consent, so the launch AppsFlyer sends on
consent carries the id. See Identity.
Watch out for¶
Android: backup rules clash with yours
The AppsFlyer SDK declares its own android:dataExtractionRules and android:fullBackupContent.
An app with its own rules fails the manifest merge. Add
tools:replace="android:dataExtractionRules,android:fullBackupContent" to <application>,
and copy AppsFlyer's exclusions into your rule files:
<exclude domain="sharedpref" path="appsflyer-data"/>
<exclude domain="sharedpref" path="appsflyer-purchase-data"/>
<exclude domain="database" path="afpurchases.db"/>
Without them, a restored backup reuses an old install's AppsFlyer identity.
Contexton Android. AppsFlyer's API takes one, so Herald's AppsFlyer trackers and service do too. Pass theApplication.- A type check on React Native.
react-native-appsflyerships its TypeScript source, so a stricttscreports errors inside it. Herald's package README shows the one-line fix.