|
| 1 | +# Analytics |
| 2 | + |
| 3 | +AnkiDroid sends opt-in anonymous usage data to Google Analytics 4 (GA4). The |
| 4 | +transport is [google-analytics-kt][lib] — see its README for the library's |
| 5 | +setup, builder API, and the full list of hit types. This document covers |
| 6 | +only the AnkiDroid-side wiring and the questions reviewers tend to have. |
| 7 | + |
| 8 | +[lib]: https://github.com/criticalAY/google-analytics-kt |
| 9 | + |
| 10 | +--- |
| 11 | + |
| 12 | +## What AnkiDroid actually sends |
| 13 | + |
| 14 | +Of the six hit types the library supports, AnkiDroid uses three: |
| 15 | + |
| 16 | +| Used by AnkiDroid | Sent from | |
| 17 | +|---|---| |
| 18 | +| `screen_view` | `AnkiDroidUsageAnalytics.sendAnalyticsScreenView(...)` | |
| 19 | +| `event` | `AnkiDroidUsageAnalytics.sendAnalyticsEvent(...)` | |
| 20 | +| `exception` | `AnkiDroidUsageAnalytics.sendAnalyticsException(...)` (truncated to 150 chars) | |
| 21 | + |
| 22 | +The complete list of categories and actions lives in |
| 23 | +[`AnalyticsConstants.kt`](../../AnkiDroid/src/main/java/com/ichi2/anki/analytics/AnalyticsConstants.kt). |
| 24 | +We never include card content, deck names, note fields, sync credentials, or |
| 25 | +file paths. |
| 26 | + |
| 27 | +`client_id` is a UUID generated and persisted once per install in a dedicated |
| 28 | +prefs file (`analyticsPrefs`); it is not tied to any Anki account and survives |
| 29 | +profile switches. |
| 30 | + |
| 31 | +--- |
| 32 | + |
| 33 | +## Consent when does a hit actually leave the device? |
| 34 | + |
| 35 | +Default state: **opted out**. A hit leaves the device only if all of the |
| 36 | +following are true: |
| 37 | + |
| 38 | +1. The user has ticked the opt-in checkbox in the analytics dialog or |
| 39 | + settings. Pref key: `analytics_opt_in_v2`, default `false`. |
| 40 | +2. `AnkiDroidUsageAnalytics.optIn` is true every `send…` function starts |
| 41 | + with `if (!optIn) return`. |
| 42 | +3. The library's own `config.enabled` is true `GaImpl.send()` re-checks |
| 43 | + this. We pass `enabled = optIn` and call `reinitialize()` when opt-in |
| 44 | + flips, so this is belt-and-suspenders. |
| 45 | +4. The process won the sampling roll (see below). |
| 46 | + |
| 47 | +If any check fails the hit is dropped before any network I/O. |
| 48 | + |
| 49 | +--- |
| 50 | + |
| 51 | +## Performance |
| 52 | + |
| 53 | +Calls are fire-and-forget on `Dispatchers.IO` `sendAsync` returns |
| 54 | +immediately. There is no on-disk queue. |
| 55 | + |
| 56 | +AnkiDroid-specific knobs: |
| 57 | + |
| 58 | +- **Sampling**: configured via `R.integer.ga_sampleFrequency` (production: 10 |
| 59 | + → 10% of installs are in-sample for their process lifetime). `setDevMode()` |
| 60 | + forces 100%. |
| 61 | +- **Batching**: left off. Each hit is its own POST. See the library's |
| 62 | + [Limitations][lib-limits] for what batching would buy us. |
| 63 | + |
| 64 | +[lib-limits]: https://github.com/criticalAY/google-analytics-kt#limitations |
| 65 | + |
| 66 | +--- |
| 67 | + |
| 68 | +## Network failures |
| 69 | + |
| 70 | +The library wraps every HTTP call in `try/catch` on any exception (server |
| 71 | +down, no internet, captive portal, timeout) it returns |
| 72 | +`GaResponse(statusCode = -1)`, logs at ERROR, and the hit is gone. No retry, |
| 73 | +no backoff, no offline replay see the library [Limitations][lib-limits]. |
| 74 | +Default timeouts are 10s connect / 30s read, blocking only the IO coroutine. |
| 75 | + |
| 76 | +--- |
| 77 | + |
| 78 | +## Can it crash AnkiDroid? |
| 79 | + |
| 80 | +**Send path: no.** Both `sendAsync`'s outer launch and the inner OkHttp call |
| 81 | +have `try/catch (Exception)`. |
| 82 | + |
| 83 | +**Init path: the only realistic risk.** `AnkiDroidUsageAnalytics.initialize` |
| 84 | +is called from `AnkiDroidApp.onCreate()` and runs |
| 85 | +`GoogleAnalytics.builder { ... }`, which constructs an `OkHttpClient`. The |
| 86 | +builder does not validate `measurementId`/`apiSecret` today empty strings |
| 87 | +just produce bad URL params, not exceptions. But to stay safe against future |
| 88 | +library changes, we should wrap `initialize` in |
| 89 | +`runCatching { ... }.onFailure { Timber.e(it) }` so a bad config can never |
| 90 | +prevent app start. |
| 91 | + |
| 92 | +--- |
| 93 | + |
| 94 | +## Debug logging seeing what would be sent |
| 95 | + |
| 96 | +The library uses [`io.github.oshai:kotlin-logging`][klog] (an slf4j facade) |
| 97 | +and already logs the full request body, response, sampling decision, and |
| 98 | +drop reasons. To see them in AnkiDroid debug builds, add an slf4j → Timber |
| 99 | +bridge in the debug flavor (e.g. the `slf4j-timber` artifact, or a small |
| 100 | +custom binding). No library change required, no-op in release. |
| 101 | + |
| 102 | +[klog]: https://github.com/oshai/kotlin-logging |
| 103 | + |
| 104 | +--- |
| 105 | + |
| 106 | +## Setting up your own GA4 property |
| 107 | + |
| 108 | +For getting a measurement ID + API secret from the GA4 admin panel, follow |
| 109 | +the library's [Prerequisites section][lib-prereqs]. Once you have them, plug |
| 110 | +them into AnkiDroid like so: |
| 111 | + |
| 112 | +| Value | Where it goes | |
| 113 | +|---|---| |
| 114 | +| Measurement ID (`G-XXXXXXXX`) | `AnkiDroid/src/main/res/values/analytic_constants.xml` → `ga_trackingId` | |
| 115 | +| API secret | `local.properties` → `ANALYTICS_API_KEY=...` (read at compile time into `BuildConfig.ANALYTICS_API_KEY`, see [`AnkiDroid/build.gradle`](../../AnkiDroid/build.gradle)) | |
| 116 | + |
| 117 | +Builds without an `ANALYTICS_API_KEY` fall back to `DUMMY_API_XXX`, which GA |
| 118 | +rejects at ingest contributor builds can't accidentally write to our |
| 119 | +production property. |
| 120 | + |
| 121 | +[lib-prereqs]: https://github.com/criticalAY/google-analytics-kt#prerequisites |
| 122 | + |
| 123 | +### Keeping dev traffic out of prod |
| 124 | + |
| 125 | +A signed dev build with the real `ANALYTICS_API_KEY` would hit our |
| 126 | +production GA4 property. Two reasonable options when we decide to address |
| 127 | +this: |
| 128 | + |
| 129 | +- A separate GA4 property for the `debug` flavor (cleanest, needs another |
| 130 | + secret). |
| 131 | +- Set `debug = true` on the library config in debug builds that routes to |
| 132 | + GA's validation-only endpoint, which doesn't record. |
| 133 | + |
| 134 | +Not implemented yet. |
0 commit comments