Skip to content

Commit 12492d9

Browse files
authored
feat: anonymous opt-in telemetry module (#91)
1 parent e40b23a commit 12492d9

23 files changed

Lines changed: 884 additions & 1 deletion

.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,7 @@ LIVEMODE=
6666
# hosting, POST /v1/billing/run with x-operator-key via cron / Cloud Scheduler.
6767
# BILLING_MONITOR_ENABLED=false
6868
# BILLING_POLL_INTERVAL_MS=60000
69+
70+
# Anonymous usage telemetry (opt-in via setup/settings; off by default).
71+
# Set to 0 to force disable.
72+
# ZONELESS_TELEMETRY=

.env.production.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,8 @@ LIVEMODE=true
5959
# Trigger POST /v1/billing/run from Cloud Scheduler with x-operator-key,
6060
# or POST /v1/billing/run_for_platform with a platform API key.
6161
# BILLING_MONITOR_ENABLED=false
62-
# BILLING_POLL_INTERVAL_MS=60000
62+
# BILLING_POLL_INTERVAL_MS=60000
63+
64+
# Anonymous usage telemetry (opt-in via setup/settings; off by default).
65+
# Set to 0 to force disable.
66+
# ZONELESS_TELEMETRY=

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,11 @@ zoneless/
172172

173173
See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup, style guidelines, and the pull request process.
174174

175+
## Anonymous usage telemetry
176+
177+
Self-hosted Zoneless can optionally share anonymous usage heartbeats with
178+
maintainers (opt-in, off by default). See [TELEMETRY.md](./TELEMETRY.md).
179+
175180
## Security
176181

177182
See [SECURITY.md](./SECURITY.md) to report vulnerabilities.

SECURITY.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ Key points:
4949
- Never commit `.env` files to version control
5050
- Rotate API keys periodically
5151

52+
### Anonymous usage telemetry
53+
54+
Opt-in anonymous usage heartbeats may be sent to zoneless.com (off by default).
55+
Force disable with `ZONELESS_TELEMETRY=0`. See [TELEMETRY.md](./TELEMETRY.md).
56+
5257
## Acknowledgments
5358

5459
We would like to thank the following individuals for responsibly disclosing security issues.

TELEMETRY.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Anonymous Usage Telemetry
2+
3+
Zoneless can optionally send **anonymous, aggregate usage heartbeats** to
4+
[zoneless.com](https://zoneless.com). Telemetry is **opt-in and off by default**,
5+
and only applies to **self-hosted** instances (not operator-managed hosting).
6+
7+
## Consent
8+
9+
Enable via the checkbox during setup or under **Settings → Anonymous usage**,
10+
or `POST /v1/telemetry` with `{ "enabled": true }` as a platform user.
11+
12+
Disable the same way, or set `ZONELESS_TELEMETRY=0`.
13+
14+
## What is sent
15+
16+
One HTTPS POST roughly once per day when enabled **and** `LIVEMODE=true`.
17+
Test-mode instances (`LIVEMODE` unset/false) never send; consent can still be
18+
saved and will take effect after switching to live mode.
19+
20+
| Field | Description |
21+
| ----------------------- | ----------------------------------------------------------------------------------------------------- |
22+
| `instance_id` | Random UUID generated at opt-in |
23+
| `zoneless_version` | Package version string |
24+
| `livemode` | Always `true` when a report is sent (test mode never reports) |
25+
| `single_tenant` | Whether single-tenant mode is on |
26+
| `setup_completed` | Whether at least one platform account exists |
27+
| `os` | `process.platform` (e.g. `linux`) |
28+
| `node_major` | Major Node.js version number |
29+
| `payment_count_7d` | Bucket: `0`, `1-10`, `11-100`, `101-1000`, `1000+` (payment/charge BTs) |
30+
| `usdc_volume_7d` | Payment/charge volume bucket: `0`, `lt_10`, `lt_100`, `lt_1k`, `lt_10k`, `lt_100k`, `lt_1m`, `gte_1m` |
31+
| `usdc_payout_volume_7d` | Payout volume bucket (same bands; absolute amount) |
32+
| `connected_accounts` | Bucket: `0`, `1`, `2-10`, `11-100`, `100+` |
33+
34+
## What is never sent
35+
36+
Emails, business names, domains, API keys, wallet addresses, customer PII,
37+
exact dollar amounts, request bodies, logs, or IP addresses.

apps/api/src/main.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import {
2626
import { db } from './modules/Database';
2727
import { GetTopUpMonitor, TopUpMonitor } from './modules/TopUpMonitor';
2828
import { GetBillingMonitor, BillingMonitor } from './modules/BillingMonitor';
29+
import {
30+
GetTelemetryMonitor,
31+
TelemetryMonitor,
32+
} from './modules/TelemetryMonitor';
2933
import { AccountModule } from './modules/Account';
3034
import { ExternalWalletModule } from './modules/ExternalWallet';
3135

@@ -207,6 +211,11 @@ async function StartServer() {
207211
);
208212
}
209213

214+
// Daily anonymous usage heartbeats (no-ops unless opted in)
215+
if (TelemetryMonitor.ShouldStart()) {
216+
GetTelemetryMonitor(db).Start();
217+
}
218+
210219
const server = app.listen(port, () => {
211220
console.log(`🚀 API running at http://localhost:${port}/v1`);
212221
console.log(`📊 Health check at http://localhost:${port}/api/health`);
@@ -241,6 +250,11 @@ async function StartServer() {
241250
billingMonitor.Stop();
242251
}
243252

253+
if (TelemetryMonitor.ShouldStart()) {
254+
const telemetryMonitor = GetTelemetryMonitor(db);
255+
telemetryMonitor.Stop();
256+
}
257+
244258
server.close(async () => {
245259
await mongoose.connection.close();
246260
console.log('Server closed');

apps/api/src/modules/Database.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,7 @@ export class Database {
449449
'TopUps',
450450
'Transfers',
451451
'UsageCounters',
452+
'TelemetryConfigs',
452453
'WebhookEndpoints',
453454
];
454455

apps/api/src/modules/Setup.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ import { ExternalWalletModule } from './ExternalWallet';
1616
import { SignToken } from '../utils/Token';
1717
import { SetupRequest, SetupResponse } from '@zoneless/shared-types';
1818
import { GetJwtSecret } from './AppConfig';
19+
import { GetTelemetryModule } from './Telemetry';
1920
import { AppError } from '../utils/AppError';
2021
import { ERRORS } from '../utils/Errors';
22+
import { Logger } from '../utils/Logger';
2123

2224
/**
2325
* Validates a setup request body.
@@ -159,6 +161,17 @@ export class SetupModule {
159161
'7d'
160162
);
161163

164+
// Persist telemetry consent from setup (default off)
165+
if (request.telemetry_enabled === true) {
166+
try {
167+
await GetTelemetryModule(this.db).SetEnabled(true);
168+
} catch (err) {
169+
Logger.warn('Failed to enable telemetry during setup', {
170+
error: err instanceof Error ? err.message : String(err),
171+
});
172+
}
173+
}
174+
162175
return {
163176
object: 'setup_response',
164177
success: true,

0 commit comments

Comments
 (0)