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
- Etch is local-first by design. Your cards, decks, and study history live in private storage on your device and are not synced to any server.
- There are no ads, no third-party advertising SDKs, no Firebase, no analytics SDKs, and no user accounts.
- The only network-touching feature is 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.
- 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.
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:
- 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 |
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. "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, 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_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 |
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_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) |
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:
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 2 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.
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. Beta testers consented to this when joining the closed-test program.
- 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.
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:
- 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 and images are stored in an encrypted-at-rest SQLite database (
etch.db) and an internal images folder (card_images/). - 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, and your last-import-format marker. - 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.
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:
- 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.
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).
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.