-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
doc(analytics): add README.md for GA4 analytics #21142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| # Analytics | ||
|
|
||
| AnkiDroid sends opt-in anonymous usage data to Google Analytics 4 (GA4). The | ||
| transport is [google-analytics-kt][lib] — see its README for the library's | ||
| setup, builder API, and the full list of hit types. This document covers | ||
| only the AnkiDroid-side wiring and the questions reviewers tend to have. | ||
|
|
||
| [lib]: https://github.com/criticalAY/google-analytics-kt | ||
|
|
||
| --- | ||
|
|
||
| ## What AnkiDroid actually sends | ||
|
|
||
| Of the six hit types the library supports, AnkiDroid uses three: | ||
|
|
||
| | Used by AnkiDroid | Sent from | | ||
| |---|---| | ||
| | `screen_view` | `AnkiDroidUsageAnalytics.sendAnalyticsScreenView(...)` | | ||
| | `event` | `AnkiDroidUsageAnalytics.sendAnalyticsEvent(...)` | | ||
| | `exception` | `AnkiDroidUsageAnalytics.sendAnalyticsException(...)` (truncated to 150 chars) | | ||
|
|
||
| The complete list of categories and actions lives in | ||
| [`AnalyticsConstants.kt`](../../AnkiDroid/src/main/java/com/ichi2/anki/analytics/AnalyticsConstants.kt). | ||
| We never include card content, deck names, note fields, sync credentials, or | ||
| file paths. | ||
|
|
||
| `client_id` is a UUID generated and persisted once per install in a dedicated | ||
| prefs file (`analyticsPrefs`); it is not tied to any Anki account and survives | ||
| profile switches. | ||
|
|
||
| --- | ||
|
|
||
| ## Consent when does a hit actually leave the device? | ||
|
|
||
| Default state: **opted out**. A hit leaves the device only if all of the | ||
| following are true: | ||
|
|
||
| 1. The user has ticked the opt-in checkbox in the analytics dialog or | ||
| settings. Pref key: `analytics_opt_in_v2`, default `false`. | ||
| 2. `AnkiDroidUsageAnalytics.optIn` is true every `send…` function starts | ||
| with `if (!optIn) return`. | ||
| 3. The library's own `config.enabled` is true `GaImpl.send()` re-checks | ||
| this. We pass `enabled = optIn` and call `reinitialize()` when opt-in | ||
| flips, so this is belt-and-suspenders. | ||
| 4. The process won the sampling roll (see below). | ||
|
|
||
| If any check fails the hit is dropped before any network I/O. | ||
|
|
||
| --- | ||
|
|
||
| ## Performance | ||
|
|
||
| Calls are fire-and-forget on `Dispatchers.IO` `sendAsync` returns | ||
| immediately. There is no on-disk queue. | ||
|
|
||
| AnkiDroid-specific knobs: | ||
|
|
||
| - **Sampling**: configured via `R.integer.ga_sampleFrequency` (production: 10 | ||
| → 10% of installs are in-sample for their process lifetime). `setDevMode()` | ||
| forces 100%. | ||
| - **Batching**: left off. Each hit is its own POST. See the library's | ||
| [Limitations][lib-limits] for what batching would buy us. | ||
|
|
||
| [lib-limits]: https://github.com/criticalAY/google-analytics-kt#limitations | ||
|
|
||
| --- | ||
|
|
||
| ## Network failures | ||
|
|
||
| The library wraps every HTTP call in `try/catch` on any exception (server | ||
| down, no internet, captive portal, timeout) it returns | ||
| `GaResponse(statusCode = -1)`, logs at ERROR, and the hit is gone. No retry, | ||
| no backoff, no offline replay see the library [Limitations][lib-limits]. | ||
| Default timeouts are 10s connect / 30s read, blocking only the IO coroutine. | ||
|
|
||
| --- | ||
|
|
||
| ## Can it crash AnkiDroid? | ||
|
|
||
| **Send path: no.** Both `sendAsync`'s outer launch and the inner OkHttp call | ||
| have `try/catch (Exception)`. | ||
|
|
||
| **Init path: the only realistic risk.** `AnkiDroidUsageAnalytics.initialize` | ||
| is called from `AnkiDroidApp.onCreate()` and runs | ||
| `GoogleAnalytics.builder { ... }`, which constructs an `OkHttpClient`. The | ||
| builder does not validate `measurementId`/`apiSecret` today empty strings | ||
| just produce bad URL params, not exceptions. But to stay safe against future | ||
| library changes, we should wrap `initialize` in | ||
| `runCatching { ... }.onFailure { Timber.e(it) }` so a bad config can never | ||
| prevent app start. | ||
|
|
||
| --- | ||
|
|
||
| ## Debug logging seeing what would be sent | ||
|
|
||
| The library uses [`io.github.oshai:kotlin-logging`][klog] (an slf4j facade) | ||
| and already logs the full request body, response, sampling decision, and | ||
| drop reasons. To see them in AnkiDroid debug builds, add an slf4j → Timber | ||
| bridge in the debug flavor (e.g. the `slf4j-timber` artifact, or a small | ||
| custom binding). No library change required, no-op in release. | ||
|
|
||
| [klog]: https://github.com/oshai/kotlin-logging | ||
|
|
||
| --- | ||
|
|
||
| ## Setting up your own GA4 property | ||
|
|
||
| For getting a measurement ID + API secret from the GA4 admin panel, follow | ||
| the library's [Prerequisites section][lib-prereqs]. Once you have them, plug | ||
| them into AnkiDroid like so: | ||
|
|
||
| | Value | Where it goes | | ||
| |---|---| | ||
| | Measurement ID (`G-XXXXXXXX`) | `AnkiDroid/src/main/res/values/analytic_constants.xml` → `ga_trackingId` | | ||
| | API secret | `local.properties` → `ANALYTICS_API_KEY=...` (read at compile time into `BuildConfig.ANALYTICS_API_KEY`, see [`AnkiDroid/build.gradle`](../../AnkiDroid/build.gradle)) | | ||
|
|
||
| Builds without an `ANALYTICS_API_KEY` fall back to `DUMMY_API_XXX`, which GA | ||
| rejects at ingest contributor builds can't accidentally write to our | ||
| production property. | ||
|
|
||
| [lib-prereqs]: https://github.com/criticalAY/google-analytics-kt#prerequisites | ||
|
|
||
| ### Keeping dev traffic out of prod | ||
|
|
||
| A signed dev build with the real `ANALYTICS_API_KEY` would hit our | ||
| production GA4 property. Two reasonable options when we decide to address | ||
| this: | ||
|
|
||
| - A separate GA4 property for the `debug` flavor (cleanest, needs another | ||
| secret). | ||
| - Set `debug = true` on the library config in debug builds that routes to | ||
| GA's validation-only endpoint, which doesn't record. | ||
|
|
||
| Not implemented yet. | ||
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.