fix(telemetry): report instance after deployment (#417)

This commit is contained in:
Jasper Van
2026-06-08 12:40:59 -04:00
committed by GitHub
parent 1d7ac9b071
commit bd9f047ebb
9 changed files with 218 additions and 36 deletions
+2
View File
@@ -19,6 +19,7 @@ import downloaders, { downloaderSelfRoute } from './routes/downloaders'
import emailConfig from './routes/email-config'
import ihost from './routes/ihost'
import ihostConfig from './routes/ihost-config'
import internal from './routes/internal'
import { adminInviteCodes, publicInviteCodes } from './routes/invite-codes'
import licensing from './routes/licensing'
import licensingAdmin from './routes/licensing-admin'
@@ -80,6 +81,7 @@ export function createApp(platform: Platform, auth: Auth) {
app.route('/api/branding', publicBranding)
app.route('/api/site-invitations', publicSiteInvitations)
app.route('/api/store', cloudStoreWebhooks)
app.route('/api/internal', internal)
app.use('/api/*', authMiddleware)
+6 -3
View File
@@ -70,8 +70,7 @@ setInterval(() => {
void syncPendingRemoteDownloadUsageReports({ db: platform.db, cloudBaseUrl })
}, TRAFFIC_SYNC_INTERVAL_MS)
console.log('instance.telemetry.scheduler.started interval=12h')
setInterval(() => {
function reportNodeInstanceTelemetry(): void {
void (async () => {
try {
await reportInstanceTelemetry({
@@ -92,4 +91,8 @@ setInterval(() => {
console.error(`instance.telemetry.error code=${code}`)
}
})()
}, INSTANCE_TELEMETRY_INTERVAL_MS)
}
console.log('instance.telemetry.scheduler.started interval=12h')
reportNodeInstanceTelemetry()
setInterval(reportNodeInstanceTelemetry, INSTANCE_TELEMETRY_INTERVAL_MS)
+64
View File
@@ -0,0 +1,64 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { reportInstanceTelemetry } from '../services/instance-telemetry'
import { createTestApp } from '../test/setup.js'
vi.mock('../services/instance-telemetry', () => ({
INSTANCE_TELEMETRY_CRON: '0 */12 * * *',
reportInstanceTelemetry: vi.fn(),
}))
describe('POST /api/internal/instance-telemetry/report', () => {
beforeEach(() => {
vi.mocked(reportInstanceTelemetry).mockReset()
vi.mocked(reportInstanceTelemetry).mockResolvedValue({ reported: true })
})
it('returns 404 when the internal token is not configured', async () => {
const { app } = await createTestApp()
const res = await app.request('/api/internal/instance-telemetry/report', {
method: 'POST',
headers: { Authorization: 'Bearer test-token' },
})
expect(res.status).toBe(404)
expect(reportInstanceTelemetry).not.toHaveBeenCalled()
})
it('rejects requests with the wrong token', async () => {
const { app } = await createTestApp({ ZPAN_INTERNAL_API_TOKEN: 'test-token' })
const res = await app.request('/api/internal/instance-telemetry/report', {
method: 'POST',
headers: { Authorization: 'Bearer wrong-token' },
})
expect(res.status).toBe(401)
expect(reportInstanceTelemetry).not.toHaveBeenCalled()
})
it('reports telemetry with the configured internal token', async () => {
const { app, db } = await createTestApp({
ZPAN_INTERNAL_API_TOKEN: 'test-token',
ZPAN_INSTANCE_ID: 'configured-instance',
})
const res = await app.request('/api/internal/instance-telemetry/report', {
method: 'POST',
headers: { Authorization: 'Bearer test-token' },
})
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ reported: true })
expect(reportInstanceTelemetry).toHaveBeenCalledWith({
db,
config: {
configuredInstanceId: 'configured-instance',
},
cron: '0 */12 * * *',
runtime: expect.objectContaining({
target: 'node/docker',
}),
})
})
})
+41
View File
@@ -0,0 +1,41 @@
import { release as osRelease } from 'node:os'
import { Hono } from 'hono'
import type { Env } from '../middleware/platform'
import { INSTANCE_TELEMETRY_CRON, reportInstanceTelemetry } from '../services/instance-telemetry'
const INTERNAL_API_TOKEN_ENV = 'ZPAN_INTERNAL_API_TOKEN'
const internal = new Hono<Env>()
internal.post('/instance-telemetry/report', async (c) => {
const platform = c.get('platform')
const token = platform.getEnv(INTERNAL_API_TOKEN_ENV)?.trim()
if (!token) return c.json({ error: 'Not found' }, 404)
const auth = c.req.header('authorization') ?? ''
if (auth !== `Bearer ${token}`) return c.json({ error: 'Unauthorized' }, 401)
const runtime = platform.getBinding('DB')
? {
target: 'cloudflare-worker' as const,
}
: {
target: 'node/docker' as const,
osPlatform: process.platform,
osArch: process.arch,
osRelease: osRelease(),
}
const result = await reportInstanceTelemetry({
db: platform.db,
config: {
configuredInstanceId: platform.getEnv('ZPAN_INSTANCE_ID'),
},
cron: INSTANCE_TELEMETRY_CRON,
runtime,
})
return c.json(result)
})
export default internal
+5 -5
View File
@@ -5,7 +5,7 @@ import {
INSTANCE_TELEMETRY_CRON,
INSTANCE_TELEMETRY_ENDPOINT,
INSTANCE_TELEMETRY_EVENT,
INSTANCE_TELEMETRY_PRODUCT_TOKEN,
INSTANCE_TELEMETRY_POSTHOG_PROJECT_TOKEN,
reportInstanceTelemetry,
} from './instance-telemetry'
@@ -18,12 +18,12 @@ describe('instance telemetry', () => {
vi.mocked(getOrCreateInstanceId).mockReset()
})
it('does not call the telemetry endpoint when product token is disabled', async () => {
it('does not call the telemetry endpoint when PostHog project token is disabled', async () => {
const fetchFn = vi.fn()
const result = await reportInstanceTelemetry({
db: {} as Database,
config: { productToken: '' },
config: { posthogProjectToken: '' },
cron: INSTANCE_TELEMETRY_CRON,
runtime: { target: 'cloudflare-worker' },
fetchFn,
@@ -34,7 +34,7 @@ describe('instance telemetry', () => {
expect(getOrCreateInstanceId).not.toHaveBeenCalled()
})
it('captures the expected telemetry event with built-in endpoint and product token', async () => {
it('captures the expected telemetry event with built-in PostHog endpoint and project token', async () => {
vi.mocked(getOrCreateInstanceId).mockResolvedValue('inst-1')
const fetchFn = vi.fn().mockResolvedValue(new Response('{}', { status: 200 }))
@@ -67,7 +67,7 @@ describe('instance telemetry', () => {
const body = JSON.parse(fetchFn.mock.calls[0][1].body)
expect(body).toMatchObject({
api_key: INSTANCE_TELEMETRY_PRODUCT_TOKEN,
api_key: INSTANCE_TELEMETRY_POSTHOG_PROJECT_TOKEN,
event: INSTANCE_TELEMETRY_EVENT,
distinct_id: 'inst-1',
timestamp: '2026-06-08T12:00:00.000Z',
+7 -7
View File
@@ -6,11 +6,11 @@ export const INSTANCE_TELEMETRY_CRON = '0 */12 * * *'
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 const INSTANCE_TELEMETRY_POSTHOG_PROJECT_TOKEN = 'pub_4709cd351f9bf91df7a4926d8ec835f423b0b2539a1d6f53'
export interface InstanceTelemetryConfig {
endpoint?: string
productToken?: string
posthogProjectToken?: string
configuredInstanceId?: string
}
@@ -45,8 +45,8 @@ interface TelemetryCapturePayload {
export async function reportInstanceTelemetry(params: InstanceTelemetryParams): Promise<InstanceTelemetryResult> {
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 posthogProjectToken = (params.config.posthogProjectToken ?? INSTANCE_TELEMETRY_POSTHOG_PROJECT_TOKEN).trim()
if (!endpoint || !posthogProjectToken) return { reported: false, reason: 'disabled' }
const instanceId = await getOrCreateInstanceId(params.db, params.config.configuredInstanceId)
const timestamp = (params.now ?? new Date()).toISOString()
@@ -55,7 +55,7 @@ export async function reportInstanceTelemetry(params: InstanceTelemetryParams):
cron: params.cron,
runtime: params.runtime,
timestamp,
productToken,
posthogProjectToken,
})
const res = await (params.fetchFn ?? fetch)(endpoint, {
@@ -75,7 +75,7 @@ function buildTelemetryPayload(params: {
cron: string
runtime: InstanceTelemetryRuntime
timestamp: string
productToken: string
posthogProjectToken: string
}): TelemetryCapturePayload {
const properties: Record<string, string> = {
instance_id: params.instanceId,
@@ -91,7 +91,7 @@ function buildTelemetryPayload(params: {
addOptionalProperty(properties, 'os_release', params.runtime.osRelease)
return {
api_key: params.productToken,
api_key: params.posthogProjectToken,
event: INSTANCE_TELEMETRY_EVENT,
distinct_id: params.instanceId,
properties,