Skip to content

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();
});
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.
AppsFlyer.registerSessionReadyListener(() => {
  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.

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.

  • Context on Android. AppsFlyer's API takes one, so Herald's AppsFlyer trackers and service do too. Pass the Application.
  • A type check on React Native. react-native-appsflyer ships its TypeScript source, so a strict tsc reports errors inside it. Herald's package README shows the one-line fix.