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:
In Info.plist:
In both native projects: the Android lines in android/app/src/main/AndroidManifest.xml, and
the iOS lines in ios/Runner/Info.plist.
With Expo, the package's config plugin in app.json already turns them off: leave
isAutoInitEnabled, autoLogAppEventsEnabled and advertiserIDCollectionEnabled out, or
false.
Without Expo, add the Android lines to AndroidManifest.xml and the iOS lines to Info.plist:
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.
Consent¶
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.xmlor in Events Manager, as Meta's change log shows. - Flutter on iOS: keep
FacebookAutoInitEnabled. IfInfo.plistsets it tofalse, 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-metauses 18.