Repository navigation
feat: add App Events feature and bump Facebook Android SDK to 19.0.0 - #6
Merged
Merged
Conversation
Pin facebook-core, -common, -bolts, -login and -share 19.0.0 from Maven Central with their SHA-256 digests (cross-checked against the published .sha1 files). 19.0.0 adds the Google Play Billing Library 8 auto-logging wrapper and the AutoLogMetaDataEnabled / AutoLogProductImprovementEnabled controls. The AAR manifests and POM dependency sets are unchanged from 18.1.3 except installreferrer 1.0 -> 2.2 in facebook-core. Declare facebook-core explicitly so the interop module compiles against the module it calls directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add FacebookAppEventsInterop, a static Java facade over FacebookSdk and AppEventsLogger for SDK initialization, the consent-relevant settings (auto-init, auto-log, advertiser ID, metadata and product-improvement collection, data processing options), event and purchase logging, flush and flush behavior, anonymous ID, user ID and hashed user data, plus diagnostic logging. The facade lives in the existing interop AAR because facebook-core is already always injected, so App Events needs no new native artifact. It uses plain static Java methods (equivalent to a Kotlin object with @JvmStatic) to keep the module Java-only. Purchase amounts cross JNI as decimal strings to avoid double rounding. The existing consumer keep rule already covers the new class; its comment now explains the JNI reachability. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add FacebookAppEvents, a static JNI bridge to FacebookAppEventsInterop following the Kapusch.PostHog.Android pattern (JNIEnv.FindClass plus cached static method IDs, local references released after each call): - SDK lifecycle: InitializeSdk, IsSdkInitialized, IsSdkFullyInitialized - consent switches: auto-init, auto-log App Events, advertiser ID collection, SDK 19 metadata and product-improvement collection, data processing options (LDU) - events: ActivateApp, LogEvent, LogPurchase and typed LogStartTrial, LogSubscribe and LogCompleteRegistration helpers, Flush and flush behavior - identity: GetAnonymousId, Set/Get/ClearUserId, Set/ClearUserData with SDK-side hashing - diagnostics: SetDebugLoggingEnabled (debug mode + APP_EVENTS logging) Parameters become an Android Bundle; decimals and purchase amounts are sent as invariant strings because the SDK stringifies every value. The standard event, parameter and value names are generated from AppEventsConstants in the bundled facebook-core AAR by scripts/app-event-constants.py, which also verifies them in CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Accept AppEvents in KapuschFacebookFeatures (alone or combined with Login and Share). The default stays Login, so existing consumers get the same AARs and manifest as before. AppEvents injects no extra AAR: the facade ships in the wrapper and facebook-core is always injected. Selecting it adds: - buildTransitive/proguard/kapusch-facebook-appevents.pro, an R8 rule that keeps the public Play Billing API facebook-core looks up by name through reflection for purchase auto-logging (its own consumer rules do not), so the rule never reaches Login- or Share-only apps. .NET for Android passes -dontobfuscate by default, so the risk it covers is shrinking of members only reached by reflection; - warning KFBA001 when no Play Install Referrer library is referenced, because facebook-core reads the install referrer only when its classes are present. The package does not bundle installreferrer so apps keep a single copy shared with other SDKs; the warning can be silenced with KapuschFacebookAndroidSuppressInstallReferrerWarning. Reword the package description. The feature selection test now covers AppEvents and checks the R8 rule file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Select Login;Share;AppEvents in the sample, import the package .props so source mode reproduces NuGet's import order (without it the default Login feature was never applied and facebook-login was not injected), and reference Xamarin.Google.Android.InstallReferrer. The manifest template now disables every automatic collection switch; the activity shows the SDK flags and anonymous ID and has buttons to grant or withdraw consent, log StartTrial, Subscribe and a custom event, and flush. Release builds shrink with R8 and write a mapping file so CI can prove the JNI-only facade survives. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add deterministic validators and run them in CI (and the release checks in publish before any push): - scripts/validate-package.py: required entries, android/deps equal to the lockstate set with matching SHA-256 and one SDK version, no bundled AndroidX/Kotlin/Google artifacts, wrapper classes and keep rule present, default feature still Login, AppEvents wired; - scripts/verify-interop.py jni: every JNI call in FacebookAppEvents.cs exists with the same descriptor in the built wrapper AAR; - scripts/verify-interop.py apk: the R8-shrunk sample APK still defines the facade, every method the managed bridge calls and the SDK entry points (parsed from dex, no Android build-tools needed); - scripts/verify-public.py: real Meta app ID, client token and email detection, plus private terms read only from an untracked source (the KAPUSCH_PRIVATE_TERMS secret in CI, or .git/info/private-terms locally) so the scanner never publishes them. Failures name the file and the term index, never the term, because CI logs are public; - scripts/app-event-constants.py now runs in CI as well; - tests/InstallReferrerCheck.proj covers KFBA001. The PR workflow builds the sample in Release with R8 (arm64 only to bound CI time). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Integration.md now covers feature selection, the dependencies the app provides (Play Install Referrer via the managed binding, optional advertising ID), the five SDK manifest switches including the SDK 19 AutoLogMetaDataEnabled and AutoLogProductImprovementEnabled keys, a consent-gating recipe (off in the manifest, applied on every launch because runtime values persist, withdrawal path, optional removal of FacebookInitProvider), event logging, the double-counting warning for server-side purchase events, identity, diagnostics, R8, Android gaps, and upgrade notes for the 19.0.0 behaviour changes. SourceMode.md now imports the package .props like NuGet does. README, the NuGet readme, AGENTS.md, CONTRIBUTING.md, Release.md, the PR template and THIRD_PARTY_NOTICES describe the new feature, the dependency ingestion paths and the validators. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Passing RuntimeIdentifiers globally also reached the referenced library project and broke its build. The sample now maps SampleRuntimeIdentifiers to its own RuntimeIdentifiers, which CI sets to android-arm64 to keep the R8 job short. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A runtime smoke test of the R8-shrunk sample on an API 36 emulator crashed at startup with NoClassDefFoundError for androidx.localbroadcastmanager.content.LocalBroadcastManager: facebook-core needs it (via legacy-support-core-utils) and a bare AppCompat app does not bring it. This predates App Events; the sample was only ever built, never launched. Integration.md now lists the AndroidX/Kotlin runtime dependencies the app provides, and records what the same smoke test showed with AutoInitEnabled=false: the SDK still prepares its startup Graph requests and rejects them before processing a response. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UserDataStore only stores the non-null fields it receives, so null properties keep their previously stored hash rather than clearing it. Document that, and that values already shaped like SHA-256 hashes are kept as is; ClearUserData remains the way to remove everything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
publishInstallAsync is not gated by automatic logging, but the SDK rejects Graph responses until it is fully initialized, so an install ping sent earlier is not recorded as delivered and only retried on a later activation. Say so on ActivateApp and in the consent recipe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… login AppEventsLogger captures AccessToken.getCurrentAccessToken() when it is constructed and sends every event it logs under that token. The facade cached one logger for the whole process, so with Facebook Login in the same package every event logged after a sign-out kept carrying the signed-out user's token, and a first event logged before sign-in meant events never carried the signed-in user's token. Build the logger per call instead. Construction is cheap: the event queue and flush state are static in the SDK. Document a manual log-in/log-out check in the sample README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FacebookSdk.setDataProcessingOptions reads the application context that sdkInitialize stores. Called earlier, it throws inside CrashShield, which swallows the error while CrashShield is not enabled, so in the strictest setup (FacebookInitProvider removed, InitializeSdk after consent) SetDataProcessingOptions(LDU) followed by InitializeSdk() silently dropped Limited Data Use. The other UserSettingsManager-backed setters only kept their value in memory, and the user ID and user data calls failed on an SDK background thread. - Every setter, identity call and event call now throws IllegalStateException until the SDK is initialized, so a consent setting is never silently lost. - InitializeSdk accepts data processing options and applies them between sdkInitialize and fullyInitialize, the only point where they reach the SDK's first Graph requests. The existing call shape InitializeSdk() is unchanged. - Integration.md documents the order for the provider-removed setup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shared code that targets both Kapusch.Facebook.Android and Kapusch.Facebook.iOS did not compile on both. Adopt the iOS names, which already followed the event-name and registration-value shape Meta documents: - LogEvent takes eventName (named arguments now work on both); - LogCompleteRegistration takes value and currencyIsoCode (Meta's CompleteRegistration accepts valueToSum and fb_currency), between registrationMethod and parameters as on iOS; - FacebookFlushBehavior and FacebookUserData become FacebookAppEventsFlushBehavior and FacebookAppEventsUserData; - SetAutoLogMetaDataEnabled/IsAutoLogMetaDataEnabled become SetMetadataCollectionEnabled/IsMetadataCollectionEnabled (the manifest key is unchanged); - GetAnonymousId returns string? instead of an empty-string fallback; - SetUserData replaces every field, so a null property clears it as on iOS. AppEventsLogger.setUserData alone merges, so the facade clears first; both calls run in order on the SDK's single-threaded executor; - the constants generator spells AppLink, IPhone and IPad like the iOS constants (ProductAppLinkIPhoneUrl, ...). Nothing has been released with the old names. Integration.md lists the shared surface and the Android-only members (Context arguments, SetAutoInitEnabled, IsSdk*Initialized, FacebookAppEventsUserData.ExternalId). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bundled 19.0.0 bytecode shows that turning the switches off does not stop the SDK in the running process: setAutoInitEnabled(false) never clears the fully-initialized state, so queued and new events keep flushing, and the ActivityLifecycleCallbacks registered by ActivateApp (or automatic logging) keep logging activation and deactivation on every resume without checking either flag. - The consent recipe says withdrawal fully applies on the next process start, and recommends gating the app's own Log* calls, setting ExplicitOnly without further Flush() calls, and notes that queued events stay on disk until a later fully initialized session. - ActivateApp only sends the install ping and registers activation tracking, so the activity already on screen is not counted. Document calling it on every launch before the first activity resumes, and that the session in which consent is first granted is counted from the next resume. - The sample and its README follow the same wording, and the sample's withdrawal sets ExplicitOnly. A facade-level kill switch was not added: it would be an Android-only API, and gating at the call site already makes manual events stop at once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kapusch
force-pushed
the
feat/app-events
branch
from
October 26, 2026 22:57
6ccdfd6 to
c7a795b
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Release 1.2.0 (to be published first as
1.2.0-rc.1, not tagged yet):.sha1). The AAR manifests and POM dependency sets are identical to 18.1.3, exceptinstallreferrer1.0 → 2.2 in facebook-core. 19.0.0 adds Google Play Billing Library 8 auto-logging and theAutoLogMetaDataEnabled/AutoLogProductImprovementEnabledcontrols.AppEventsfeature.KapuschFacebookFeaturesnow acceptsAppEventsalone or combined, e.g.Login;AppEvents. The default staysLogin.FacebookAppEventsAPI over a static Java facade, called through JNI.Public C# API (namespace
Kapusch.Facebook.Android)FacebookAppEvents(static):InitializeSdk(Context? = null, string[]? dataProcessingOptions = null, int dataProcessingCountry = 0, int dataProcessingState = 0),IsSdkInitialized,IsSdkFullyInitializedSet*method plus anIs*getter:SetAutoInitEnabled,SetAutoLogAppEventsEnabled,SetAdvertiserIdCollectionEnabled,SetMetadataCollectionEnabled,SetAutoLogProductImprovementEnabledSetDataProcessingOptions(string[], int country = 0, int state = 0),SetDebugLoggingEnabled(bool)(debug mode plus theAPP_EVENTSlogging behavior),SetFlushBehavior(FacebookAppEventsFlushBehavior)with theFlushBehaviorgetterActivateApp(Context? = null),LogEvent(string eventName, IReadOnlyDictionary<string, object>? = null, double? valueToSum = null),LogPurchase(decimal, string currencyIsoCode, parameters?)LogStartTrial(decimal? value, string? currency, string? orderId, parameters?), the same signature forLogSubscribe, andLogCompleteRegistration(string? registrationMethod, decimal? value, string? currencyIsoCode, parameters?)Flush()GetAnonymousId()(string?),SetUserId(string?),GetUserId(),ClearUserId(),SetUserData(FacebookAppEventsUserData)(replaces every field;nullclears),ClearUserData()Java.Lang.IllegalStateExceptionbeforesdkInitializeAppEventsConstants:FacebookAppEventNames,FacebookAppEventParameterNamesandFacebookAppEventParameterValues, 77 constants in total.FacebookAppEventsUserData(sealed record),FacebookAppEventsFlushBehavior,FacebookDataProcessingOptions.LimitedDataUse.Kapusch.Facebook.iOSpackage so shared code compiles on both; Integration.md lists the shared surface and the Android-only members.Docs/Integration.md.Why these choices
object. The interop module is Java-only. A class withpublic staticmethods has the same JNI shape asobject+@JvmStatic. Using it avoids adding the Kotlin Gradle plugin and Kotlin metadata keep rules. The C# side follows the Kapusch.PostHog.Android pattern:JNIEnv.FindClassand cached static method IDs, with local references released after each call.decimalparameters cross JNI as invariant strings. The SDK builds aBigDecimaland stringifies every parameter, so no precision is lost todouble.com.android.installreferrerclasses are present.Xamarin.Google.Android.InstallReferrerbinding.AppEventsfeature emits warningKFBA001when no referrer library is referenced. It can be silenced withKapuschFacebookAndroidSuppressInstallReferrerWarning=true.installreferrer-2.2.aarfrom Google Maven on the consumer's first build (Xamarin.Build.Download). This is why the package recommends it but does not depend on it.com.kapusch.facebook.androidinterop.**. The facade is reachable only through JNI.proguard.txtcovers its install-referrer and GMS reflection.com.android.billingclient.api.*, which it uses through reflection for purchase auto-logging.AppEventstherefore injectsbuildTransitive/proguard/kapusch-facebook-appevents.pro, which keeps the public Play Billing API. The rule is inert without Billing, and Login- and Share-only apps never receive it.-dontobfuscate, so the real risk is shrinking rather than renaming. The rule's comment says so.FacebookInitProviderbehaviour is unchanged.InitializeSdk()only callssdkInitializewhen the provider has not already done so, then callsfullyInitialize.IllegalStateExceptionon an uninitialized SDK, instead of the SDK dropping it silently (see review follow-up).string;bool, sent as"1"/"0", the SDK's YES/NO values; integral types; finitefloat/double; anddecimal. Anything else throwsArgumentException, because the SDK rejects non-string, non-numeric values.Behaviour for existing consumers
1.1.0from NuGet.org and against this1.2.0package. The mergedAndroidManifest.xmlfiles are byte-identical apart from the generated-from comment, and thelp/AAR set is the same: bolts, common, core, login and the interop AAR.KFBA001and no new R8 rule for consumers that do not selectAppEvents.AutoLogMetaDataEnabled=false.AutoLogAppEventsEnabled=falsenow wins over the Events Manager setting.Pre-existing issues fixed along the way
Loginfeature. It imported only the.targets, so facebook-login was not injected. It now imports the.propslike NuGet does, andDocs/SourceMode.mdsays to do the same.NoClassDefFoundError: androidx.localbroadcastmanager.... facebook-core needs this class, and a bare AppCompat app does not include it. The sample had only ever been built, never run. It now referencesXamarin.AndroidX.LocalBroadcastManager, and Integration.md lists the AndroidX and Kotlin runtime dependencies the app must provide.Validation (local: .NET 10.0.203, JDK 21, Android SDK 36, Android workload 36.1.53)
python3 scripts/verify-public.py: passes. It rejects real Meta app IDs, client tokens and emails, plus private terms read only from an untracked source (KAPUSCH_PRIVATE_TERMSsecret in CI,.git/info/private-termslocally). Negative test: a configured term that occurs in the tree fails the scan and the output names only "private term Add Facebook Android interop wrapper and documentation #1".bash tests/validate-feature-selection.sh: covers 6 feature combinations and 5KFBA001cases.python3 scripts/app-event-constants.py: the 77 constants match facebook-core 19.0.0.python3 scripts/verify-interop.py jni: all 28 managed JNI calls match thejavap -sdescriptors in the wrapper AAR.dotnet packthenpython3 scripts/validate-package.py artifacts/nuget: checks the deps set and SHA-256 against the lockstate, a single SDK version, no bundled AndroidX/Kotlin/Google artifacts, wrapper classes and keep rule, a default ofLogin, and the AppEvents wiring.verify-interop.py apk: the facade, every called method and the SDK entry points survive in the dex.Login;AppEvents, R8, packed nupkg):KFBA001fires without the referrer library, and the APK check passes.GetAnonymousId()returns anXZ…ID.GraphRequest can't be used when Facebook SDK isn't fully initialized, which is now documented.Review follow-up (history rewritten and rebased on current
master)The branch was rebased onto
master(which now has #5, the CI toolchain pins) and rewritten before merge, so that the review's genericity finding never reachesmaster:scripts/verify-public.pyin every commit. Private terms come from the optionalKAPUSCH_PRIVATE_TERMSrepository secret (wired inci.ymlandpublish.yml; the check is skipped with a notice when it is unset) or the untracked.git/info/private-terms. Output never prints the term, because Actions logs on a public repo are public..prowording now lands in the packaging commit, and the version bump is its ownchore(release)commit at the end.New commits on top:
fix(appevents): create a logger per call—AppEventsLoggercaptures the current access token at construction and sends every event under it (checked withjavapon 19.0.0). The cached process-wide logger kept tagging post-logout events with the signed-out user's token. Construction is cheap because the queue is static. The sample README gives a manual log-in/log-out check (by inspectingPOST /<app id>/activities, since the SDK redacts tokens in logcat).fix(appevents): fail before SDK init and apply LDU in InitializeSdk— beforesdkInitialize,FacebookSdk.setDataProcessingOptionsthrows inside CrashShield, which swallows it, soSetDataProcessingOptions(LDU)+InitializeSdk()silently dropped LDU in the provider-removed setup. Now every setter, identity and event call throws until the SDK is initialized, andInitializeSdktakes the options and applies them betweensdkInitializeandfullyInitialize, the only point where they reach the SDK's first Graph requests.InitializeSdk()with no arguments keeps working.refactor(appevents): align the managed API with the iOS package— adopt the iOS names (eventName,LogCompleteRegistrationvalue/currency,FacebookAppEvents*type names,SetMetadataCollectionEnabled,string? GetAnonymousId) and the iOS user-data semantics (anullfield clears it; the facade callsclearUserDatabeforesetUserData, both on the SDK's single-threaded executor). The constants generator now spellsAppLink/IPhone/IPadlike the iOS constants. Why iOS's names rather than Android's: the review's explicit suggestions (eventName, value/currency on registration, nullable anonymous ID) already matched iOS, so following iOS for the remaining names keeps a single direction of change. Nothing has been released with the old names.docs(appevents): state that withdrawal applies at the next process start—setAutoInitEnabled(false)never clears the fully-initialized state, and the lifecycle callbacks registered byActivateAppkeep logging activation on every resume. The recipe now says so, recommends gating the app's ownLog*calls plusExplicitOnlywithout furtherFlush(), and notes that queued events stay on disk.ActivateAppdocs now say activation is logged from the next resume, and recommend calling it on every launch before the first activity resumes. No facade-level kill switch: it would be Android-only, and gating at the call site already stops manual events at once.chore(release): bump package version to 1.2.0.Re-validated after the rewrite: native build, AAR restore, pack,
validate-package.py(17 entries),verify-public.py(with a private term configured), constants (77), JNI contract (28 calls), feature selection and KFBA001, Debug sample, and Release R8 sample (arm64) +verify-interop.py apk. No new runtime smoke test was run for these changes.Checklist
bash src/Kapusch.FacebookApisForAndroidComponents/Native/Android/build.shbash src/Kapusch.FacebookApisForAndroidComponents/Native/Android/restore-facebook-aars.shdotnet pack src/Kapusch.FacebookApisForAndroidComponents/Kapusch.FacebookApisForAndroidComponents.csproj -c Release -o artifacts/nugetREADME.mdNext, after merge: tag
v1.2.0-rc.1on the merge commit, followingDocs/Release.md.🤖 Generated with Claude Code