Skip to content

Meta

Herald's Meta module sends conversion events to Meta's App Events, for Meta's ads. Meta only gets the events you choose: your conversions, under Meta's standard names such as fb_mobile_complete_registration, and purchases as fb_mobile_purchase. It sends events only, because Meta keeps no user attributes.

SDK Package
Android io.github.mkhytarmkhoian:herald-meta, over Meta's Android SDK
Flutter herald_meta, over facebook_app_events
iOS herald-ios-meta, module HeraldMeta, over Meta's iOS SDK
React Native @herald-analytics/meta, over react-native-fbsdk-next

facebook_app_events and react-native-fbsdk-next are community packages over Meta's own SDKs: Meta has no Flutter or React Native SDK.

Setup

Set Meta's app id and client token as Meta's guide shows, and turn its automatic collection off, so Meta collects nothing by itself before consent:

In AndroidManifest.xml:

<meta-data android:name="com.facebook.sdk.AutoInitEnabled" android:value="false" />
<meta-data android:name="com.facebook.sdk.AutoLogAppEventsEnabled" android:value="false" />
<meta-data android:name="com.facebook.sdk.AdvertiserIDCollectionEnabled" android:value="false" />

In Info.plist:

<key>FacebookAutoLogAppEventsEnabled</key>
<false/>
<key>FacebookAdvertiserIDCollectionEnabled</key>
<false/>

In both native projects: the Android lines in android/app/src/main/AndroidManifest.xml, and the iOS lines in ios/Runner/Info.plist.

<meta-data android:name="com.facebook.sdk.AutoInitEnabled" android:value="false" />
<meta-data android:name="com.facebook.sdk.AutoLogAppEventsEnabled" android:value="false" />
<meta-data android:name="com.facebook.sdk.AdvertiserIDCollectionEnabled" android:value="false" />
<key>FacebookAutoLogAppEventsEnabled</key>
<false/>
<key>FacebookAdvertiserIDCollectionEnabled</key>
<false/>

With Expo, the package's config plugin in app.json already turns them off: leave isAutoInitEnabled, autoLogAppEventsEnabled and advertiserIDCollectionEnabled out, or false.

["react-native-fbsdk-next", { "appID": "YOUR_APP_ID", "clientToken": "YOUR_CLIENT_TOKEN" }]

Without Expo, add the Android lines to AndroidManifest.xml and the iOS lines to Info.plist:

<meta-data android:name="com.facebook.sdk.AutoInitEnabled" android:value="false" />
<meta-data android:name="com.facebook.sdk.AutoLogAppEventsEnabled" android:value="false" />
<meta-data android:name="com.facebook.sdk.AdvertiserIDCollectionEnabled" android:value="false" />
<key>FacebookAutoLogAppEventsEnabled</key>
<false/>
<key>FacebookAdvertiserIDCollectionEnabled</key>
<false/>

Then build the provider. It has no properties, because Meta keeps no user attributes, and no generic factory at the end, so only the conversions a factory handles reach Meta:

val logger = AppEventsLogger.newLogger(application)
val service = MetaAnalyticsService(logger)

val tracker = MetaAnalyticsTrackerService(
    // Your conversions. Nothing at the end, so other events aren't sent.
    CompositeMetaEventTrackerFactory(featureFactories),
)

val provider = Herald.Provider(
    name = "meta",
    events = tracker, // no properties: Meta keeps no user attributes
    identity = service,
    lifecycle = service,
    consent = service,
)
let service = MetaAnalyticsService()  // uses Meta's shared AppEvents, set up at start-up

let tracker = MetaAnalyticsTrackerService(
    // Your conversions. Nothing at the end, so other events aren't sent.
    eventTrackerFactory: CompositeMetaEventTrackerFactory(featureFactories)
)

let provider = HeraldProvider(
    name: "meta",
    events: tracker,  // no properties: Meta keeps no user attributes
    identity: service,
    lifecycle: service,
    consent: service
)
final appEvents = FacebookAppEvents();
final service = MetaAnalyticsService(appEvents);

final tracker = MetaAnalyticsTrackerService(
  // Your conversions. Nothing at the end, so other events aren't sent.
  eventTrackerFactory: CompositeMetaEventTrackerFactory(featureFactories),
);

