Skip to content

Commit e314f6d

Browse files
committed
doc(analytics): add README.md for GA4 analytics
1 parent 05d51b8 commit e314f6d

1 file changed

Lines changed: 134 additions & 0 deletions

File tree

docs/analytics/README.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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

Comments
 (0)