The analytics API records how your application is used in the field. It’s built around a small provider SPI: you register one or more providers with the Analytics entry point, then report screen views, events, user properties and crashes. The Analytics class fans every call out to all registered providers, but only after the matching consent has been granted. Consent is central to this API and is explained in Consent and privacy below.
Codename One ships several providers: a first-party service that reports into the Codename One cloud (with capabilities gated by your subscription tier), Google Analytics 4, Matomo (a privacy-first, self-hostable option), Firebase, and a logging provider for the simulator and tests. You can also write your own provider for any other backend.
AnalyticsService class still works but is deprecated. It now delegates to the API described here. See Migrating from AnalyticsService at the end of this chapter.Registering providers
Register providers once, early in your app’s lifecycle. Providers are plain objects — there is no reflection or service lookup, which keeps the mechanism safe under the obfuscation applied to release builds.
Once at least one provider is registered, report usage from anywhere in your app:
Analytics.screen("Home", null);
// Pick a name of your own. Do not send "purchase" yourself: Purchase
// emits that one for every completed receipt, so a manual copy counts
// the same transaction twice and skews the funnel. The automatic events
// are listed further down this chapter.
Analytics.event(AnalyticsEvent.create("tutorial_complete")
.category("onboarding")
.param("step", "checkout")
.param("seconds", 42)
.build());
Analytics.setUserProperty("plan", "pro");
Analytics.crash(throwable, "checkout failed", false);
Analytics.crash emits a lightweight exception event (for example GA4’s app_exception) to whichever analytics backends you have registered, so exception counts appear next to your usage metrics. It’s separate from Codename One Crash Protection (com.codename1.crash.CrashProtection), which is a dedicated pipeline that records full, symbolicated stack traces to the build cloud crash console for debugging. The two are independent: use Crash Protection to diagnose crashes, use Analytics.crash when you also want exception events in your analytics, and enable either or both.
Consent and privacy
The API is designed to help you comply with privacy regulations such as GDPR and CCPA. A ConsentMode controls what happens before the user makes an explicit choice. The default is OPT_IN: nothing is collected or transmitted until the application records consent.
AnalyticsConsent.all() grants every category and AnalyticsConsent.none() denies every category. The older granted() and denied() methods remain as aliases.
Consent is broken down by category, so you can honor granular choices — for example allowing crash reporting while declining behavioral analytics:
Analytics.setConsent(AnalyticsConsent.builder()
.analytics(true)
.crashReporting(true)
.personalization(false)
.build());
The consent choice is persisted, so it survives restarts. Screen views and events require the analytics category, crash reports require crashReporting, and setUserId requires personalization. Calls made without the matching consent are dropped.
Consent you already hold
If your application has already obtained consent through its own mechanism — a custom prompt, a third-party consent-management platform, an enterprise device-management policy, or a jurisdiction where you have determined consent isn’t required — record it once at startup so reporting flows without a second prompt:
Analytics.setConsent(AnalyticsConsent.all());
You can also seed every category and then revoke just one with the builder, for example to allow analytics but not ad storage:
Analytics.setConsent(AnalyticsConsent.builder().all().adStorage(false).build());
If you would rather not gate at all, switch the default so collection is active unless the user withdraws it. This places the compliance responsibility on you as the integrator:
Analytics.setConsentMode(ConsentMode.OPT_OUT);
Note that the service records pseudonymous data rather than anonymous data. The client id described below is a persistent identifier stored on the device; it isn’t tied to a real-world identity and the server discards full IP addresses, but storing and reading such an identifier for usage tracking is often treated as consent-requiring under GDPR and similar regimes. That’s why opt-in is the safe default. Relax it only when you are confident the change fits your use.
Client identity and erasure
Each device is identified by a pseudonymous client id. It’s generated on first use and stored locally — it isn’t derived from any hardware identifier. To honor an erasure request ("right to be forgotten"), reset it:
Analytics.resetClientId();
For the first-party service this can be paired with a server-side deletion of the previous id’s data.
Automatic events
Codename One emits a few events for you, so the common funnels work without any wiring. Like every report these are consent-gated and do nothing until a provider is registered, so they’re always safe.
| Event | When it fires |
|---|---|
|
|
| A purchase completes and its receipt is posted. The gap between this and |
| A social login ( |
| A social login fails, with the provider in |
| The native share sheet is invoked. |
| The invite funnel, under the |
You can add your own events at any time with Analytics.event(…); these built-ins simply save you the boilerplate for the most common funnels.
Built-in providers
Codename One first-party service
CodenameOneAnalyticsProvider batches events and posts them to the Codename One cloud, which aggregates them into the reports shown in the developer console. App identity is read from the build-injected properties, so no API key is embedded in your app.
The capabilities you actually get — screen views, custom events, user properties, retention window, raw export — are gated server-side by your subscription tier. Higher tiers retain data longer and unlock richer reporting.
Google Analytics 4
GoogleAnalyticsProvider uses the GA4 Measurement Protocol. Create it with a measurement id (G-XXXXXXXX) and a Measurement Protocol API secret from the GA4 admin console:
Analytics.addProvider(new GoogleAnalyticsProvider("G-XXXXXXXX", "API_SECRET"));
Screen views are sent as the GA4 screen_view event and crashes as app_exception.
Matomo
MatomoAnalyticsProvider targets Matomo (formerly Piwik) through its HTTP tracking API. Matomo can be self-hosted and supports IP anonymization, which makes it a good fit for privacy-sensitive deployments:
Analytics.addProvider(new MatomoAnalyticsProvider("https://matomo.example.com", 1));
Firebase
FirebaseAnalyticsProvider forwards to the native Firebase Analytics SDK on Android and iOS. Register it like any other provider:
Analytics.addProvider(new FirebaseAnalyticsProvider());
Firebase reports only from a native Android or iOS build, and only when two things are in place. Without both, FirebaseAnalyticsProvider does nothing — which is why it’s always safe to register, including in the Codename One simulator and the desktop build, where it’s a no-op.
First, add the Firebase configuration file for your app (downloaded from the Firebase console) to the matching platform module’s resources:
Android:
android/src/main/resources/google-services.jsoniOS:
ios/src/main/resources/GoogleService-Info.plist
These are Maven project paths. The native/android and native/ios directories from the old Ant projects no longer apply.
Second, enable the SDK with build hints:
codename1.arg.android.firebaseAnalytics=true
codename1.arg.ios.firebaseAnalytics=true
These hints add the Firebase Analytics Gradle dependency (Android) and the Firebase/Analytics pod (iOS), and make the build generate the native bridge that FirebaseAnalyticsProvider talks to.
Logging provider
LoggingAnalyticsProvider logs every call instead of sending it anywhere. It’s the recommended default in the simulator and is used as the backing provider in tests.
Writing your own provider
Implement AnalyticsProvider, or extend AbstractAnalyticsProvider and override only the calls you support. The Analytics class handles consent gating and fan-out for you, so a provider just forwards the data to its backend.
The init(AnalyticsContext) callback hands you the app name, version, platform, locale and the pseudonymous client id. onConsentChanged(AnalyticsConsent) lets a provider reconfigure when the user updates consent. supports(AnalyticsCapability) lets tooling introspect which features a provider offers.
Invite a friend
com.codename1.analytics.invite follows an invitation through to what it caused: who sent it, who installed because of it, and what that invited cohort went on to spend. It replaces what Firebase Dynamic Links and Firebase Invites used to do, both of which have been shut down.
Sending an invite
Invite invite = Invites.create(InviteRequest.create()
.campaign("spring")
.channel("share_sheet")
.title("Join me")
.description("I am using this and thought of you.")
.build());
Invites.share(invite, "Come and try this with me");
Invites.create returns immediately and works with no network at all, so the share sheet never waits on a server — the code is generated on the device and registered with the link service in the background. title and description drive the preview card the link shows in a messaging app.
Invites.share reports invite_shared only when the platform confirms the user shared; a dismissed sheet reports invite_share_dismissed. If you send the invite through your own user interface instead, call Invites.reportShareResult from your share callback so the funnel still records it.
Receiving an invite
Invites.setInviteListener(new InviteListener() {
public void inviteReceived(InviteAttribution attribution) {
// attribution.getCampaign(), getCode(), getPayload()
if (Invites.MATCH_APP_CLIP.equals(attribution.getMatchType())) {
// An iOS App Clip received the link and handed the code
// over. Exact, like every other match type.
}
}
public void attributionUnavailable(String reason) {
// The ordinary outcome: most installs are not invited.
}
});
Invites.checkForInvite();
Call checkForInvite() from start(). It’s a pull rather than a callback on purpose: Android delivers a link by replacing the activity intent and iOS by setting a property, and reading the launch argument is the one path that behaves the same on both. An answer that arrives before you register a listener — which happens routinely on a cold launch from a link — is held and delivered when you register.
Exactly one of the two listener methods is called per install, and neither is called again on later launches.
Closing the funnel
// Once the invited user reaches whatever the invite existed for.
Invites.conversion("signup");
Invites.conversion("subscribed", 9.99, "USD");
When attribution resolves it’s also written as persistent analytics dimensions (cn1_campaign, cn1_channel, cn1_invite_code, cn1_invite_match). Every later event carries them, including the purchase event the framework already emits for you, so revenue and lifetime value per campaign come out of the reports you already have without any extra wiring. The cn1_ prefix is reserved — don’t use it for your own dimensions.
How exact the answer is
InviteAttribution.getMatchType() tells you how the attribution was made, and the three answers aren’t equally trustworthy:
| Match type | What it means |
|---|---|
| The link opened an app that was already installed. Exact. |
| The invite code travelled through the app store and came back verbatim. Exact. This is the Android path. |
| An iOS App Clip received the invite link and handed the code to the app the person then installed. Exact. This is the iOS path. |
Every match type is exact. The App Store carries no referrer parameter of its own, so on iOS the code travels a different road than it does on Android: tapping the invite link offers an App Clip, the clip is launched by the link itself and so receives the code exactly, and it leaves that code where the full app can read it after installation. Nothing is matched, estimated or guessed, and a referral bounty can be paid on any of these.
Invites.setAttributionWindow(0) stops a deferred lookup being started at all — neither the Play install referrer nor the App Clip handoff is read. An exact code the device is already holding, one that arrived on a link, is still claimed: there is nothing to defer about it.
Consent
Everything reported here is gated on the analytics consent category, and nothing is transmitted until consent is granted.
Nothing at all is collected about someone who only taps a link. An earlier design wrote a coarse device profile to local storage on first launch — OS version, hardware model, language, screen size — so an iOS install could be matched to a click, and the server kept a hashed fingerprint of the visitor’s address for seven days to match it against. App Clips made all that unnecessary: the clip is handed the code by the link, so there is nothing to match and nothing to keep.
Analytics.resetClientId() erases the invite attribution along with the identity, so an erasure request can’t leave a fresh pseudonymous id linked to the same inviter.
What the build does for you
Referencing this package makes the build wire the platform side: an autoVerify App Links intent filter on Android, an associated domain on iOS, and the Play Install Referrer dependency. An app that only reports analytics gets none of it.
On iOS it also generates the App Clip, because there is no attribution without one. The clip is a separate binary embedded in your app: a few hundred lines of UIKit that show your app’s name, offer to install it, and record the invite code the link handed them. It’s not a Codename One application and doesn’t run your code. You don’t write it, open it or maintain it.
Three things the generated clip needs from you, and each fails in its own way:
The App Store identifier of your app, in
ios.invite.appStoreId. Without it the clip still records the code, it simply shows no install sheet — which is the right behavior before your first release, when the app doesn’t exist in the store yet.An App Group registered on your developer account. The build derives one from your package name and adds it to
ios.app_groups; override it withios.invite.appGroupif you already have one. A group that isn’t registered signs cleanly and the clip and the app can then never reach each other, which is the failure with no symptom.An App Clip enabled on your App ID, alongside Associated Domains.
An Advanced App Clip Experience registered in App Store Connect for your invite link prefix. Codename One authorizes your clip for the domain; what maps a particular link to a particular clip is that experience, and until it exists tapping the invite link shows no clip card at all. The console shows the prefix to register once invites are switched on.
Set ios.invite.appClip to false only if you ship an App Clip of your own. The build then generates no clip, but your app still gets the shared App Group and the reader, so a clip of yours that writes the handoff is still picked up.
android.invite.signingFingerprint. Without it verification fails on every Play install, the link opens the browser instead of your app, and nothing reports an error.Migrating from AnalyticsService
The deprecated AnalyticsService continues to compile and now routes through this API. To preserve its historical always-on behavior — which predates the consent model — AnalyticsService.init switches the default consent mode to OPT_OUT. New code should migrate to Analytics and manage consent explicitly:
| Deprecated call | Replacement |
|---|---|
|
|
|
|
|
|
subclassing | implement |