final provider = HeraldProvider(
  name: 'meta',
  events: tracker, // no properties: Meta keeps no user attributes
  identity: service,
  lifecycle: service,
  consent: service,
);
const service = new MetaAnalyticsService({ appEvents: AppEventsLogger, settings: Settings });

const tracker = new MetaAnalyticsTrackerService({
  // Your conversions. Nothing at the end, so other events aren't sent.
  eventTrackerFactory: new CompositeMetaEventTrackerFactory(featureFactories),
});

const provider = new HeraldProvider({
  name: 'meta',
  events: tracker, // no properties: Meta keeps no user attributes
  identity: service,
  lifecycle: service,
  consent: service,
});

What reaches Meta

Herald Meta
an event a factory handles logEvent(name, bundle), with numbers as numbers and true/false as text
a MetaPurchaseEvent logPurchase(amount, currency, bundle), sent as fb_mobile_purchase
identify / reset AppEventsLogger.setUserID(userId) / clearUserID()
flush flush()
start nothing: Meta's SDK starts itself
setEnabled(true) / setEnabled(false) automatic events, advertiser id collection and auto init on, then fullyInitialize() / the same three off
Herald Meta
an event a factory handles AppEvents.shared.logEvent(name, parameters:), with numbers as numbers and true/false as text
a MetaPurchaseEvent logPurchase(amount:currency:parameters:), sent as fb_mobile_purchase
identify / reset AppEvents.shared.userID = userId / nil
flush flush()
start nothing: Meta's SDK starts from your app delegate
setEnabled isAutoLogAppEventsEnabled and isAdvertiserIDCollectionEnabled on Settings.shared
Herald Meta
an event a factory handles logEvent(name:, parameters:), with numbers as numbers and true/false as text
a MetaPurchaseEvent logPurchase(amount:, currency:, parameters:), sent as fb_mobile_purchase
identify / reset setUserID(userId) / clearUserID()
flush flush()
start nothing: the plugin starts Meta's SDK when the app launches
setEnabled(true) / setEnabled(false) initializeSdk(), then automatic events and advertiser id collection on / the same two off
Herald Meta
an event a factory handles AppEventsLogger.logEvent(name, parameters), with numbers as numbers and true/false as text
a MetaPurchaseEvent logPurchase(amount, currency, parameters), sent as fb_mobile_purchase
identify / reset setUserID(userId) / setUserID(null)
flush flush()
start nothing: Meta's SDK starts when the app launches
setEnabled(true) / setEnabled(false) Settings.initializeSDK(), then automatic events and advertiser id collection on / the same two off

Your conversions

Most Meta events are your own conversions under Meta's standard names. Write a tracker and a factory per event in the feature that owns it, as the factory chain describes. The names are in AppEventsConstants on Android, AppEvents.Name on iOS, FacebookAppEvents on Flutter and AppEventsLogger.AppEvents on React Native.

For a purchase, map your event to a MetaPurchaseEvent and send it with the purchase tracker: PurchaseEventTracker on Android, PurchaseMetaEventTracker on the others. A conversion of your own calls Meta directly, with toMetaParameters() for its parameters:

class CheckoutMetaEventTrackerFactory(
    private val logger: AppEventsLogger,
) : MetaEventTrackerFactory {

    override fun create(event: Event): Resolution<MetaEventTracker> = when (event) {
        // A purchase: Meta's fb_mobile_purchase, which it counts as revenue.
        is CheckoutCompleted -> Resolution.Claimed(
            PurchaseEventTracker(
                MetaPurchaseEvent(event.name, event.value, event.currency, event.parameters),
                logger,
            ),
        )
        // Any other conversion, under one of Meta's standard names.
        is PlanSelected -> Resolution.Claimed(
            MetaEventTracker {
                logger.logEvent(
                    AppEventsConstants.EVENT_NAME_INITIATED_CHECKOUT,
                    event.parameters.toMetaParameters(),
                )
            },
        )
        else -> Resolution.Declined
    }
}
struct CheckoutMetaEventTrackerFactory: MetaEventTrackerFactory {
    func create(_ event: any Event) -> Resolution<any MetaEventTracker> {
        switch event {
        // A purchase: Meta's fb_mobile_purchase, which it counts as revenue.
        case let checkout as CheckoutCompleted:
            let purchase = MetaPurchaseEvent(
                name: checkout.name, amount: checkout.value, currency: checkout.currency,
                parameters: checkout.parameters)
            return .claimed([PurchaseMetaEventTracker(event: purchase)])
        // Any other conversion, under one of Meta's standard names.
        case let plan as PlanSelected:
            return .claimed([InitiatedCheckoutTracker(event: plan)])
        default:
            return .declined
        }
    }
}

