Skip to content

Commit bdae1a8

Browse files
committed
doc(analytics): add README.md for GA4 analytics
1 parent ad2182e commit bdae1a8

1 file changed

Lines changed: 136 additions & 0 deletions

File tree

docs/analytics/README.md

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

0 commit comments

Comments
 (0)