Skip to content

feat: add App Events feature and bump Facebook Android SDK to 19.0.0 - #6

Merged
Kapusch merged 16 commits into
masterfrom
feat/app-events
Oct 26, 2026
Merged

Kapusch merged 16 commits into
masterfrom
feat/app-events

Conversation

@Kapusch

@Kapusch Kapusch commented Oct 26, 2026 •

Copy link
Copy Markdown
Owner

Summary

Release 1.2.0 (to be published first as 1.2.0-rc.1, not tagged yet):

  1. Facebook Android SDK 18.1.3 → 19.0.0 (own commit). All five AARs locked by SHA-256 (cross-checked against Maven Central .sha1). The AAR manifests and POM dependency sets are identical to 18.1.3, except installreferrer 1.0 → 2.2 in facebook-core. 19.0.0 adds Google Play Billing Library 8 auto-logging and the AutoLogMetaDataEnabled / AutoLogProductImprovementEnabled controls.
  2. New selectable AppEvents feature. KapuschFacebookFeatures now accepts AppEvents alone or combined, e.g. Login;AppEvents. The default stays Login.
  3. Managed FacebookAppEvents API over a static Java facade, called through JNI.
  4. Validators wired into CI and publish, plus an R8 build of the sample.
  5. Docs: integration, consent gating, double counting, upgrade notes.

Public C# API (namespace Kapusch.Facebook.Android)

  • FacebookAppEvents (static):
    • SDK state: InitializeSdk(Context? = null, string[]? dataProcessingOptions = null, int dataProcessingCountry = 0, int dataProcessingState = 0), IsSdkInitialized, IsSdkFullyInitialized
    • SDK switches, each a Set* method plus an Is* getter: SetAutoInitEnabled, SetAutoLogAppEventsEnabled, SetAdvertiserIdCollectionEnabled, SetMetadataCollectionEnabled, SetAutoLogProductImprovementEnabled
    • other settings: SetDataProcessingOptions(string[], int country = 0, int state = 0), SetDebugLoggingEnabled(bool) (debug mode plus the APP_EVENTS logging behavior), SetFlushBehavior(FacebookAppEventsFlushBehavior) with the FlushBehavior getter
    • events: ActivateApp(Context? = null), LogEvent(string eventName, IReadOnlyDictionary<string, object>? = null, double? valueToSum = null), LogPurchase(decimal, string currencyIsoCode, parameters?)
    • typed helpers: LogStartTrial(decimal? value, string? currency, string? orderId, parameters?), the same signature for LogSubscribe, and LogCompleteRegistration(string? registrationMethod, decimal? value, string? currencyIsoCode, parameters?)
    • Flush()
    • identity: GetAnonymousId() (string?), SetUserId(string?), GetUserId(), ClearUserId(), SetUserData(FacebookAppEventsUserData) (replaces every field; null clears), ClearUserData()
    • every setter, identity call and event call throws Java.Lang.IllegalStateException before sdkInitialize
  • Constants generated from the bundled AppEventsConstants: FacebookAppEventNames, FacebookAppEventParameterNames and FacebookAppEventParameterValues, 77 constants in total.
  • Support types: FacebookAppEventsUserData (sealed record), FacebookAppEventsFlushBehavior, FacebookDataProcessingOptions.LimitedDataUse.
  • The App Events names match the Kapusch.Facebook.iOS package so shared code compiles on both; Integration.md lists the shared surface and the Android-only members.
  • Android has no iOS-only features: ATT, SKAdNetwork, AEM and URL forwarding. Not wrapped: push-open logging, product items, WebView augmentation, codeless setup and manual install referrer. These gaps are documented in Docs/Integration.md.

Why these choices

  • Extending this package instead of creating a new one. facebook-core is already always injected. A separate App Events package would ship a second facebook-core, causing duplicate classes and version skew. App Events therefore needs no new AAR: the facade lives in the existing interop AAR.
  • Plain Java static class, not a Kotlin object. The interop module is Java-only. A class with public static methods has the same JNI shape as object + @JvmStatic. Using it avoids adding the Kotlin Gradle plugin and Kotlin metadata keep rules. The C# side follows the Kapusch.PostHog.Android pattern: JNIEnv.FindClass and cached static method IDs, with local references released after each call.
  • Purchase amounts and decimal parameters cross JNI as invariant strings. The SDK builds a BigDecimal and stringifies every parameter, so no precision is lost to double.
  • Install Referrer comes from the consumer's managed NuGet, not bundled and not a hard dependency. facebook-core reads the referrer only when com.android.installreferrer classes are present.
    • Bundling the AAR would create duplicate classes with other SDKs that use Microsoft's Xamarin.Google.Android.InstallReferrer binding.
    • A hard NuGet dependency would change the graph and the collected data for Login-only consumers.
    • Instead, the AppEvents feature emits warning KFBA001 when no referrer library is referenced. It can be silenced with KapuschFacebookAndroidSuppressInstallReferrerWarning=true.
    • Caveat, documented: that Microsoft binding downloads installreferrer-2.2.aar from Google Maven on the consumer's first build (Xamarin.Build.Download). This is why the package recommends it but does not depend on it.
  • R8 rules.
    • The existing consumer rule already keeps com.kapusch.facebook.androidinterop.**. The facade is reachable only through JNI.
    • facebook-core's own proguard.txt covers its install-referrer and GMS reflection.
    • It does not keep com.android.billingclient.api.*, which it uses through reflection for purchase auto-logging. AppEvents therefore injects buildTransitive/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.
    • The R8 run showed that .NET for Android passes -dontobfuscate, so the real risk is shrinking rather than renaming. The rule's comment says so.
  • FacebookInitProvider behaviour is unchanged.
    • InitializeSdk() only calls sdkInitialize when the provider has not already done so, then calls fullyInitialize.
    • Setters map one to one to upstream, and the wrapper sets no defaults.
    • Every setter, identity call and event call fails with an actionable IllegalStateException on an uninitialized SDK, instead of the SDK dropping it silently (see review follow-up).
  • Bundle typing. Accepted value types: string; bool, sent as "1"/"0", the SDK's YES/NO values; integral types; finite float/double; and decimal. Anything else throws ArgumentException, because the SDK rejects non-string, non-numeric values.