struct InitiatedCheckoutTracker: MetaEventTracker {
    let event: any Event

    func track() {
        AppEvents.shared.logEvent(
            .initiatedCheckout, parameters: event.parameters.toMetaParameters())
    }
}
final class const CheckoutMetaEventTrackerFactory(final FacebookAppEvents appEvents)
    implements MetaEventTrackerFactory {
  @override
  Resolution<MetaEventTracker> create(Event event) => switch (event) {
    // A purchase: Meta's fb_mobile_purchase, which it counts as revenue.
    CheckoutCompleted() => .claimed([
      PurchaseMetaEventTracker(
        MetaPurchaseEvent(
          name: event.name,
          amount: event.value,
          currency: event.currency,
          parameters: event.parameters,
        ),
        appEvents,
      ),
    ]),
    // Any other conversion, under one of Meta's standard names.
    PlanSelected() => .claimed([_InitiatedCheckoutTracker(event, appEvents)]),
    _ => .declined(),
  };
}

final class const _InitiatedCheckoutTracker(final Event event, final FacebookAppEvents appEvents)
    implements MetaEventTracker {
  @override
  Future<void> track() => appEvents.logEvent(
    name: FacebookAppEvents.eventNameInitiatedCheckout,
    parameters: event.parameters.toMetaParameters(),
  );
}
export class CheckoutMetaEventTrackerFactory implements MetaEventTrackerFactory {
  constructor(private readonly appEvents: MetaAppEvents) {}

  create(event: Event): Resolution<MetaEventTracker> {
    // A purchase: Meta's fb_mobile_purchase, which it counts as revenue.
    if (event instanceof CheckoutCompleted) {
      const purchase = new MetaPurchaseEvent({
        name: event.name,
        amount: event.value,
        currency: event.currency,
        parameters: event.parameters,
      });
      return Resolution.claimed([new PurchaseMetaEventTracker(purchase, this.appEvents)]);
    }
    // Any other conversion, under one of Meta's standard names.
    if (event instanceof PlanSelected) {
      return Resolution.claimed([new InitiatedCheckoutTracker(event, this.appEvents)]);
    }
    return Resolution.declined();
  }
}

class InitiatedCheckoutTracker implements MetaEventTracker {
  constructor(
    private readonly event: Event,
    private readonly appEvents: MetaAppEvents,
  ) {}

  async track(): Promise<void> {
    this.appEvents.logEvent(
      AppEventsLogger.AppEvents.InitiatedCheckout,
      toMetaParameters(parametersOf(this.event)),
    );
  }
}

See Revenue.

setEnabled turns Meta's own switches on or off: the events Meta logs by itself, such as installs and app opens, and the advertiser id. They don't stop the events your app logs: an event a factory sends reaches Meta with or without consent.

Call setEnabled with the stored decision after start-up, on every launch, as for every vendor.

Meta's other privacy settings, such as Limited Data Use (setDataProcessingOptions) and, on iOS, setAdvertiserTrackingEnabled, are set on Meta's SDK directly.

Watch out for

  • Android: more event details from 31 October 2026. Meta's Android SDK 19 adds the screen title and app link to the events it sends, and sends samples of your screens' layout. Turn them off in AndroidManifest.xml or in Events Manager, as Meta's change log shows.
  • Flutter on iOS: keep FacebookAutoInitEnabled. If Info.plist sets it to false, the plugin refuses every call until the SDK is started, and each refusal reaches Herald's error reporter.
  • iOS: Meta's SDK 18. Meta's SDK 19 for iOS doesn't install through Swift Package Manager yet, so herald-ios-meta uses 18.