How Self-Host Telemetry Works in OpenSEO Deployments
OpenSEO's self-host telemetry system transmits anonymous heartbeat events containing aggregate usage counts and health-check statuses from Docker or Cloudflare deployments to PostHog, while honoring opt-out environment variables and implementing intelligent throttling to minimize database overhead.
The every-app/open-seo repository includes a privacy-conscious telemetry implementation designed specifically for self-hosted instances. This system helps maintainers understand deployment patterns and diagnose widespread issues without collecting personally identifiable information or impacting application performance.
Telemetry Control and Privacy Safeguards
The telemetry system first determines whether it should activate by checking deployment context and user preferences.
Environment Variable Opt-Outs
In src/shared/selfhost-checks.ts, the isTelemetryOptOutValue helper evaluates two environment variables to respect user privacy:
OPENSEO_TELEMETRY_DISABLEDDO_NOT_TRACK
Setting either variable to 1 completely disables the telemetry pipeline. The telemetryIsDisabled() function in src/server/lib/self-host-telemetry.ts combines these checks with a hosted-mode detection (isHostedServerAuthMode) to ensure telemetry only runs on self-hosted installations, never on the managed cloud service.
Production Build Guards
The system also verifies isNonProductionBuild to prevent telemetry transmission from development or preview builds, ensuring that only production deployments contribute to usage statistics.
Heartbeat Scheduling and State Persistence
OpenSEO implements a sophisticated throttling mechanism to avoid database pressure while maintaining accurate installation tracking.
The telemetry_state Table
On first run, the system creates a single row in the telemetry_state table defined in src/db/telemetry.schema.ts (SQLite) or src/db/pg/telemetry.schema.ts (Postgres). This record stores:
- A random
installId(UUID) that serves as the anonymous distinct identifier lastHeartbeatAttimestamp for cadence enforcement- Version information and first-run flags
- An MCP tool-call counter
Cadence and Throttling Logic
The claimHeartbeat() function enforces a tiered reporting schedule:
- Initial burst: Every 5 minutes for the first 2 hours after installation
- Steady state: Daily thereafter
To minimize database queries, the implementation uses in-memory variables lastCheckedAt and cachedInstalledAt as a lightweight memory throttle, preventing table lookups on every request.
Data Collection and PostHog Integration
When a heartbeat triggers, the system aggregates anonymous metrics and transmits them via the PostHog SDK.
Aggregate Count Collection
The collectCounts() function queries core application tables—including user, projects, and audits—to generate aggregate usage statistics. These counts reveal feature adoption trends without exposing individual user data.
Setup Issue Reporting
The collectSetupIssues() function invokes getSetupIssueSummary to compile a compact list of failing health checks. This helps the development team identify common configuration problems across the self-host community.
Heartbeat Payload Structure
The sendHeartbeat() function constructs a HeartbeatProperties object containing:
- Deployment target:
dockerorcloudflare - Database backend:
d1orpostgres - Application version and first-run flag
- MCP tool-call count (incremented via
incrementSelfHostMcpToolCallCount()) - Setup issue summaries
The event posts to PostHog with the installId UUID as the distinct ID, ensuring anonymous but consistent session tracking per installation.
Request Lifecycle Integration
The telemetry system hooks into the main request handler in src/server.ts (line 41). Each incoming request invokes maybeSendSelfHostHeartbeat() wrapped in ctx.waitUntil, allowing the heartbeat logic to execute asynchronously without delaying the HTTP response. This pattern ensures telemetry collection adds zero latency to user-facing operations.
Configuring and Debugging Telemetry
Disabling Telemetry
Add one of the following to your .env or .env.selfhost file:
# Option 1: OpenSEO-specific flag
OPENSEO_TELEMETRY_DISABLED=1
# Option 2: Universal do-not-track standard
DO_NOT_TRACK=1
Restart your containers or redeploy the Cloudflare Worker to apply the changes.
Manual Heartbeat Triggering
For debugging purposes, you can manually trigger a heartbeat:
import { maybeSendSelfHostHeartbeat } from "@/server/lib/self-host-telemetry";
await maybeSendSelfHostHeartbeat();
Incrementing MCP Tool-Call Counters
When building MCP client integrations, use the provided helper to track tool usage:
import { incrementSelfHostMcpToolCallCount } from "@/server/lib/self-host-telemetry";
await incrementSelfHostMcpToolCallCount();
Summary
- Opt-out by default: Set
OPENSEO_TELEMETRY_DISABLED=1orDO_NOT_TRACK=1to disable transmission entirely - Anonymous identifiers: Each installation receives a random UUID stored in the
telemetry_statetable, never linked to user accounts - Adaptive cadence: Heartbeats send every 5 minutes initially, then daily after the first 2 hours
- Zero-latency design: Asynchronous execution via
ctx.waitUntilensures telemetry never blocks requests - Rich metadata: Payloads include deployment target (Docker/Cloudflare), database type (D1/Postgres), aggregate counts, and health-check summaries
Frequently Asked Questions
What data does OpenSEO's self-host telemetry collect?
The system collects only anonymous aggregate counts—such as the number of users, projects, and audits—and health-check failure summaries. It explicitly excludes personally identifiable information, content data, or individual user actions. According to the source code in src/server/lib/self-host-telemetry.ts, the collectCounts() function runs aggregation queries that return totals rather than individual records.
How do I completely disable telemetry in my Docker deployment?
Set the environment variable OPENSEO_TELEMETRY_DISABLED=1 in your .env file and restart the containers. Alternatively, use the universal DO_NOT_TRACK=1 standard. The isTelemetryOptOutValue helper in src/shared/selfhost-checks.ts evaluates both variables, and the telemetryIsDisabled() function ensures no data transmits when either is present.
Why does telemetry send frequently at first but then slow down?
The claimHeartbeat() function implements a graduated cadence: every 5 minutes for the first 2 hours to capture initial setup patterns, then daily thereafter. This design, found in src/server/lib/self-host-telemetry.ts, provides immediate feedback on new installations while minimizing long-term database load and network traffic for stable deployments.
Can telemetry run in development or preview builds?
No. The maybeSendSelfHostHeartbeat() function checks isNonProductionBuild and exits early if the code runs outside a production environment. This prevents test data from polluting the analytics, as implemented in the telemetry control logic of src/server/lib/self-host-telemetry.ts.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →