How the Open SEO Self-Host Telemetry Heartbeat Works: Function and Data Reported
The Open SEO self-host telemetry heartbeat periodically reports install-level usage metrics to PostHog via the maybeSendSelfHostHeartbeat function in src/server/lib/self-host-telemetry.ts, capturing entity counts, setup health, and environment metadata while respecting strict throttling and opt-out controls.
The self-host telemetry system in Open SEO provides production installs with a privacy-conscious mechanism to share anonymous usage statistics. According to the source code in the every-app/open-seo repository, this system operates through a carefully throttled heartbeat routine that aggregates database metrics and configuration states without exposing personally identifiable information.
Core Implementation in self-host-telemetry.ts
The heartbeat implementation centers on a single orchestration function that manages timing, data collection, and transmission.
The Entry Point and Guards
The maybeSendSelfHostHeartbeat function serves as the primary entry point for all telemetry activities. Before executing any logic, it performs mandatory environment checks defined at lines 15-34. The function immediately aborts for non-production builds—including development servers, Vitest test environments, and preview builds—to prevent test data from polluting production metrics.
Following the build check, the function evaluates three separate opt-out conditions: hosted-server mode, the OPENSEO_TELEMETRY_DISABLED environment variable, and the DO_NOT_TRACK standard. If any condition is true, the heartbeat skips execution entirely.
Memory Throttling and Timing Intervals
To minimize database load, the heartbeat implements a memory throttle that regulates how often the telemetryState table is queried. As implemented in getCheckIntervalMs at lines 47-52, the check frequency varies by install age:
- First two hours: During the
ONBOARDING_WINDOW_MSperiod, checks run every minute (ONBOARDING_CHECK_INTERVAL_MS) - After onboarding: Checks throttle to every 15 minutes (
STEADY_CHECK_INTERVAL_MS)
The Claim Mechanism and Slot Reservation
The claimHeartbeat function manages the actual heartbeat scheduling by reading the singleton row (ID = 1) from the telemetryState table. If no row exists, the function initializes one with a fresh installId. This mechanism ensures only one heartbeat fires per install per time window.
At lines 70-88, the function calculates the install's age and determines a cutoff timestamp based on either ONBOARDING_HEARTBEAT_INTERVAL_MS or DAILY_HEARTBEAT_INTERVAL_MS, depending on the lifecycle phase. If the existing lastHeartbeatAt timestamp exceeds this cutoff, the function updates the row with the current timestamp and returns a claim object; otherwise, it returns null to suppress redundant transmission.
Data Collected by the Heartbeat
When a heartbeat slot is successfully claimed, the system aggregates a comprehensive payload defined by the HeartbeatProperties type at lines 72-84.
Usage Metrics and Entity Counts
The collectCounts function (lines 95-122) queries the database to enumerate:
userCount: Registered users in the systemprojectCount: Total projects createdsiteAuditCount: Site audit recordsrankTrackingKeywordCount: Keywords under rank trackingsavedKeywordCount: Saved keyword tagsgscConnected: Boolean indicating Google Search Console integration statussamChatUsed: Boolean tracking whether the SAM chat feature has been accessedmcpToolCalls: Counter of MCP tool invocations since the last heartbeat
Setup Health and Environment Details
The payload includes a setupIssues array populated by getSetupIssueSummary from src/server/lib/setup-status.ts (lines 81-89). This function runs the same pre-flight checks used during Docker initialization, returning compact error codes (e.g., "dataforseo:error") for any checks not returning "ok".
Environment metadata captures:
deployTarget:"docker"for local-noauth installs, otherwise"cloudflare"dbBackend:"d1"or"postgres"based on the configured provider$process_person_profile: Alwaysfalseto comply with PostHog's anonymous event schema
Version Tracking and Install History
The heartbeat records versioning information to track upgrade patterns:
version: Current Open SEO versionprevVersion: Previous version (if upgraded since last heartbeat)firstRun: Boolean indicating whether this is the install's first heartbeatminutesSinceInstall: Approximate elapsed time since initialization
Transmitting to PostHog
Once assembled, the payload transmits to PostHog using a dedicated client instance.
Event Dispatch and Client Configuration
The sendHeartbeat function (lines 25-42) initializes a PostHog client with the self-host project key phc_xaXj4vE4LikxfvR7q6EHemAYNBSZW4hQkqor7fpf8aGT and dispatches an event named self_host.heartbeat containing the assembled properties object.
To ensure immediate transmission without batching delays, the client configures flushAt: 1 and flushInterval: 0, forcing synchronous delivery before the function completes.
Post-Heartbeat State Management
Following successful transmission, markHeartbeatSent updates the telemetryState table (lines 47-63) to set lastVersion to the current version and decrements the MCP tool call counter by the amount reported, resetting the counter for the next interval.
How to Interact with the Telemetry System
Developers and administrators can manually trigger or influence the telemetry system using the exported utility functions.
To manually trigger a heartbeat in scripts or administrative tools:
import { maybeSendSelfHostHeartbeat } from "@/server/lib/self-host-telemetry";
await maybeSendSelfHostHeartbeat(); // Respects throttling, opt-out, and environment guards
To increment the MCP tool call counter when building custom integrations:
import { incrementSelfHostMcpToolCallCount } from "@/server/lib/self-host-telemetry";
await incrementSelfHostMcpToolCallCount();
Summary
- The self-host telemetry heartbeat runs exclusively in production builds via
maybeSendSelfHostHeartbeatinsrc/server/lib/self-host-telemetry.ts - Throttling logic adapts check frequency based on install age, running every minute initially then every 15 minutes
- The claim mechanism uses a singleton
telemetryStatetable row to prevent duplicate transmissions and enforce intervals - Reported data includes entity counts (users, projects, audits), setup health checks, deployment environment, and version history
- Transmission occurs via PostHog with immediate flushing, sending to the
self_host.heartbeatevent stream - Opt-out controls respect
OPENSEO_TELEMETRY_DISABLED,DO_NOT_TRACK, and hosted-server mode flags
Frequently Asked Questions
How can I disable the self-host telemetry heartbeat?
Set the OPENSEO_TELEMETRY_DISABLED environment variable to any truthy value, or set DO_NOT_TRACK=1 in your environment. Additionally, running in hosted-server mode automatically disables telemetry. When any of these conditions are met, the maybeSendSelfHostHeartbeat function returns early without executing database queries or network calls.
How often does the heartbeat actually transmit data to PostHog?
During the first two hours after installation, the heartbeat transmits every minute if data has changed. After the onboarding window expires, transmission throttles to once every 15 minutes during steady-state operation. However, the actual PostHog event only fires if the claimHeartbeat function successfully reserves a slot based on these intervals.
What database tables store the telemetry state?
The system uses the telemetryState table defined in src/db/telemetry.schema.ts (D1) and src/db/pg/telemetry.schema.ts (Postgres). This table maintains a singleton row (ID = 1) containing the installId, lastHeartbeatAt timestamp, lastVersion string, and MCP tool call counters. Schema definitions are available in the respective database provider directories.
Does the heartbeat expose any personal user data?
No. According to the HeartbeatProperties type definition, the payload contains only aggregated counts and boolean flags. The system explicitly sets $process_person_profile: false for all PostHog events, and no email addresses, names, or content data are transmitted. The setupIssues array contains only diagnostic error codes, not configuration values or secrets.
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 →