Skip to main content

Telemetry

MailCopilot collects a small amount of anonymous diagnostic and usage data when you enable Send anonymous diagnostics and usage data in Settings → About. This page documents exactly what is collected, and — just as importantly — what is never collected.

What we never collect

Under no circumstances does MailCopilot transmit any of the following:

  • The text of your messages (subject, body, attachments, drafts)
  • Your email addresses or those of your contacts
  • Folder names or paths on your IMAP server
  • File names of attachments
  • The text of your search queries
  • The content of AI chat conversations or AI memory
  • Server hostnames, ports, or credentials

How data is routed

All telemetry is sent to Sentry, our error monitoring and performance platform. When you disable the toggle in Settings, the pipeline is bypassed entirely — nothing is sent. When you enable debug logging, the same events also appear in your local main.log so you can inspect exactly what would be transmitted.

Anonymous install identifier

On first run, MailCopilot generates a random UUID and stores it in the local config file. This UUID never leaves your device. What is transmitted instead is a SHA-256 hash of it — truncated to 16 hex characters — which we call install_id_hash. It is attached to every telemetry event as the Sentry user id so we can answer questions like "how many unique installs are running version X" or "is crash Y affecting 1 user or 100". The hash is:

  • Anonymous — not derived from, or correlated with, any account email, device fingerprint, IP address, or hardware identifier.
  • Stable across releases — the same install keeps the same hash when the app auto-updates, so retention metrics survive version bumps.
  • Not reversible — there is no mapping on our side from the hash back to the UUID or to your device.
  • Dropped when you disable telemetry — flipping the Settings toggle off immediately clears the identifier from the Sentry client and stops all further transmissions.

We use this identifier in the same way a web analytics tool would use an anonymous visitor id: it lets us count distinct installs rather than total events. That difference is the entire reason telemetry is useful — without it, one noisy install would look the same as a hundred calm installs.

Events

App lifecycle

EventKindAggregatedTagsPurpose
app.session_startedeventnoversion, platform, theme, lang, accounts_count, install_id_hashFired once per app start. Carries install_id_hash for DAU/MAU.
app.session_endedhistogramnoreason, install_id_hashFired once on graceful shutdown. value_ms = session duration.
app.updatedeventnofrom_version, to_versionFired once after an auto-update installs a new version.
app.startup_mshistogramnoaccounts_countTime from app.whenReady to the first visible BrowserWindow.

Usage summary

EventKindAggregatedTagsPurpose
usage.session_summaryeventnosearch_used, compose_used, snooze_used, read_later_used, ai_used, rules_used, templates_used, followup_used, install_id_hashEnd-of-session feature-reach bitmap. Which features were used at least once?

Onboarding

EventKindAggregatedTagsPurpose
onboarding.wizard_openedeventnofirst_runUser opened the add-account flow.
onboarding.method_selectedeventnomethodUser picked OAuth vs manual IMAP/SMTP.
onboarding.autoconfig_resulteventnosuccess, providerAutoconfig probe finished — did we find IMAP/SMTP settings?
onboarding.connection_test_resulteventnokind, success, failure_kindIMAP or SMTP connectivity test finished.
onboarding.google_oauth_resulteventnosuccess, failure_kindGoogle OAuth2 flow finished.
onboarding.account_savedeventnoprovider, auth_typeAccount credentials were written to keytar/electron-store.
onboarding.first_headers_sync_completedhistogramnoprovider, folder_count_bucketTime from account_saved to first header sync done (value_ms).
onboarding.first_message_openedeventnotime_since_sync_bucketUser opened their first message after signing in.

Compose

EventKindAggregatedTagsPurpose
compose.openedeventnosource, has_draftCompose window opened; tracks which entry point was used.

Send queue

EventKindAggregatedTagsPurpose
send_queue.enqueuedeventnoscheduled, send_and_archive, has_attachments, body_size_bucketOutgoing message added to send_queue (immediate or scheduled).
send_queue.senthistogramnoscheduledTime from enqueue to successful delivery — SMTP for most accounts, Microsoft Graph for Outlook (value_ms).
send_queue.failedeventnofailure_kindSend attempt failed permanently (queue gave up). Covers both SMTP and Microsoft Graph send paths.
send_queue.retriedeventnoattempt_numberTransient send error — message rescheduled. Covers both SMTP and Microsoft Graph send paths.

Misdirection warnings

EventKindAggregatedTagsPurpose
misdirection.promptedeventnokindCompose showed the misdirection warning dialog.
misdirection.outcomeeventnooutcome, kindUser responded to the misdirection warning.

Templates

EventKindAggregatedTagsPurpose
template.appliedeventnovar_countUser inserted a template into compose.

Follow-up reminders

EventKindAggregatedTagsPurpose
followup.createdeventnoduration_days_bucketFollow-up reminder attached to an outgoing message.
EventKindAggregatedTagsPurpose
search.duration_mshistogramnoscope, folder_role, account_count, sort, pagination, len_bucket, token_count, result_bucket, duration_bucket, zero_resultsEnd-to-end FTS search latency (main-side, pre-remote-merge). Will be replaced by search.completed in PR 2.
search.erroreventnoscope, kindSearch handler threw — either user cancelled or a real failure.

Body indexer

EventKindAggregatedTagsPurpose
body_indexer.tick.duration_mshistogramnoindexed, folders_scannedOne full indexer tick across all folders.
body_indexer.coverage_pctgaugenototal_messages, indexed_messagesFraction of cached messages that have body_text indexed.
body_indexer.backloggaugenoAbsolute number of cached messages still missing body_text.
body_indexer.folder_erroreventnofolder_role, error_streak, backoff_msBody indexer hit a folder-wide error streak and backed off.

Full-text index maintenance

EventKindAggregatedTagsPurpose
fts.optimize.duration_mshistogramnosegments_before, segments_after, reductionFTS5 optimize pass: time and segment count before/after.
fts.optimize.failedeventnoreasonFTS5 optimize threw an error.

Header sync

EventKindAggregatedTagsPurpose
sync.headers.wall_mshistogramnofolder_role, upsert_ms, other_ms, batches, rows, max_batch_msFull syncFolderHeaders run — upsert vs other split for profiling.
sync.headers.coalescedeventnofolder_roleDuplicate syncFolderHeaders attached to an in-flight run.

Mail open instrumentation

EventKindAggregatedTagsPurpose
mail.openhistogramnocache_hit_level, body_size_bucket, attachments_countEnd-to-end mail-open latency as observed from the renderer (open click to details rendered). The cache_hit_level tag encodes which cache tier served the body: memory, db, eml, imap, or imap_timeout.
net.message_details.wall_mshistogramnocache_hit_levelMain-process wall time of the net:messageDetails IPC handler. Isolates the server-side latency from renderer-to-main round-trip noise. One sample per terminal branch (memory, db, eml, imap, imap_timeout).
imap.pool_queue_wait_mseventnorequester, wait_ms_bucketTime spent waiting to acquire an IMAP connection from the per-account pool. Emitted only when the wait exceeds 500 ms, so dashboards capture the long tail without noise from fast acquisitions.

IMAP OAuth token refresh

EventKindAggregatedTagsPurpose
imap.auth_refresh_attempteventnoproviderOAuth token refresh triggered by an IMAP auth failure (XOAUTH2 / AUTHENTICATE).
imap.auth_refresh_successeventnoproviderRefresh succeeded — IMAP retry will use the fresh token.
imap.auth_refresh_failureeventnoprovider, reasonRefresh failed — the original auth error will surface to the caller.
imap.auth_refresh_suppressedeventnoreasonPer-account cooldown suppressed a refresh attempt to prevent /token request storms when a refresh token has been revoked.
imap.idle_auth_refreshedeventnoproviderIDLE loop recovered from a mid-cycle auth failure via in-loop refresh — push delivery resumed without the 60-min auth backoff.
imap.auth_refresh_exhaustedeventnoprovider, consecutiveIDLE loop tripped the storm-brake — N consecutive refreshes succeeded at the provider but IMAP kept rejecting the fresh tokens, so we fell back to ordinary auth backoff.