Behaviour for existing consumers

  • Same injected AARs and same merged manifest. I built a clean consumer with default features against 1.1.0 from NuGet.org and against this 1.2.0 package. The merged AndroidManifest.xml files are byte-identical apart from the generated-from comment, and the lp/ AAR set is the same: bolts, common, core, login and the interop AAR.
  • No KFBA001 and no new R8 rule for consumers that do not select AppEvents.
  • The SDK itself behaves differently in 19.0.0. These changes are listed under "Upgrading to 1.2.0" in Integration.md:
    • Billing 8 purchases can now be auto-logged when Meta enables it server side.
    • Events carry the screen title and AppLink URL unless AutoLogMetaDataEnabled=false.
    • A local AutoLogAppEventsEnabled=false now wins over the Events Manager setting.

Pre-existing issues fixed along the way

  • The sample never applied the default Login feature. It imported only the .targets, so facebook-login was not injected. It now imports the .props like NuGet does, and Docs/SourceMode.md says to do the same.
  • The sample crashed at launch with 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 references Xamarin.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_TERMS secret in CI, .git/info/private-terms locally). 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 5 KFBA001 cases.
  • 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 the javap -s descriptors in the wrapper AAR.
  • dotnet pack then python3 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 of Login, and the AppEvents wiring.
  • Sample builds:
    • Debug: passes.
    • Release with R8, then verify-interop.py apk: the facade, every called method and the SDK entry points survive in the dex.
  • Package-mode consumer (Login;AppEvents, R8, packed nupkg): KFBA001 fires without the referrer library, and the APK check passes.
  • Runtime smoke test: R8 Release x86_64 build of the sample on an API 36 emulator, with the consent switches set to off in the manifest.
    • The getters reflect the manifest values, and GetAnonymousId() returns an XZ… ID.
    • After "Grant consent", the SDK reports itself fully initialized, with auto-init and advertiser ID on.
    • StartTrial, Subscribe, the custom event and Flush all ran without exceptions.
    • The Graph calls were rejected only because of the placeholder app ID.
    • After withdrawing consent and relaunching, the SDK logs GraphRequest can't be used when Facebook SDK isn't fully initialized, which is now documented.
  • Not done: delivery to Meta Events Manager > Test events. This needs a real Meta app ID and token, and none are committed.

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 reaches master:

  • Public-tree scanner no longer embeds a private term. The hex-escaped pattern is gone from scripts/verify-public.py in every commit. Private terms come from the optional KAPUSCH_PRIVATE_TERMS repository secret (wired in ci.yml and publish.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.
  • Commit hygiene. The .pro wording now lands in the packaging commit, and the version bump is its own chore(release) commit at the end.

New commits on top:

  1. fix(appevents): create a logger per call — AppEventsLogger captures the current access token at construction and sends every event under it (checked with javap on 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 inspecting POST /<app id>/activities, since the SDK redacts tokens in logcat).
  2. fix(appevents): fail before SDK init and apply LDU in InitializeSdk — before sdkInitialize, FacebookSdk.setDataProcessingOptions throws inside CrashShield, which swallows it, so SetDataProcessingOptions(LDU) + InitializeSdk() silently dropped LDU in the provider-removed setup. Now every setter, identity and event call throws until the SDK is initialized, and InitializeSdk takes the options and applies them between sdkInitialize and fullyInitialize, the only point where they reach the SDK's first Graph requests. InitializeSdk() with no arguments keeps working.
  3. refactor(appevents): align the managed API with the iOS package — adopt the iOS names (eventName, LogCompleteRegistration value/currency, FacebookAppEvents* type names, SetMetadataCollectionEnabled, string? GetAnonymousId) and the iOS user-data semantics (a null field clears it; the facade calls clearUserData before setUserData, both on the SDK's single-threaded executor). The constants generator now spells AppLink/IPhone/IPad like 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.
  4. docs(appevents): state that withdrawal applies at the next process start — setAutoInitEnabled(false) never clears the fully-initialized state, and the lifecycle callbacks registered by ActivateApp keep logging activation on every resume. The recipe now says so, recommends gating the app's own Log* calls plus ExplicitOnly without further Flush(), and notes that queued events stay on disk. ActivateApp docs 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.
  5. 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

  • No secrets committed (samples use all-zero placeholders; the scan enforces it)
  • Built wrapper: bash src/Kapusch.FacebookApisForAndroidComponents/Native/Android/build.sh
  • Restored pinned AARs: bash src/Kapusch.FacebookApisForAndroidComponents/Native/Android/restore-facebook-aars.sh
  • Packed NuGet: dotnet pack src/Kapusch.FacebookApisForAndroidComponents/Kapusch.FacebookApisForAndroidComponents.csproj -c Release -o artifacts/nuget
  • Ran the validators listed in README.md
  • Updated docs

Next, after merge: tag v1.2.0-rc.1 on the merge commit, following Docs/Release.md.

🤖 Generated with Claude Code

Kapusch and others added 16 commits October 26, 2026 23:46
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
Kapusch merged commit c9bd1d2 into master Oct 26, 2026
1 check passed
@Kapusch
Kapusch deleted the feat/app-events branch October 26, 2026 23:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant