fix: make telemetry opt-in

This commit is contained in:
Linus Rath
2026-06-14 14:31:26 +02:00
parent 8df483d8c7
commit 3e12fca517
5 changed files with 75 additions and 21 deletions
+8 -4
View File
@@ -110,12 +110,16 @@ JMAP_SERVER_URL=https://your-jmap-server.com
# Anonymous Telemetry # Anonymous Telemetry
# ============================================================================= # =============================================================================
# Anonymous instance telemetry is enabled by default. Heartbeats contain no PII: # Anonymous instance telemetry is OPT-IN and disabled by default. Enabling it
# version, platform, bucketed account counts, and feature toggles only. See # helps us understand how Bulwark is used so we can make the product better.
# Heartbeats contain no PII: version, platform, bucketed account counts, and
# feature toggles only - never email addresses, hostnames, or IPs. See
# https://bulwarkmail.org/docs/legal/privacy/telemetry for the full schema. # https://bulwarkmail.org/docs/legal/privacy/telemetry for the full schema.
# #
# Disable telemetry entirely (overrides the admin UI): # Enable telemetry (also toggleable in the admin UI):
# BULWARK_TELEMETRY=off # BULWARK_TELEMETRY=on
#
# Setting this (on or off) locks the choice and disables the admin UI toggle.
# Directory for telemetry state: instance id, consent, login HMACs # Directory for telemetry state: instance id, consent, login HMACs
# (default: ./data/telemetry). For Docker, the default resolves to # (default: ./data/telemetry). For Docker, the default resolves to
+1 -1
View File
@@ -136,7 +136,7 @@ Automatic browser detection with persistent preference. Configurable locale URL
- Progressive Web App with service worker, install prompt, web push notifications for inbox mail, dynamic manifest, and configurable (per-domain) install screenshots - Progressive Web App with service worker, install prompt, web push notifications for inbox mail, dynamic manifest, and configurable (per-domain) install screenshots
- Automatic update check with server-side logging of new releases and a non-dismissible update notice - Automatic update check with server-side logging of new releases and a non-dismissible update notice
- Structured logging (`text` or `json`) with category-based levels - Structured logging (`text` or `json`) with category-based levels
- Anonymous instance telemetry (opt-out via admin UI or `BULWARK_TELEMETRY=off`) version, platform, bucketed account counts, feature toggles only - Anonymous instance telemetry (opt-in via admin UI, the installer, or `BULWARK_TELEMETRY=on`; off by default) version, platform, bucketed account counts, feature toggles only
- Release (`main`) and development (`dev`) Docker images on GHCR - Release (`main`) and development (`dev`) Docker images on GHCR
- Subpath deployment via `NEXT_PUBLIC_BASE_PATH` for mounting behind a reverse proxy - Subpath deployment via `NEXT_PUBLIC_BASE_PATH` for mounting behind a reverse proxy
- Demo mode with fixture data no mail server required - Demo mode with fixture data no mail server required
+6 -5
View File
@@ -118,9 +118,10 @@ export function TelemetryTab() {
<header className="space-y-2"> <header className="space-y-2">
<h1 className="text-2xl font-semibold">Anonymous Usage Stats</h1> <h1 className="text-2xl font-semibold">Anonymous Usage Stats</h1>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Bulwark sends one anonymous heartbeat per day so we can see how many instances are Bulwark can send one anonymous heartbeat per day so we can see how many instances are
running, on what platforms, and which features they use. <strong>Enabled by default</strong>; running, on what platforms, and which features they use. It&apos;s <strong>off by
one click below disables it. No email addresses, no hostnames, no IPs are sent.{' '} default</strong>; one click below enables it and helps us make the product better. No
email addresses, no hostnames, no IPs are sent.{' '}
<a <a
href="https://bulwarkmail.org/docs/legal/privacy/telemetry" href="https://bulwarkmail.org/docs/legal/privacy/telemetry"
target="_blank" target="_blank"
@@ -138,8 +139,8 @@ export function TelemetryTab() {
<div className="font-medium">Status</div> <div className="font-medium">Status</div>
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{status.consent === 'pending' && 'Initialising - no heartbeats sent yet.'} {status.consent === 'pending' && 'Initialising - no heartbeats sent yet.'}
{status.consent === 'on' && 'Heartbeats are enabled (default).'} {status.consent === 'on' && 'Heartbeats are enabled. Thanks for helping us improve!'}
{status.consent === 'off' && 'Heartbeats are off.'} {status.consent === 'off' && 'Heartbeats are off (default).'}
{envOverridden && ( {envOverridden && (
<> Locked by <code>BULWARK_TELEMETRY</code> env var.</> <> Locked by <code>BULWARK_TELEMETRY</code> env var.</>
)} )}
+11 -11
View File
@@ -17,6 +17,7 @@ function idPath(): string { return path.join(getDir(), '.telemetry-id'); }
function envOverride(): ConsentState | null { function envOverride(): ConsentState | null {
const v = (process.env.BULWARK_TELEMETRY ?? '').toLowerCase(); const v = (process.env.BULWARK_TELEMETRY ?? '').toLowerCase();
if (v === 'off' || v === 'false' || v === '0' || v === 'no') return 'off'; if (v === 'off' || v === 'false' || v === '0' || v === 'no') return 'off';
if (v === 'on' || v === 'true' || v === '1' || v === 'yes') return 'on';
if (process.env.BULWARK_TELEMETRY_DISABLED) { if (process.env.BULWARK_TELEMETRY_DISABLED) {
const d = process.env.BULWARK_TELEMETRY_DISABLED.toLowerCase(); const d = process.env.BULWARK_TELEMETRY_DISABLED.toLowerCase();
if (d === '1' || d === 'true' || d === 'yes') return 'off'; if (d === '1' || d === 'true' || d === 'yes') return 'off';
@@ -41,11 +42,13 @@ export async function getInstanceId(): Promise<string> {
return fresh; return fresh;
} }
// Default consent is 'on' - telemetry is anonymous and enabled by default. // Default consent is 'off' - telemetry is opt-in. Admins can enable it during
// Admins can disable via the UI, the BULWARK_TELEMETRY env var, or by clearing // install (BULWARK_TELEMETRY=on in .env.local), via the BULWARK_TELEMETRY env
// the endpoint. See https://bulwarkmail.org/docs/legal/privacy/telemetry. // var, or with one click in the admin UI. Heartbeats are anonymous: no PII,
// just version/platform/feature toggles. Enabling helps us improve the product.
// See https://bulwarkmail.org/docs/legal/privacy/telemetry.
const DEFAULTS: TelemetryStateFile = { const DEFAULTS: TelemetryStateFile = {
consent: 'on', consent: 'off',
endpoint: DEFAULT_ENDPOINT, endpoint: DEFAULT_ENDPOINT,
consentedAt: null, consentedAt: null,
lastSentAt: null, lastSentAt: null,
@@ -64,13 +67,10 @@ export async function loadState(): Promise<TelemetryStateFile> {
error: err instanceof Error ? err.message : String(err), error: err instanceof Error ? err.message : String(err),
}); });
} }
// First-ever load on a fresh install: persist the default-on state with // First-ever load on a fresh install: persist the default-off state so the
// an autoEnabledAt stamp so the admin UI can show "telemetry was // instance id and (lack of) consent are stable across restarts. The admin
// auto-enabled at <time>; disable here" without re-arming on restart. // can opt in later via the UI or the BULWARK_TELEMETRY env var.
const fresh: TelemetryStateFile = { const fresh: TelemetryStateFile = { ...DEFAULTS };
...DEFAULTS,
consentedAt: new Date().toISOString(),
};
await saveState(fresh); await saveState(fresh);
return fresh; return fresh;
} }
+49
View File
@@ -63,6 +63,7 @@ CFG_OAUTH_ISSUER_URL=""
CFG_SESSION_SECRET="" CFG_SESSION_SECRET=""
CFG_SETTINGS_SYNC_ENABLED="false" CFG_SETTINGS_SYNC_ENABLED="false"
CFG_SETTINGS_DATA_DIR="./data/settings" CFG_SETTINGS_DATA_DIR="./data/settings"
CFG_TELEMETRY="false"
CFG_LOG_FORMAT="text" CFG_LOG_FORMAT="text"
CFG_LOG_LEVEL="info" CFG_LOG_LEVEL="info"
CFG_APP_SHORT_NAME="" CFG_APP_SHORT_NAME=""
@@ -733,6 +734,30 @@ screen_security_config() {
echo -e " ${DIM}You can add a SESSION_SECRET to .env.local at any time.${RESET}" echo -e " ${DIM}You can add a SESSION_SECRET to .env.local at any time.${RESET}"
fi fi
echo ""
hr
echo ""
echo -e " ${BOLD}Anonymous Usage Stats${RESET}"
echo ""
echo -e " Bulwark can send one anonymous heartbeat per day. It helps us see how"
echo -e " many instances run, on what platforms, and which features are enabled"
echo -e " so we can ${BOLD}make the product better${RESET}."
echo ""
echo -e " ${STAR} ${BOLD}No private data${RESET} - no email addresses, hostnames, or IPs"
echo -e " ${STAR} Just version, platform, and which features are turned on"
echo -e " ${STAR} ${BOLD}Off by default${RESET} - you can change it any time in the admin UI"
echo ""
prompt_yesno "Enable anonymous telemetry to help improve Bulwark?" "$CFG_TELEMETRY" "CFG_TELEMETRY"
if [[ "$CFG_TELEMETRY" == "true" ]]; then
echo ""
echo -e " ${OK} ${GREEN}Thanks! Telemetry will be enabled. We appreciate it.${RESET}"
else
echo ""
note "Telemetry stays off. No heartbeats will be sent."
fi
draw_footer draw_footer
read -r read -r
} }
@@ -948,6 +973,11 @@ screen_summary() {
else else
echo -e " Session Secret ........ ${DIM}Not set${RESET}" echo -e " Session Secret ........ ${DIM}Not set${RESET}"
fi fi
if [[ "$CFG_TELEMETRY" == "true" ]]; then
echo -e " Anonymous Telemetry ... ${GREEN}${BOLD}Enabled${RESET} ${DIM}(thank you!)${RESET}"
else
echo -e " Anonymous Telemetry ... ${DIM}Off${RESET}"
fi
echo "" echo ""
# Logging # Logging
@@ -1093,6 +1123,20 @@ ENVEOF
cat >> "$ENV_FILE" << ENVEOF cat >> "$ENV_FILE" << ENVEOF
# -- Anonymous Telemetry -------------------------------------------------------
# Opt-in, anonymous heartbeats (no PII). Off by default; helps improve Bulwark.
# Toggleable later in the admin UI unless this is set. See
# https://bulwarkmail.org/docs/legal/privacy/telemetry
ENVEOF
if [[ "$CFG_TELEMETRY" == "true" ]]; then
echo "BULWARK_TELEMETRY=on" >> "$ENV_FILE"
else
echo "# BULWARK_TELEMETRY=on" >> "$ENV_FILE"
fi
cat >> "$ENV_FILE" << ENVEOF
# -- Logging ------------------------------------------------------------------- # -- Logging -------------------------------------------------------------------
LOG_FORMAT=${CFG_LOG_FORMAT} LOG_FORMAT=${CFG_LOG_FORMAT}
LOG_LEVEL=${CFG_LOG_LEVEL} LOG_LEVEL=${CFG_LOG_LEVEL}
@@ -1340,6 +1384,11 @@ load_existing_config() {
get_env_val "SESSION_SECRET"; [[ -n "$val" ]] && CFG_SESSION_SECRET="$val" get_env_val "SESSION_SECRET"; [[ -n "$val" ]] && CFG_SESSION_SECRET="$val"
get_env_val "SETTINGS_SYNC_ENABLED"; [[ -n "$val" ]] && CFG_SETTINGS_SYNC_ENABLED="$val" get_env_val "SETTINGS_SYNC_ENABLED"; [[ -n "$val" ]] && CFG_SETTINGS_SYNC_ENABLED="$val"
get_env_val "SETTINGS_DATA_DIR"; [[ -n "$val" ]] && CFG_SETTINGS_DATA_DIR="$val" get_env_val "SETTINGS_DATA_DIR"; [[ -n "$val" ]] && CFG_SETTINGS_DATA_DIR="$val"
get_env_val "BULWARK_TELEMETRY"
case "$(echo "$val" | tr '[:upper:]' '[:lower:]')" in
on|true|1|yes) CFG_TELEMETRY="true" ;;
off|false|0|no) CFG_TELEMETRY="false" ;;
esac
get_env_val "LOG_FORMAT"; [[ -n "$val" ]] && CFG_LOG_FORMAT="$val" get_env_val "LOG_FORMAT"; [[ -n "$val" ]] && CFG_LOG_FORMAT="$val"
get_env_val "LOG_LEVEL"; [[ -n "$val" ]] && CFG_LOG_LEVEL="$val" get_env_val "LOG_LEVEL"; [[ -n "$val" ]] && CFG_LOG_LEVEL="$val"
get_env_val "LOGIN_COMPANY_NAME"; [[ -n "$val" ]] && CFG_LOGIN_COMPANY_NAME="$val" get_env_val "LOGIN_COMPANY_NAME"; [[ -n "$val" ]] && CFG_LOGIN_COMPANY_NAME="$val"