Cache retention

EventKindAggregatedTagsPurpose
cache.eml_prunedeventnocount_bucket, freed_bytes_bucketBody retention sweep deleted .eml files older than the configured cutoff. Counts and sizes are bucketed — no exact file paths or counts are transmitted.
cache.folder_index_disabledeventnocount, roleA folder was excluded from full-text search — either automatically for Junk/Spam/Trash on first registration, or manually via the folder context menu. role is spam, trash, or manual.

Cache safety and data-loss signals

EventKindAggregatedTagsPurpose
db.mass_delete_messageseventnofolder_role, reason, deleted_count_bucket, watermark_preservedFolder-wide DELETE FROM messages emitted. Every call site provides a reason so a regression that wipes healthy caches is distinguishable from a legitimate UIDVALIDITY bump.
imap.stale_wipe_guard_trippedeventnofolder_role, providerThe mass-delete guard refused to purge the local folder cache because mailbox.exists came back non-numeric. A spike here points to a provider/connection issue, not user data loss.
db.shutdown_wal_checkpoint_mshistogramnobusy, reclaimed_kb_bucket, okWall-clock duration of the PRAGMA wal_checkpoint(TRUNCATE) we run before quit so committed-but-not-checkpointed writes survive across sessions.

MCP stdio gate (renderer-to-RCE protection)

EventKindAggregatedTagsPurpose
mcp.stdio.connect_attemptedeventnoapproved_sourceStdio MCP transport was about to be spawned — fires once per successful connect after the approval and allowlist gates passed.
mcp.stdio.connect_blockedeventnoreasonStdio connect or save refused by the gate (not_approved, unapproved_command, forbidden_field, forbidden_env_key, env_disabled).
mcp.stdio.approval_grantedeventnosource, scopeUser granted stdio MCP approval (global enable or per-connection); source distinguishes env vs native-confirm, scope distinguishes global vs per-connection.
mcp.stdio.env_sanitized_on_loadeventnocount_bucketSettings migration stripped forbidden loader-hook env keys from persisted MCP connections on load. Fires at most once per launch.

AI action audit (preview → apply confirmation barrier)

EventKindAggregatedTagsPurpose
ai.action.preview_createdeventnokindA *_preview MCP tool registered a pending mutating action awaiting user click on Apply.
ai.action.appliedeventnokindAn *_apply MCP tool successfully executed a previously-confirmed mutating action.
ai.action.rejectedeventnokind, reasonAn *_apply call was rejected at the validation gate (preview missing/expired, token mismatch, kind mismatch, callback missing, or rate limit).
ai.action.expiredeventnokindA pending mutating action expired without the user ever clicking Apply (TTL).
ai.action.apply_duration_mshistogramnokindWall-clock duration of a successful apply — how long the underlying DB / IMAP / SMTP mutation took.

AI outbound egress gate

EventKindAggregatedTagsPurpose
ai.egress.blockedeventnotool_name, account_idAn outbound egress tool call (e.g. WebSearch, WebFetch, generic external MCP tool) was refused while user email data was in scope — either filtered out of the SDK toolset or stopped at the runtime guard.
ai.egress.allowed_onceeventnotool_name, account_idThe user granted a one-shot egress consent and the AI exercised it. Distinguishes "users routinely override" from "the gate holds, attempts are mostly injection-driven".

IPC performance

EventKindAggregatedTagsPurpose
ipc.slow_mshistogramyes (10s window)channel, duration_bucketIPC handler took longer than the slow threshold.

UI responsiveness

EventKindAggregatedTagsPurpose
ui.freeze.renderer_mshistogramyes (10s window)duration_bucket, inflight_count, top_inflightRenderer event loop was blocked longer than the freeze threshold.
ui.freeze.main_mshistogramyes (10s window)duration_bucket, inflight_count, top_inflightMain process event loop was blocked (perf_hooks delay).

Contact

Questions or concerns about what we collect? Open an issue at github.com/mailcopilot/mailcopilot or contact the team directly through the feedback form in Settings → About.