Etch Privacy Policy
v2.9 — Last updated: 2026-09-27
Etch is a spaced-repetition flashcard app for Android. It is built to work entirely on your device. This page explains, in plain language, what that means for your data — and what changes when you opt in to the optional crash-reporting and feedback features.
1. Overview
- Etch is local-first by design. Your cards, decks, and study history live in private storage on your device. Etch has no server or account of its own and never syncs your data to one. (Android's own system backup can copy this data to your Google Drive — see §12 for exactly what, and how to turn it off.)
- There are no ads, no third-party advertising SDKs, no Firebase, no analytics SDKs, and no user accounts.
- Etch has two network-touching features: (a) the optional crash / feedback flow that ships data to our error-tracking service (Sentry) — in production builds it is OFF by default and you control it from Settings → Privacy; in beta/testing builds it is ON by default and cannot be turned off in-app, so we can diagnose problems quickly during the test period (see §6); and (b) Google Play Billing, which contacts Google Play to check, complete, and restore Etch's one-time in-app purchases — Etch Pro, Etch Supporter, the Supporter upgrade, and individual premium decks (see §7a). Neither transmits your card content, and Google — not Etch — processes any payment.
- Even with telemetry off, the Send feedback form remains available — if you tap it and submit, what you typed (plus the context items listed in §5) is sent. This is explicit user intent, so it bypasses the on/off toggle.
The rest of this document explains, field by field, exactly what is collected, when, and where it goes.
2. What's collected by default
Production builds
Nothing. No crash reports, no usage data, no events. The Sentry SDK is initialised so the toggle in Settings can switch it on if you choose, but until you do, every event the SDK would emit is gated and dropped before it reaches the network.
The only data that ever leaves your device without you opting in is data you explicitly trigger yourself — for example, by tapping Export, picking an Import file, or hitting Submit on the Send feedback screen.
On devices with Google Play there is one platform exception: at launch Etch automatically asks Google Play which of its in-app products you own (and their prices), regardless of the telemetry toggle. That request is handled by Google Play, not by an Etch server, and carries no card or personal content (see §7a and §10). On a device without Google Play the check returns "unavailable" and nothing is sent.
Beta builds
Beta builds are a separate, time-bounded pre-launch APK distributed to testers through the Play Console closed-testing track. In beta telemetry is ON by default and cannot be turned off in-app — the Settings → Privacy toggle that lets production users opt out does not appear in a beta build, so there is nothing in the app to switch off for the beta duration. Taking part in the closed test is how testers consent to it, and the app also mentions it on a one-time welcome card on its home screen — see §6 for details. Once v1 launches, no beta APK is distributed.
3. What's collected when telemetry is enabled in production
When you turn on Settings → Privacy → Anonymous usage data, the following surfaces become active. Every event passes through a local consent gate (beforeSend) and a path-scrubber that replaces any app-private file path with the literal token <media-path> before the event leaves your device. The same scrubbing applies to breadcrumb messages and to exception text.
3.1 Crashes (uncaught exceptions + ANRs + native crashes)
When Etch crashes, Sentry's SDK captures:
- Stack trace. Includes Etch's own class names and method names. File paths inside the stack frames are scrubbed.
- Exception type and message. Message text is scrubbed of any app-private storage paths and truncated to 500 characters.
- Device fingerprint. Manufacturer (e.g.
samsung), model (e.g.Pixel 7), OS version (e.g.Android 14), CPU architecture, total memory, screen size and density bucket, and device locale (e.g.en-US). - App version. Application ID, version name, version code, and the
beta/productionenvironment tag. - Recent breadcrumbs. Up to the last 100 breadcrumbs from the current session — see §3.3.
- Tags. The full tag inventory from §3.2.
- ANRs (Android's "App Not Responding" signal) are treated the same as crashes.
- Native crashes (crashes in C/C++ libraries) are also captured. They contain native stack frames; no card content can be in them because card content never crosses into native code.
Things that are explicitly disabled in our Sentry configuration and so are never captured: automatic UI-interaction breadcrumbs (which would capture text-field content), automatic screenshots, view-hierarchy snapshots (which would expose the Compose UI tree), performance/tracing data, profiling data, and Session Replay.
3.2 Tags (attached to every event)
Tags are short categorical labels attached to every event we send — crashes, ANRs, recoverable warnings, and feedback. They make the Sentry dashboard filterable without our team needing to dig into event details. Every count is bucketed so a single tag value cannot be used to re-identify a specific user.
| Tag key | Value shape | Example | Source |
|---|---|---|---|
release |
com.barteknowak.etch@VERSION+CODE |
com.barteknowak.etch@1.0+5 |
App's build config |
environment |
beta, production, or qa (emulator / QA-fleet builds are routed to qa so test traffic never mixes with real users) |
production |
App's build config |
is_beta_build |
yes or no |
no |
App's build config |
card_count_bucket |
One of 0, 1-100, 100-1k, 1k-5k, 5k+ — never an exact count |
100-1k |
Total cards in your library |
deck_count_bucket |
One of 0, 1-3, 4-10, 11+ |
4-10 |
Number of decks (counts every card_lists row regardless of isFolder) |
folder_count_bucket |
One of 0, 1, 2-5, 6+ |
1 |
Number of card_lists rows flagged as folders. Lets the dashboard tell "user organises into folders" from "flat library." |
total_reviews_bucket |
One of 0, 1-100, 100-1k, 1k+ |
1k+ |
Lifetime review-log row count |
cards_due_now_bucket |
One of 0, 1-20, 21-100, 100+ |
21-100 |
Cards due at event time |
triggers_active |
Comma list drawn from unlock, timer, app, or the literal none |
unlock,timer |
Trigger settings |
dark_mode |
Currently always dark (Etch is dark-only at present) |
dark |
Theme preference |
font_scale_bucket |
One of <=1.0, 1.0-1.3, >1.3 |
<=1.0 |
System font-scale setting |
oem |
Build.MANUFACTURER lowercased |
samsung / xiaomi / oppo |
Android build properties |
onboarding_completed |
yes or no |
yes |
Onboarding preference |
last_import_format |
One of anki, csv, etch, none |
anki |
Last successful import |
days_since_install_bucket |
One of 0, 1-7, 8-30, 30+, or unknown |
8-30 |
PackageInfo.firstInstallTime |
has_overlay_perm |
yes or no |
yes |
Settings.canDrawOverlays() |
has_notif_perm |
yes or no |
yes |
Notification permission |
has_usage_stats_perm |
yes or no |
no |
AppOps usage-stats check |
battery_optimization_exempt |
yes or no |
no |
PowerManager check |
app_locale |
lang-region |
en-US |
Locale.getDefault() |
density_bucket |
One of mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi |
xxhdpi |
Display density |
branch |
Git branch the build was compiled from; omitted when empty (e.g. a non-git source tarball) | main |
App's build config (build metadata, not PII) |
entitlement_tier |
free, pro, or supporter |
pro |
Which of Etch's one-time purchases this device has recorded as owned, from Google Play's answer the last time the app checked — never an order ID or purchase token |
owns_paid_deck |
yes or no |
no |
Whether that record includes a purchased premium deck |
paid_decks_bucket |
One of 0, 1, 2, 3+ — never which decks |
0 |
How many purchased premium decks that record includes |
entitlement_override |
none, seed, or forced |
none |
Whether this build behaves at a different tier than the one purchased: seed for a build compiled with Pro unlocked, forced for a developer test setting that exists only in debug builds. none in every build published on Google Play |
The four purchase tags (entitlement_tier through entitlement_override) describe what you have bought, so they are removed from a feedback message you send while usage data is off — the one kind of event that is sent without that setting (§5). They never name a deck, an order, or a payment. One thing does identify a Supporter whatever the setting, and it comes from where you send feedback, not from these tags: a message sent from the Supporter feedback row (Settings → Supporter, which only Supporters see) is labelled with that entry point (§4).
Tags refresh whenever the app comes to the foreground, so they reflect your current configuration rather than the configuration you had at install time.
3.3 Breadcrumbs (sent only with an event)
Breadcrumbs are a rolling log of recent in-app events. The SDK keeps up to 100 of them in memory; they are not transmitted on their own. They only ride along when an event (a crash, an ANR, a recoverable warning, or a feedback submission) is also being sent. If telemetry is off, breadcrumbs are gated off at the source too, so the buffer stays clean and a later opt-in won't surface stale pre-consent activity. Flipping the toggle off explicitly clears the buffer.
Every breadcrumb message and data field is also run through the path-scrubber as defence in depth.
| Category | Fires when | Data fields |
|---|---|---|
nav.screen_view |
You navigate between top-level screens | route (e.g. home, settings), from_route |
lifecycle.foreground |
App returns to the foreground | — |
lifecycle.background |
App goes to the background | — |
study.session_start |
A learn session starts | mode (e.g. QUICK), deck_count |
study.session_end |
A learn session ends | duration_ms_bucket, cards_reviewed |
study.card_reviewed |
You rate a card | rating (approve/deny/skip — Etch's grading is a two-button "Got it" / "Not quite", plus skip), time_to_answer_ms_bucket, card_type (classic/cloze/occlusion/other) |
import.started |
An import begins | format (anki/csv/etch, or unknown for an unrecognised file) |
import.completed |
An import succeeds | format, card_count_bucket, and a bucketed count for each warning category from PartialImportCounts (audio stripped, images unresolved, FSRS data unreadable, suspended cards imported, cloze rewrites, multi-template expansions, occlusion imported, closet groups, closet rects, closet markup skipped, CRT zero detected, LaTeX blocks stripped, media filenames normalised) |
import.failed |
An import errors out | format, failure_stage (parse/db/media) |
editor.opened |
You open the card editor | card_type |
editor.saved |
You save a card | card_type, is_new (boolean) |
editor.deleted |
You delete a card from the editor | card_type |
trigger.fired |
An unlock/timer/app trigger fires | trigger_type (unlock/timer/app; rarely manual) |
trigger.config_changed |
You toggle a trigger | trigger_type, enabled, plus (when set) trigger_app_packages (comma-separated list of public Play Store package identifiers — e.g. "com.reddit.frontpage,com.instagram.android" — never your app name labels or icons), trigger_timer_minutes_bucket (one of 0, 1-5, 6-15, 16-30, 31-60, 61-120, 121+) |
trigger.missed |
An alarm fires but Etch wasn't ready | trigger_type, reason (short bounded identifier such as dnd_suppressed, incoming_call, no_cards_due, daily_cap) |
overlay.shown |
A pop-up card appears | card_type |
overlay.dismissed |
The overlay is dismissed | via — same closed vocabulary as the overlay_outcome event in 3.4 below |
overlay.portrait_forced |
Portrait lock kicks in on the overlay | — |
overlay.suppressed_landscape |
Overlay would have shown but is suppressed | — |
overlay.more_sheet_shown |
The card's "More" sheet is opened from the overlay | mode (single/multi), tile_count (number of field rows — never their labels or content) |
audio.played |
Audio playback starts on a card | format (mp3/ogg/wav/m4a/opus/unknown) |
audio.failed |
Audio playback fails | format, phase (decode/io/focus/unknown) |
image.load_failed |
Coil reports a failed image load after retries | path_kind (filesDir/cacheDir/external/http/unknown), attempt_count_bucket — never the actual path |
permission.requested |
A runtime permission is requested | permission_name |
permission.granted |
A permission was granted | permission_name |
permission.denied |
A permission was denied | permission_name, rationale_shown (boolean) |
backup.created |
A backup is saved | card_count_bucket, size_bucket |
export.completed |
An export finishes | format, card_count_bucket |
settings.changed |
You change a boolean or enum setting | key (e.g. telemetry_enabled, dark_mode), new_value — only booleans and enums; free-text settings are excluded |
ai_assistant.invoked |
You open the AI Assistant to build a prompt | feature (short identifier, currently ai_assistant_prompt) — never the prompt text you build. Etch calls no AI model of its own; the prompt is for you to paste into a chatbot you already use |
shake.detected |
A shake gesture is detected | source (main/overlay) |
feedback.opened |
The Send feedback screen opens | entry_point (settings/shake_main/shake_overlay/supporter) |
deck.deleted |
You delete a deck or a list subtree | root_count, subtree_list_count — plain counts of the lists removed, never deck or list names |
overlay.proxy_direct_attach |
The overlay proxy timed out repeatedly and the service fell back to direct-attach for the rest of its lifetime | strikes (count of consecutive proxy timeouts) |
3.4 Named events (count against our Sentry quota)
Named events are sent as standalone Sentry events when they fire. Each is gated on the telemetry toggle. Some are sampled — a fixed fraction is sent rather than every occurrence — because their volume would otherwise dominate our quota. learn_session_completed is sampled at 1-in-5 in beta and search_filtered at 1-in-3 in beta; both send every occurrence in the public release, where far fewer people have telemetry switched on.
overlay_outcome is the one event sampled the other way round — more of it is sent in beta (1-in-3) than in the public release (1-in-10). It is the only event whose rate is driven by how much you use your phone rather than by a deliberate action you take, so it is the only one that could otherwise grow without bound. The per-event sample_rate field records which sampling rate was configured for the build. The counter behind it is per-app-start, so the first event after each start is always sent and the realised share is a little higher than the configured one.
Every other named event fires on each real-world occurrence.
| Event | When it fires | Fields | Sampling |
|---|---|---|---|
import_started |
An import begins | format |
every fire |
import_completed |
An import succeeds | format, card_count_bucket, duration_ms_bucket, and bucketed counts for every PartialImportCounts warning category (audio stripped, images unresolved, FSRS data unreadable, suspended cards imported, cloze rewrites, multi-template expansions, occlusion imported, closet groups, closet rects, closet markup skipped, CRT zero detected, LaTeX blocks stripped, media filenames normalised) |
every fire |
import_failed |
An import errors out | format, failure_stage, error_class (exception class name only — never the exception message) |
every fire |
trigger_enabled |
You toggle a trigger ON | trigger_type, plus (when set) trigger_app_packages (comma-separated list of public Play Store package identifiers — present only when trigger_type=app), trigger_timer_minutes_bucket (one of 0, 1-5, 6-15, 16-30, 31-60, 61-120, 121+ — present only when trigger_type=timer) |
every fire |
trigger_disabled |
You toggle a trigger OFF | trigger_type, plus (when set) trigger_app_packages, trigger_timer_minutes_bucket — same shape as trigger_enabled |
every fire |
learn_session_completed |
A learn session finishes | mode, duration_ms_bucket, cards_reviewed_bucket, accuracy_pct_bucket |
1-in-5 in beta (process-local counter); every fire in production |
learn_session_abandoned |
You exit a session before it ends | mode, cards_reviewed_bucket, progress_pct_bucket |
every fire |
onboarding_completed |
Onboarding completes for the first time | — | once per install |
feature_first_use |
The very first invocation of a tracked feature on this install | feature_name (short identifier; e.g. ai_assistant) |
once per feature per install (persistent across process restarts) |
overlay_permission_outcome |
After the SYSTEM_ALERT_WINDOW flow returns | outcome (granted/denied/skipped) |
every fire |
report_button_used |
You tap the in-card Report-card affordance | — | every fire |
deck_created |
You create a new deck via the in-app UI (NOT fired by imports — those already emit import_completed; NOT fired by folder creations) |
— | every fire |
editor_saved |
You save a card in the editor (both create and edit paths) | card_type (classic/cloze/occlusion/other), is_new (boolean) — same shape as the long-standing editor.saved breadcrumb, which continues to fire alongside |
every fire |
search_filtered |
You apply a search filter (tag chip, state chip, bulk property apply, or saved filter) | filter_kind (one of tag, state, property, saved_filter) — never your actual filter value (tag string, query text, state enum value) |
1-in-3 in beta (process-local counter); every fire in production |
custom_preset_created |
You save a named search filter from the search screen | — never the filter's user-typed name | every fire |
buy_page_viewed |
The buy / paywall page becomes visible | source (bounded entry-point label such as settings_pro_row, onboarding_pro_card, combine_triggers, stats_depth; unknown for an unrecognised entry point) |
every fire |
oem_autostart_opened |
You tap the OEM autostart guidance ("Let Etch start on its own") in onboarding or Settings | surface (onboarding/settings), route (bounded OEM route key, or none), outcome (oem_screen/app_details_fallback/failed), battery_exempt (boolean) — records only that we sent you to a system screen, never whether you changed anything there |
every fire |
purchase_started |
The Google Play purchase flow is launched for a product | product (bounded label etch_pro / etch_supporter / etch_supporter_upgrade / deck / other — the raw product id is never sent; every premium-deck purchase reports as the single label deck) |
every fire |
purchase_completed |
A purchase is acknowledged / owned | product (same bounded label set, including deck for any premium deck), is_first_purchase (boolean) — never an order ID or purchase token |
every fire |
purchase_failed |
A purchase attempt fails (WARNING level) | product (same bounded label set, including deck), failure_reason (user_canceled / billing_unavailable / item_already_owned / developer_error / network / unknown) |
every fire |
restore_completed |
A restore re-grants owned products (e.g. a reinstall on the same Google account) | restored_count_bucket (0 / 1 / 2+) — the raw count is never sent |
every fire |
overlay_outcome |
A pop-up card goes away | trigger_type (unlock/timer/app), overlay_outcome — how it ended, one of answered (you graded it), swiped (you skipped it), paused (you paused pop-ups from the card), edited (you opened it in the editor), screen_off, call_yield (an incoming call), service_stop, hidden_by_system (Android hid the overlay, e.g. for a banking app), dismissed (anything else), or unknown if a future code path reports something outside this list, card_type (classic/cloze/occlusion, or unknown), batch (single/multi — whether the pop-up carried one card or several), cards_in_batch, cards_answered, sample_rate. No card, deck or app identity is attached — only which kind of trigger produced the card and how it ended. |
1-in-3 in beta; 1-in-10 in the public release |
3.5 Recoverable warnings (deduped per session, per cause)
These are signals that something went silently wrong but the app handled it gracefully — you typically don't see them. They're shipped as Sentry events at WARNING level. Each distinct cause within a category is sent at most once per process to avoid quota burn — so a category with several possible causes (for example a trigger that could be skipped for any of a dozen reasons) can send one event per cause, not one event in total. Previously the first cause to occur suppressed all the others, which meant genuine faults could be hidden behind harmless ones.
| Category | When it fires | Extras |
|---|---|---|
audio_decode_failed |
Audio playback failed inside the decoder | format |
image_load_failed |
Coil reported an image load failure after its own retries | path_kind, attempt_count |
cloze_partial_parse |
Anki import finished with cloze-parse warnings | warning_count, format |
trigger_missed |
An unlock/timer/app trigger fired but no card was shown | trigger_type, reason — a fixed identifier from a closed list, never free text. Includes deliberate skips (dnd_suppressed, media_playing, landscape, active_call, paused, cooldown, …), window_hidden_by_system when another app hid the card, and overlay_permission_missing when the "Display over other apps" permission is off |
fgs_start_failed |
A foreground service failed to start — on app launch, when the app returns to the foreground, after a device reboot, from the background watchdog, after an import, or when you tap the restart button in Settings | service_class, phase (which of those six moments it was), error_class (exception class name only), error_desc (the Android system's own explanation of the refusal, truncated to 200 characters, with any file paths replaced before sending — never your card content) |
overlay_show_failed |
An uncaught failure in the overlay service's coroutine scope while attaching a pop-up card | scope (fixed value service) |
fgs_start_failed is the one warning that carries an exception message (error_desc), while import_failed in §3.4 deliberately carries only the exception class. The difference is what the text can contain: a foreground-service refusal is written by Android itself and describes the system's own policy state, whereas an import error can quote the file you chose. Paths are stripped from error_desc before it leaves the device, and it is capped at 200 characters.
The plan that drove this work listed nine recoverable categories, five of which are wired today; a sixth category, overlay_show_failed, was added afterwards and is also wired — so six are emitted from production code paths today. The remaining four planned categories (review_save_retry, migration_warning, overlay_bal_denied, fsrs_unexpected_state) are not currently emitted.
3.6 Session lifecycle
When telemetry is enabled, Etch records Sentry sessions. A "session" here is not a learn session — it's the SDK's term for a continuous foreground interval. Etch opens one when the app comes to the foreground and closes it when the app goes to the background. Sessions give us aggregate metrics like crash-free session rate and anonymous DAU/MAU; we never look at individual sessions.
Each session carries:
| Field | Value | Notes |
|---|---|---|
| Session UUID | A random identifier generated per session | Not linked to any other user identifier; not derived from device IDs |
| Started | UTC timestamp at session start | |
| Ended | UTC timestamp at session end | |
| Status | One of Ok, Exited, Crashed, Abnormal |
Crashed sessions are how crash-free session rate is computed |
| Release / environment | com.barteknowak.etch@VERSION+CODE / beta or production |
Same values as §3.2 |
Sessions carry no new PII fields beyond the device info / app version already documented in §3.2.
Sessions only run while telemetry is on. Etch does not use the SDK's automatic session tracking. It starts and ends sessions itself and only ever starts one while the telemetry toggle is on — so nothing, not even an anonymous session, is recorded or sent before you opt in. Turning the toggle off immediately ends the open session and stops new ones from starting.
4. What's in a state snapshot (feedback only)
When you submit feedback (via the Settings entry, the Supporter feedback row, or by shaking your phone), Etch attaches a one-shot snapshot of your current app state to the event as the etch_state context block. This is only sent with feedback submissions, never with automatic crash reports.
The snapshot contains:
Counts — cards_total, decks_total, cards_due_now, cards_suspended. These are exact, not bucketed, because you self-selected for sharing that granularity by hitting Submit. The plan listed a cards_buried field — we omit it because "buried" is a transient queue state with no persistent counterpart in Etch.
Timings — last_review_at_relative and last_session_at_relative (both rendered as "3h ago" / "2d ago" / "never" — coarse, never an absolute timestamp). last_import_at_relative and last_backup_at_relative are currently reported as "unknown" because those flows don't persist their own timestamps yet.
Configuration — triggers_active as a list (e.g. ["unlock", "timer"]), trigger_timer_minutes if a timer is set, trigger_app_packages (comma-separated list of public Play Store package identifiers for any apps configured as triggers, e.g. "com.reddit.frontpage,com.instagram.android" — package names are not PII), and non_default_settings: a map of preference values that differ from their defaults. Only booleans, enums, and bounded numeric settings are eligible; free-text preference values are excluded by design.
Permissions — overlay, notifications, usage_stats, camera, battery_optimization_exempt, each as true/false.
System — free_storage_bucket (<100MB / 100MB-1GB / 1-10GB / >10GB), available_memory_bucket (<100MB / 100-500MB / >500MB).
Recent errors — crashes_last_7d (count over the last 7 days, maintained in a local on-device ledger) and recoverable_warnings_last_24h (count over the last 24 hours).
App — version_name, version_code, is_beta_build, days_since_install, feedback_entry_point (settings / shake_main / shake_overlay / supporter). supporter means the message came from the Supporter feedback row, which only Supporters see, so it tells us you are a Supporter even when usage data is off.
The snapshot never contains card content, deck names, file paths, search queries, AI prompts, or free-text settings.
5. Feedback submissions
The Send feedback or report a bug flow is accessible from Settings → Privacy, from a shake gesture (which can be disabled in the same section) and, for Supporters, from Settings → Supporter. It bypasses the consent gate: hitting Submit sends data regardless of the telemetry toggle's state, because submitting feedback is itself explicit consent for that one submission.
Before sending, the form shows a confirmation dialog that lists exactly what will be sent. What goes out:
- The free-text description you typed (truncated at 5 000 characters as a safety cap; the form's own char-counter warns you well before that).
- The category you picked (
Bug report,Suggestion,Question, orOther). - The contact email you typed, if you provided one (left blank means no email is attached to the event).
- The screenshot captured by the shake gesture, if one is pending and you left the "Include screenshot" toggle on.
- Any file attachments you explicitly attached (any single attachment over 25 MB is rejected; the total caps at 25 MB).
- The state snapshot described in §4.
- The tags described in §3.2 — without the four purchase tags when usage data is off — and the recent breadcrumbs described in §3.3.
- If you are an Etch Supporter and tick "Credit me as a Supporter," a single bounded flag (
feedback_supporter_credit_optin=true) is attached so we know you'd like to be credited. It is set only when you tick that box, and carries no other information.
The screenshot toggle is on by default when a shake screenshot exists, but you can turn it off in the form before submitting, and the form shows a thumbnail so you can review what would be sent.
6. Beta builds
During the pre-launch beta phase:
- The Beta APK is identifiable by
BuildConfig.IS_BETA_BUILD = trueand by theis_beta_build=yestag on every event. In Sentry it appears underenvironment=beta. - Telemetry is ON by default and cannot be turned off in-app. The Settings → Privacy → Anonymous usage data toggle that lets production users opt out does not appear in a beta build — there is no in-app control to reduce reporting for the beta duration. Beta testers consent to this by taking part in the closed-test program. The app also mentions it on a one-time welcome card on its home screen ("during the beta, anonymous crash & usage reporting stays on").
- The data collected from a beta tester is exactly the same as the data collected from a production user who opted in via the Settings toggle (§3). That includes the anonymous session payloads described in §3.6.
- Shake-to-report is on by default; the same toggle that disables shake in production also disables it in beta.
- The
learn_session_completedevent is sampled 1-in-5 in beta to keep within our Sentry plan. - Beta ends at v1 launch. No new beta APKs are distributed afterwards.
Beta-to-production transition
If you ran a beta build on a device and then update to the production APK, Etch detects the transition on first launch and resets the telemetry toggle to OFF. A one-time card appears on the Home screen explaining the change. If you want to keep helping, re-enable the toggle in Settings → Privacy.
7. Where the data goes
The data described above is sent to Sentry (https://sentry.io), an error-tracking service operated by Functional Software, Inc. We do not run our own server. Once events reach Sentry, they are stored in Sentry's infrastructure and we access them through Sentry's dashboard.
Sentry's own documentation is the authoritative reference for how they handle the data:
- Sentry Privacy Policy: https://sentry.io/legal/privacy/
- Sentry Data Processing Agreement (DPA): https://sentry.io/legal/dpa/
- Sentry security / compliance overview: https://sentry.io/trust/
Sentry is SOC 2 Type II certified and supports GDPR-style data processing arrangements. We have selected Sentry's hosted offering; we are not configured to use a self-hosted Sentry instance.
In-app purchases are handled by a second, separate recipient — Google Play (Google LLC). See §7a.
7a. In-app purchases
Etch offers optional one-time in-app purchases through Google Play Billing. Today these are Etch Pro, Etch Supporter, the Supporter upgrade, and individual premium decks bought from the Deck Library; the same handling applies to any other in-app item Etch adds later. Google Play (Google LLC) is the seller / merchant of record and handles the entire payment — Etch never sees or stores your card number, billing address, order ID, or Google account details.
- What leaves your device. When you open the buy page, tap Buy, tap Restore, or tap Get on a premium deck in the Deck Library, Etch asks Google Play — over the network — which of its products you own and the current prices, and, for a purchase, launches Google Play's own checkout. Because entitlement is also re-checked at launch (see §10), Etch contacts Google Play automatically when the app starts on any device that has Google Play, independent of the telemetry toggle.
- What Etch keeps. Only an on-device cache of which products you own (e.g.
etch_pro, or a deck's product id), used to unlock Pro / Supporter features and purchased decks offline. Never card content, order IDs, or purchase tokens. - What usage data says about purchases. Only when usage data is on (§3) or in a beta build (§6), events carry a summary of that cache — your tier and how many premium decks you own, never which ones (§3.2) — and the purchase steps you take are logged as named events with a product label (§3.4). No order ID, purchase token, or payment detail is ever included. The one exception to "only when usage data is on": feedback sent from the Supporter feedback row is labelled as such whatever the setting, so it shows you are a Supporter (§3.2).
- No Etch server. Entitlement is decided on your device from Google Play's response. Etch runs no server of its own and does no server-side receipt validation.
- Google's handling of the transaction is governed by the Google Play Terms of Service and the Google Privacy Policy.
8. Retention
We defer to Sentry's default retention policy — currently 90 days for events on Sentry's standard plans. If we change the org-level retention setting in the future, we will note it here.
The local on-device ledgers used to compute crashes_last_7d and recoverable_warnings_last_24h keep timestamps for 7 days and 24 hours respectively and never leave the device except as the aggregate counts in a feedback state snapshot.
For in-app purchases, Etch keeps no purchase data on any server of its own — it has none. The only purchase-related data it stores is an on-device cache of which products you own, removed when you uninstall Etch or clear its data. Records of the purchase itself — order history and payment — are held by Google Play under your Google account and governed by Google's retention policies, not Etch's. Refunds are handled through Google Play.
9. Your rights and how to use them
Opt out of telemetry
Settings → Privacy → Anonymous usage data (production builds — see §6 for beta). Toggling this off takes effect immediately: subsequent crashes, ANRs, named events, and recoverable warnings are dropped before they hit the network, and the breadcrumb buffer is cleared so nothing pre-existing rides along on a future feedback submission. In a beta/testing build this control does not appear and telemetry cannot be turned off in-app for the beta duration; the production release, which you get at v1 launch, ships with telemetry off by default.
Disable shake-to-report
Settings → Privacy → Shake to report a bug. With this off, shaking your phone does nothing — no screenshot is captured and the feedback screen does not open.
Request deletion
To request deletion of any data you have submitted (typically via feedback), or to ask what we have for you, email:
feedback@barteknowak.com
Include enough context (approximate date, device model, or any identifier you used in the feedback) so we can find the right event in Sentry. Where possible we will delete by Sentry event ID; where the data overlaps with operational logs we will redact what we can within Sentry's tooling.
Uninstalling
Uninstalling Etch removes all local data — cards, study history, settings, the on-device ledgers, and any pending shake screenshot. It does not delete events that have already been transmitted to Sentry; for those, use the email path above.
10. INTERNET permission
Etch declares the android.permission.INTERNET permission in its manifest. This permission is declared so that the telemetry toggle and feedback flow can work at all — Android requires the declaration up front, even if the app never opens a socket.
When the telemetry toggle is off and you never tap Submit on the feedback form, Etch sends no data to Sentry — with no Sentry events being captured, there is no Sentry network traffic to inspect. This does not cover Google Play Billing: on devices with Google Play, Etch still contacts Google Play at launch (and when you open the buy page or tap Restore) to check which in-app products you own, regardless of the telemetry toggle (see §7a). On a device without Google Play, that check returns "unavailable" and no such call is made.
11. What Etch will NEVER send
This is the hard list. None of the following ever leaves your device through the telemetry or feedback pipelines:
- Card front, back, or hint text.
- Card tags (the user-typed tag strings on a card).
- Deck names.
- Image and audio file contents.
- Image and audio file paths — always scrubbed to
<media-path>before any event leaves the device. - Search queries you type into the in-app search.
- Card editor input (anything you type into front, back, hint, or tag fields).
- AI Assistant prompt text (the topic, URL, or pasted document you feed the prompt builder).
- Custom reminder text or any other free-text settings value.
If you explicitly attach a file in the Send feedback form, that file's contents do leave the device — but only because you picked it from the file picker and confirmed the submission. The form displays a standing warning that picked files may contain personal content.
12. What Etch stores on your device
This part is unchanged from the offline-first design and is included here for completeness. None of these files are transmitted by Etch itself — they live in app-private storage and only leave the device if you use Android's system backup, export them yourself, or attach them to a feedback submission.
- Your flashcards. Text, images, and audio are stored in a local SQLite database (
etch.db), an internal images folder (card_images/), and an internal audio folder (card_audio/) in Etch's private app storage, which Android isolates from other apps. Etch does not add its own encryption layer on top; the files rely on Android's own device storage protections. - Your study history. Every review (card, timestamp, score) is stored locally and used to schedule future reviews.
- Your settings. Including the telemetry and shake-to-report toggles, the post-beta-card dismissal sentinel, the
featuresUsedset used byfeature_first_use, your last-import-format marker, and a locally-cached list of the in-app products you own (your Pro / Supporter entitlement and any premium decks you've bought, so the app knows your tier offline). None of these is sent to an Etch server; the entitlement cache is derived from Google Play (see §7a). - Your Suppressed-history buckets (the History → Suppressed tab) — stored in a separate on-device log, not sent to an Etch server.
- The recoverable-warnings ledger (24 hours) and the crash-count ledger (7 days), used only to populate the feedback state snapshot.
- A pending-shake screenshot bitmap — in-memory only, cleared when the feedback screen closes or the process dies.
Android system backup. Etch leaves Android's built-in Auto Backup enabled. When it is on (it is on by default on most devices), Android copies etch.db, the card_images/ and card_audio/ folders, and your settings — including the list of any apps you've configured as triggers — to your own Google Drive backup, and restores them when you reinstall Etch on a new device. This is a backup in your Google account managed by Android, not a sync to any Etch server (Etch has none). The same data is included in a direct device-to-device transfer. If you would rather none of this leave the device, you can turn off backup in Android Settings → Google → Backup (or Settings → System → Backup, depending on your device). Regenerable caches are excluded.
13. Permissions Etch requests, and why
| Permission | What Etch uses it for |
|---|---|
| Internet | So the telemetry toggle, the feedback Submit button, and Google Play Billing can use the network. With telemetry off and no feedback submitted, Etch sends nothing to Sentry; the Google Play purchase check still runs (see §7a and §10). |
| Notifications | The persistent notification that confirms Etch's background service is running. |
| Boot completed | To restart the background service after you reboot your phone, so your triggers keep working. |
| Foreground service | To keep the trigger-listener service running reliably. |
| Ignore battery optimizations (optional) | So OEM battery / Doze restrictions don't kill the background trigger service. Etch shows Android's one-tap exemption prompt; you can decline or revoke it at any time. No data is read or sent. |
| Display over other apps | To show a pop-up flashcard on top of whatever is on screen when a trigger fires — after you unlock, on your timer, or when you open a watched app. |
| Usage Access (App triggers only) | To detect when you switch to one of the apps you've configured to trigger a flashcard. |
| Phone / call state (optional) | Only if you enable "suppress during call," so a flashcard doesn't appear over an incoming or active call. Etch reads only the ringing / off-hook state — never your phone number or any other call data. |
| Camera | To take a photo for attaching to a card. Optional. |
| Photo / media access (optional) | To let you paste your most recent screenshot onto a card — Etch checks only the newest image in your gallery, and only when you tap Paste. Picking a photo from your gallery uses Android's system photo picker, which needs no permission. |
| In-app purchases (Google Play Billing) | So Google Play can sell, complete, and restore Etch's one-time purchases — Etch Pro, Etch Supporter, the Supporter upgrade, and individual premium decks (see §7a). Granted automatically by the Play Billing library; there is no runtime prompt. |
| Wake lock | Added automatically by Android's WorkManager and the billing / crash libraries so short background tasks can finish. Etch never holds a wake lock itself. |
| Network state | Added automatically by the billing and crash libraries so they can tell whether the device is online before retrying. |
The last three rows — in-app purchases, wake lock, and network state — are not declared by Etch. They merge into the installed app from the libraries listed above, which is why they appear on the Play listing's permission list.
Where to take each one back. These don't all live in one place:
- Runtime permissions — Notifications, Phone / call state, Camera, Photo / media access — are in Android Settings → Apps → Etch → Permissions.
- Display over other apps and Usage access are *special app access* permissions, not runtime ones: they live under Android Settings → Apps → Special app access, and Etch sends you to each system screen itself when you enable the feature that needs it.
- The battery-optimization exemption is its own system dialog, under Settings → Apps → Etch → Battery.
- Internet, Boot completed, Foreground service, and the three library-merged permissions above are install-time permissions and cannot be revoked.
Features that depend on a revoked permission will stop working; the rest of the app will continue normally.
14. Information Etch reads but does not collect
These behaviours predate the telemetry integration and are unchanged:
- Foreground app names. If you enable "App triggers" in Settings, Etch reads the name of the app currently in the foreground only to check whether it matches one of the apps you asked it to watch. The names are read in memory only, are never transmitted, and are never written to disk. They can appear in the in-app diagnostic log if you switch on Developer Options yourself; that log lives in memory, holds the last 500 lines, is cleared when the app process ends, and is never sent anywhere unless you copy it and share it.
- Installed-app list. When you open the App Triggers picker, Etch asks Android for the list of apps with home-screen icons so you can pick which ones should fire a flashcard. The list is shown to you, never sent anywhere.
- Camera frames. Photos you take through the camera go into the app's private image folder. They are not transmitted unless you attach one to a feedback submission.
- Photo gallery. Images you pick via Android's system photo picker reach Etch only because you selected them. Separately, when you tap Paste to attach a recent screenshot, Etch reads only the single newest image in your gallery to offer it — it is not otherwise accessed, stored, or transmitted unless you keep it on the card.
15. Network activity you trigger explicitly
- Export / Import. Tapping Export opens Android's file-save dialog; Etch has no idea where you save the file. Import is the reverse — you pick the file, Etch reads it.
- Privacy policy / Open-source licenses links. Tapping these in Settings opens your browser to the page you're reading now and to a list of open-source licenses. Your browser handles the request, not Etch.
- Telemetry / feedback (covered in §3 and §5).
- In-app purchases. Tapping Buy or Restore on the Etch Pro / Supporter page, or Get on a premium deck in the Deck Library, contacts Google Play to complete or re-check a purchase (covered in §7a).
16. Children's privacy
Etch is not directed at children under 13. We do not knowingly collect any information from children. The app does not have age-gated content, but it also does not offer a child-friendly mode.
17. Changes to this policy
Future updates increment the version number at the top of this document (e.g. v2.1, v3.0) and update the date. The "Last updated" line is always the most recent change date.
If Etch changes in a way that affects this policy — for example, adding a sync feature, changing data processors, or expanding what's collected — we will publish an updated version of this page and, where the change materially affects what we send, surface an in-app notice.
18. Contact
Questions or concerns? Email: feedback@barteknowak.com
Etch is offline-first by design, with one optional, scoped opt-in for crash reporting and feedback. If anything in this document doesn't match what you observe in the app, that's a bug — please report it to the contact address above.