Etch Privacy Policy

v2.3 — Last updated: 2026-05-23

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

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.

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 — see §6 for details. Beta is also the only context in which the closed-test program presents the telemetry opt-in. 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:

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

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)
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)
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. &quot;com.reddit.frontpage,com.instagram.android&quot; — 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, service_down, no_due_cards)
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
audio.played Audio playback starts on a card format (mp3/ogg/wav/m4a/opus/unknown)
audio.failed Audio playback fails format, phase (decode/io/unknown)
image.load_failed Coil reports a failed image load after retries path_kind (filesDir/cacheDir/external/http/unknown), attempt_count_bucketnever 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
backup.restored A backup is restored card_count_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_valueonly 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)

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. One event — learn_session_completed — is sampled at 1-in-5 in beta, because completed-session volume would otherwise dominate our quota.

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)
backup_restored A restore finishes card_count_bucket, source (file/cloud) every fire
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

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 service_class

The plan that drove this work listed nine recoverable categories; five are wired today. The other four (review_save_retry, migration_warning, overlay_bal_denied, fsrs_unexpected_state) are not currently emitted from any production code path.

3.6 Session lifecycle

When telemetry is enabled, Sentry's auto session tracking is on. A "session" here is not a learn session — it's the SDK's term for a continuous foreground interval. The SDK records one when the app comes to the foreground and ends it after the app has been in the background for at least 30 seconds. 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.

Gating limitation. Sentry's v7.22.6 SDK does not expose a per-event hook for sessions (it exposes one for crashes, transactions, and replays — but not for sessions). When you flip the telemetry toggle off, Etch immediately ends the currently-open session. After that, the SDK's lifecycle observer will still start fresh anonymous sessions on foreground events until the process is killed; the next process launch reads the toggle and skips initialisation entirely. This residual signal is acceptable because session payloads are anonymous and carry no card content, file paths, or settings.

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:

Countscards_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.

Timingslast_review_at_relative and last_session_at_relative (both rendered as &quot;3h ago&quot; / &quot;2d ago&quot; / &quot;never&quot; — coarse, never an absolute timestamp). last_import_at_relative and last_backup_at_relative are currently reported as &quot;unknown&quot; because those flows don't persist their own timestamps yet.

Configurationtriggers_active as a list (e.g. [&quot;unlock&quot;, &quot;timer&quot;]), 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. &quot;com.reddit.frontpage,com.instagram.android&quot; — 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.

Permissionsoverlay, notifications, usage_stats, camera, battery_optimization_exempt, each as true/false.

Systemfree_storage_bucket (<100MB / 100MB-1GB / 1-10GB / >10GB), available_memory_bucket (<100MB / 100-500MB / >500MB).

Recent errorscrashes_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).

Appversion_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 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:

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 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.

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.

9. Your rights and how to use them

Opt out of telemetry

Settings → Privacy → Anonymous usage data. 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.

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 makes zero outbound network calls. This is enforceable at the OS level: with no Sentry events being captured, there is no network traffic to inspect.

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:

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.

13. Permissions Etch requests, and why

Permission What Etch uses it for
Internet So the telemetry toggle and the feedback Submit button can reach Sentry. Zero network calls occur when the toggle is off and you never submit feedback.
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.
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.
Camera To take a photo for attaching to a card. Optional.
Photo / media access To pick an existing photo for a card. Optional.

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:

15. Network activity you trigger explicitly

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.