From 1d7ac9b071e621607ebc92bfd3b90f3178e65e87 Mon Sep 17 00:00:00 2001 From: Jasper Van Date: Mon, 8 Jun 2026 11:32:15 -0400 Subject: [PATCH] fix(telemetry): use built-in product reporting endpoint (#416) * fix(telemetry): use built-in product reporting endpoint * fix(telemetry): send reports through posthog capture --- .dev.vars.example | 4 --- server/entry-node.ts | 9 ----- server/scheduled-worker.test.ts | 5 --- server/services/instance-telemetry.test.ts | 26 ++++++++------ server/services/instance-telemetry.ts | 42 +++++++++++----------- workers/scheduled.ts | 17 --------- 6 files changed, 35 insertions(+), 68 deletions(-) diff --git a/.dev.vars.example b/.dev.vars.example index 46bf983b..de0c70ff 100644 --- a/.dev.vars.example +++ b/.dev.vars.example @@ -10,7 +10,3 @@ TRUSTED_ORIGINS=http://localhost:5185 # Local ZPan Cloud URL. ZPAN_CLOUD_URL=http://localhost:5186 - -# Optional instance telemetry reporting. -ZPAN_POSTHOG_HOST=https://e.zpan.space -ZPAN_POSTHOG_PROJECT_TOKEN= diff --git a/server/entry-node.ts b/server/entry-node.ts index 896bb3c8..ccb67c9a 100644 --- a/server/entry-node.ts +++ b/server/entry-node.ts @@ -77,14 +77,11 @@ setInterval(() => { await reportInstanceTelemetry({ db: platform.db, config: { - posthogHost: process.env.ZPAN_POSTHOG_HOST, - posthogProjectToken: process.env.ZPAN_POSTHOG_PROJECT_TOKEN, configuredInstanceId: process.env.ZPAN_INSTANCE_ID, }, cron: INSTANCE_TELEMETRY_CRON, runtime: { target: 'node/docker', - hostname: configuredTelemetryHostname(), osPlatform: process.platform, osArch: process.arch, osRelease: osRelease(), @@ -96,9 +93,3 @@ setInterval(() => { } })() }, INSTANCE_TELEMETRY_INTERVAL_MS) - -function configuredTelemetryHostname(): string | undefined { - const instanceUrl = configuredPublicOrigin() - if (instanceUrl) return new URL(instanceUrl).hostname - return process.env.HOSTNAME -} diff --git a/server/scheduled-worker.test.ts b/server/scheduled-worker.test.ts index f71b9277..fb980bec 100644 --- a/server/scheduled-worker.test.ts +++ b/server/scheduled-worker.test.ts @@ -65,8 +65,6 @@ describe('handleScheduled', () => { BETTER_AUTH_URL: 'https://zpan.example', ZPAN_CLOUD_URL: 'https://cloud.example', ZPAN_INSTANCE_ID: 'configured-instance', - ZPAN_POSTHOG_HOST: 'https://e.zpan.space', - ZPAN_POSTHOG_PROJECT_TOKEN: 'ph-token', }, ) @@ -74,14 +72,11 @@ describe('handleScheduled', () => { expect(reportInstanceTelemetry).toHaveBeenCalledWith({ db: 'db', config: { - posthogHost: 'https://e.zpan.space', - posthogProjectToken: 'ph-token', configuredInstanceId: 'configured-instance', }, cron: '0 */12 * * *', runtime: { target: 'cloudflare-worker', - hostname: 'zpan.example', }, }) expect(runLicensingRefresh).not.toHaveBeenCalled() diff --git a/server/services/instance-telemetry.test.ts b/server/services/instance-telemetry.test.ts index 61a7d826..72db511d 100644 --- a/server/services/instance-telemetry.test.ts +++ b/server/services/instance-telemetry.test.ts @@ -1,7 +1,13 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { getOrCreateInstanceId } from '../licensing/instance-id' import type { Database } from '../platform/interface' -import { INSTANCE_TELEMETRY_CRON, INSTANCE_TELEMETRY_EVENT, reportInstanceTelemetry } from './instance-telemetry' +import { + INSTANCE_TELEMETRY_CRON, + INSTANCE_TELEMETRY_ENDPOINT, + INSTANCE_TELEMETRY_EVENT, + INSTANCE_TELEMETRY_PRODUCT_TOKEN, + reportInstanceTelemetry, +} from './instance-telemetry' vi.mock('../licensing/instance-id', () => ({ getOrCreateInstanceId: vi.fn(), @@ -12,12 +18,12 @@ describe('instance telemetry', () => { vi.mocked(getOrCreateInstanceId).mockReset() }) - it('does not call PostHog when config is disabled', async () => { + it('does not call the telemetry endpoint when product token is disabled', async () => { const fetchFn = vi.fn() const result = await reportInstanceTelemetry({ db: {} as Database, - config: { posthogHost: 'https://e.zpan.space' }, + config: { productToken: '' }, cron: INSTANCE_TELEMETRY_CRON, runtime: { target: 'cloudflare-worker' }, fetchFn, @@ -28,21 +34,18 @@ describe('instance telemetry', () => { expect(getOrCreateInstanceId).not.toHaveBeenCalled() }) - it('captures the expected PostHog event when config is enabled', async () => { + it('captures the expected telemetry event with built-in endpoint and product token', async () => { vi.mocked(getOrCreateInstanceId).mockResolvedValue('inst-1') const fetchFn = vi.fn().mockResolvedValue(new Response('{}', { status: 200 })) const result = await reportInstanceTelemetry({ db: {} as Database, config: { - posthogHost: 'https://e.zpan.space/', - posthogProjectToken: 'ph-token', configuredInstanceId: 'configured-inst', }, cron: INSTANCE_TELEMETRY_CRON, runtime: { target: 'node/docker', - hostname: 'zpan.example', osPlatform: 'linux', osArch: 'arm64', osRelease: '6.8.0', @@ -54,15 +57,17 @@ describe('instance telemetry', () => { expect(result).toEqual({ reported: true }) expect(getOrCreateInstanceId).toHaveBeenCalledWith({}, 'configured-inst') expect(fetchFn).toHaveBeenCalledTimes(1) - expect(fetchFn).toHaveBeenCalledWith('https://e.zpan.space/capture/', { + expect(fetchFn).toHaveBeenCalledWith(INSTANCE_TELEMETRY_ENDPOINT, { method: 'POST', - headers: { 'content-type': 'application/json' }, + headers: { + 'content-type': 'application/json', + }, body: expect.any(String), }) const body = JSON.parse(fetchFn.mock.calls[0][1].body) expect(body).toMatchObject({ - api_key: 'ph-token', + api_key: INSTANCE_TELEMETRY_PRODUCT_TOKEN, event: INSTANCE_TELEMETRY_EVENT, distinct_id: 'inst-1', timestamp: '2026-06-08T12:00:00.000Z', @@ -70,7 +75,6 @@ describe('instance telemetry', () => { instance_id: 'inst-1', app_version: '0.0.1', runtime_target: 'node/docker', - hostname: 'zpan.example', os_platform: 'linux', os_arch: 'arm64', os_release: '6.8.0', diff --git a/server/services/instance-telemetry.ts b/server/services/instance-telemetry.ts index c5fdd032..6e926d4b 100644 --- a/server/services/instance-telemetry.ts +++ b/server/services/instance-telemetry.ts @@ -3,18 +3,19 @@ import { getOrCreateInstanceId } from '../licensing/instance-id' import type { Database } from '../platform/interface' export const INSTANCE_TELEMETRY_CRON = '0 */12 * * *' -export const INSTANCE_TELEMETRY_EVENT = 'zpan instance reported' +export const INSTANCE_TELEMETRY_EVENT = 'heartbeat' export const INSTANCE_TELEMETRY_INTERVAL = '12h' +export const INSTANCE_TELEMETRY_ENDPOINT = 'https://e.zpan.space/capture/' +export const INSTANCE_TELEMETRY_PRODUCT_TOKEN = 'pub_4709cd351f9bf91df7a4926d8ec835f423b0b2539a1d6f53' export interface InstanceTelemetryConfig { - posthogHost?: string - posthogProjectToken?: string + endpoint?: string + productToken?: string configuredInstanceId?: string } export interface InstanceTelemetryRuntime { target: 'cloudflare-worker' | 'node/docker' - hostname?: string osPlatform?: string osArch?: string osRelease?: string @@ -34,7 +35,7 @@ export interface InstanceTelemetryResult { reason?: 'disabled' } -interface PostHogCapturePayload { +interface TelemetryCapturePayload { api_key: string event: string distinct_id: string @@ -43,37 +44,39 @@ interface PostHogCapturePayload { } export async function reportInstanceTelemetry(params: InstanceTelemetryParams): Promise { - const posthogHost = params.config.posthogHost?.trim() - const posthogProjectToken = params.config.posthogProjectToken?.trim() - if (!posthogHost || !posthogProjectToken) return { reported: false, reason: 'disabled' } + const endpoint = (params.config.endpoint ?? INSTANCE_TELEMETRY_ENDPOINT).trim() + const productToken = (params.config.productToken ?? INSTANCE_TELEMETRY_PRODUCT_TOKEN).trim() + if (!endpoint || !productToken) return { reported: false, reason: 'disabled' } const instanceId = await getOrCreateInstanceId(params.db, params.config.configuredInstanceId) const timestamp = (params.now ?? new Date()).toISOString() - const payload = buildPostHogCapturePayload({ + const payload = buildTelemetryPayload({ instanceId, - posthogProjectToken, cron: params.cron, runtime: params.runtime, timestamp, + productToken, }) - const res = await (params.fetchFn ?? fetch)(posthogCaptureUrl(posthogHost), { + const res = await (params.fetchFn ?? fetch)(endpoint, { method: 'POST', - headers: { 'content-type': 'application/json' }, + headers: { + 'content-type': 'application/json', + }, body: JSON.stringify(payload), }) - if (!res.ok) throw new Error(`posthog_capture_failed_${res.status}`) + if (!res.ok) throw new Error(`instance_telemetry_failed_${res.status}`) return { reported: true } } -function buildPostHogCapturePayload(params: { +function buildTelemetryPayload(params: { instanceId: string - posthogProjectToken: string cron: string runtime: InstanceTelemetryRuntime timestamp: string -}): PostHogCapturePayload { + productToken: string +}): TelemetryCapturePayload { const properties: Record = { instance_id: params.instanceId, app_version: packageJson.version, @@ -83,13 +86,12 @@ function buildPostHogCapturePayload(params: { reported_at: params.timestamp, } - addOptionalProperty(properties, 'hostname', params.runtime.hostname) addOptionalProperty(properties, 'os_platform', params.runtime.osPlatform) addOptionalProperty(properties, 'os_arch', params.runtime.osArch) addOptionalProperty(properties, 'os_release', params.runtime.osRelease) return { - api_key: params.posthogProjectToken, + api_key: params.productToken, event: INSTANCE_TELEMETRY_EVENT, distinct_id: params.instanceId, properties, @@ -100,7 +102,3 @@ function buildPostHogCapturePayload(params: { function addOptionalProperty(properties: Record, key: string, value: string | undefined): void { if (value) properties[key] = value } - -function posthogCaptureUrl(host: string): string { - return `${host.replace(/\/+$/, '')}/capture/` -} diff --git a/workers/scheduled.ts b/workers/scheduled.ts index f7cf779f..5452fe23 100644 --- a/workers/scheduled.ts +++ b/workers/scheduled.ts @@ -11,12 +11,8 @@ import { ZPAN_CLOUD_URL_DEFAULT } from '../shared/constants' // The full Env is defined in bootstrap.ts; this avoids circular imports. export interface ScheduledEnv { DB: D1Database - BETTER_AUTH_URL?: string - ZPAN_PUBLIC_ORIGIN?: string ZPAN_CLOUD_URL?: string ZPAN_INSTANCE_ID?: string - ZPAN_POSTHOG_HOST?: string - ZPAN_POSTHOG_PROJECT_TOKEN?: string [key: string]: unknown } @@ -36,14 +32,11 @@ export async function handleScheduled(event: ScheduledTrigger, env: ScheduledEnv await reportInstanceTelemetry({ db: platform.db, config: { - posthogHost: env.ZPAN_POSTHOG_HOST, - posthogProjectToken: env.ZPAN_POSTHOG_PROJECT_TOKEN, configuredInstanceId: env.ZPAN_INSTANCE_ID, }, cron: event.cron, runtime: { target: 'cloudflare-worker', - hostname: configuredHostname(env), }, }) return @@ -51,13 +44,3 @@ export async function handleScheduled(event: ScheduledTrigger, env: ScheduledEnv await runLicensingRefresh(platform.db, cloudBaseUrl) } - -function configuredHostname(env: ScheduledEnv): string | undefined { - const value = env.ZPAN_PUBLIC_ORIGIN ?? env.BETTER_AUTH_URL - if (!value) return undefined - try { - return new URL(value).hostname - } catch { - return undefined - } -}