Etch Privacy Policy
v2.5 — Last updated: 2026-07-14
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 the one-time Etch Pro / Supporter in-app purchases (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. Testers are told this before they join the closed-test program, and joining is how they consent to it — 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.0+1 |
App's build config |
environment |
beta or production |
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) |
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 (again/hard/good/easy/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 |
The lockscreen-style overlay appears | card_type |
overlay.dismissed |
The overlay is dismissed | via (answered/swiped/timed_out/cancelled/dismissed) |
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 |
db.migration_ran |
A Room database migration runs | from_version, to_version |
ai_assistant.invoked |
An AI feature is invoked | feature (short identifier such as generate_card) — never your prompt or the model's response |
shake.detected |
A shake gesture is detected | source (main/overlay) |
feedback.opened |
The Send feedback screen opens | entry_point (settings/shake_main/shake_overlay) |
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. Two events are sampled in beta — learn_session_completed at 1-in-5 and search_filtered at 1-in-3 — because their volume would otherwise dominate our quota. 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 filter from the search screen (Etch's analog of "custom preset" — no separate Preset entity exists) | — 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, image_cap; unknown for an unrecognised entry point) |
every fire |
purchase_started |
The Google Play purchase flow is launched for a product | product (bounded label etch_pro / etch_supporter / etch_supporter_upgrade / other — the raw product id is never sent) |
every fire |
purchase_completed |
A purchase is acknowledged / owned | product (bounded label), is_first_purchase (boolean) — never an order ID or purchase token |
every fire |
purchase_failed |
A purchase attempt fails (WARNING level) | product (bounded label), 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 |
3.5 Recoverable warnings (deduped per session)
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. The same category is sent at most once per process to avoid quota burn.
| 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 the app wasn't ready | trigger_type, reason |
fgs_start_failed |
A foreground service failed to start (on app launch or after a device reboot) | service_class |
overlay_show_failed |
An uncaught failure in the overlay service's coroutine scope while attaching a pop-up card | scope (fixed value service) |
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 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).
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 and from a shake gesture (which can be disabled in the same section). 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 100 MB is rejected; the total caps at 100 MB).
- The state snapshot described in §4.
- The tags described in §3.2 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 joining the closed-test program, after being told about it beforehand; that advance disclosure, not an in-app toggle, is the consent basis.
- 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 unlock Etch Pro and Etch Supporter (extra features); the same handling applies to any premium decks or other in-app items Etch offers. 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, or tap Restore, 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), used to unlock Pro / Supporter features offline. Never card content, order IDs, or purchase tokens. - 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 prompts (the text you ask the AI feature to work on) and the model's responses.
- 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, 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 flashcard on top of your home/lock screen when a trigger fires. |
| 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 the one-time Etch Pro / Supporter unlocks (see §7a). Granted automatically by the Play Billing library; there is no runtime prompt. |
You can revoke any of these in Android Settings → Apps → Etch → Permissions at any time. 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 not stored, not logged in release builds, and not transmitted.
- 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 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.