fix(telemetry): use PostHog SDK for instance reports

This commit is contained in:
saltbo
2026-06-08 13:15:00 -04:00
parent 61f643ae18
commit 5c3df3c8ec
4 changed files with 87 additions and 57 deletions
+1
View File
@@ -87,6 +87,7 @@
"nodemailer": "^8.0.5",
"paseto-ts": "^2.0.6",
"pdfjs-dist": "^5.6.205",
"posthog-node": "^5.36.4",
"radix-ui": "^1.4.3",
"react": "^19.2.5",
"react-dom": "^19.2.5",
+28
View File
@@ -151,6 +151,9 @@ importers:
pdfjs-dist:
specifier: ^5.6.205
version: 5.6.205
posthog-node:
specifier: ^5.36.4
version: 5.36.4
radix-ui:
specifier: ^1.4.3
version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -1872,6 +1875,12 @@ packages:
'@poppinss/exception@1.2.3':
resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==}
'@posthog/core@1.30.10':
resolution: {integrity: sha512-R7Z5jDB3ugwfSujMmRd5osPPR6L6BqfcaSNcYOekzRMZ4Jklq74p05xByP09EnUvKXb5czI+RQVCITTWRWuFXw==}
'@posthog/types@1.382.0':
resolution: {integrity: sha512-iK4OcSgvtmS9FZ9EUpvwlRZmHCLXaZ3+6dbRjkE7q9LL0zHLewxJH84H6uGvCw8aGzxs5rIliZqPHgimTcQEaw==}
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
@@ -4761,6 +4770,15 @@ packages:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14}
posthog-node@5.36.4:
resolution: {integrity: sha512-N+1WiypMHf3SO3NNoXTUFRzX98TuM5w4bDCm8RenYPf0rvX6r8v+yH6IzL1g/Me+wWA5+sfE1JZ6kGV6pWTZyQ==}
engines: {node: ^20.20.0 || >=22.22.0}
peerDependencies:
rxjs: ^7.0.0
peerDependenciesMeta:
rxjs:
optional: true
prebuild-install@7.1.3:
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
engines: {node: '>=10'}
@@ -6973,6 +6991,12 @@ snapshots:
'@poppinss/exception@1.2.3': {}
'@posthog/core@1.30.10':
dependencies:
'@posthog/types': 1.382.0
'@posthog/types@1.382.0': {}
'@radix-ui/number@1.1.1': {}
'@radix-ui/primitive@1.1.3': {}
@@ -10041,6 +10065,10 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
posthog-node@5.36.4:
dependencies:
'@posthog/core': 1.30.10
prebuild-install@7.1.3:
dependencies:
detect-libc: 2.1.2
+34 -21
View File
@@ -3,40 +3,57 @@ import { getOrCreateInstanceId } from '../licensing/instance-id'
import type { Database } from '../platform/interface'
import {
INSTANCE_TELEMETRY_CRON,
INSTANCE_TELEMETRY_ENDPOINT,
INSTANCE_TELEMETRY_EVENT,
INSTANCE_TELEMETRY_POSTHOG_HOST,
INSTANCE_TELEMETRY_POSTHOG_PROJECT_TOKEN,
reportInstanceTelemetry,
} from './instance-telemetry'
const posthogMocks = vi.hoisted(() => {
const captureImmediateMock = vi.fn()
const shutdownMock = vi.fn()
const PostHog = vi.fn(
class {
captureImmediate = captureImmediateMock
shutdown = shutdownMock
},
)
return { PostHog, captureImmediate: captureImmediateMock, shutdown: shutdownMock }
})
vi.mock('../licensing/instance-id', () => ({
getOrCreateInstanceId: vi.fn(),
}))
vi.mock('posthog-node', () => ({
PostHog: posthogMocks.PostHog,
}))
describe('instance telemetry', () => {
beforeEach(() => {
vi.mocked(getOrCreateInstanceId).mockReset()
posthogMocks.PostHog.mockClear()
posthogMocks.captureImmediate.mockReset()
posthogMocks.shutdown.mockReset()
posthogMocks.captureImmediate.mockResolvedValue(undefined)
posthogMocks.shutdown.mockResolvedValue(undefined)
})
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: { posthogProjectToken: '' },
cron: INSTANCE_TELEMETRY_CRON,
runtime: { target: 'cloudflare-worker' },
fetchFn,
})
expect(result).toEqual({ reported: false, reason: 'disabled' })
expect(fetchFn).not.toHaveBeenCalled()
expect(posthogMocks.PostHog).not.toHaveBeenCalled()
expect(getOrCreateInstanceId).not.toHaveBeenCalled()
})
it('captures the expected telemetry event with built-in PostHog endpoint and project token', async () => {
it('captures the expected telemetry event with built-in PostHog host and project token', async () => {
vi.mocked(getOrCreateInstanceId).mockResolvedValue('inst-1')
const fetchFn = vi.fn().mockResolvedValue(new Response('{}', { status: 200 }))
const result = await reportInstanceTelemetry({
db: {} as Database,
@@ -51,26 +68,21 @@ describe('instance telemetry', () => {
osRelease: '6.8.0',
},
now: new Date('2026-06-08T12:00:00.000Z'),
fetchFn,
})
expect(result).toEqual({ reported: true })
expect(getOrCreateInstanceId).toHaveBeenCalledWith({}, 'configured-inst')
expect(fetchFn).toHaveBeenCalledTimes(1)
expect(fetchFn).toHaveBeenCalledWith(INSTANCE_TELEMETRY_ENDPOINT, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: expect.any(String),
expect(posthogMocks.PostHog).toHaveBeenCalledWith(INSTANCE_TELEMETRY_POSTHOG_PROJECT_TOKEN, {
host: INSTANCE_TELEMETRY_POSTHOG_HOST,
flushAt: 1,
flushInterval: 0,
disableCompression: true,
})
const body = JSON.parse(fetchFn.mock.calls[0][1].body)
expect(body).toMatchObject({
api_key: INSTANCE_TELEMETRY_POSTHOG_PROJECT_TOKEN,
expect(posthogMocks.captureImmediate).toHaveBeenCalledTimes(1)
expect(posthogMocks.captureImmediate).toHaveBeenCalledWith({
event: INSTANCE_TELEMETRY_EVENT,
distinct_id: 'inst-1',
timestamp: '2026-06-08T12:00:00.000Z',
distinctId: 'inst-1',
timestamp: new Date('2026-06-08T12:00:00.000Z'),
properties: {
instance_id: 'inst-1',
app_version: '0.0.1',
@@ -83,5 +95,6 @@ describe('instance telemetry', () => {
reported_at: '2026-06-08T12:00:00.000Z',
},
})
expect(posthogMocks.shutdown).toHaveBeenCalledTimes(1)
})
})
+24 -36
View File
@@ -1,3 +1,4 @@
import { PostHog } from 'posthog-node'
import packageJson from '../../package.json'
import { getOrCreateInstanceId } from '../licensing/instance-id'
import type { Database } from '../platform/interface'
@@ -5,11 +6,11 @@ import type { Database } from '../platform/interface'
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_POSTHOG_HOST = 'https://e.zpan.space'
export const INSTANCE_TELEMETRY_POSTHOG_PROJECT_TOKEN = 'pub_4709cd351f9bf91df7a4926d8ec835f423b0b2539a1d6f53'
export interface InstanceTelemetryConfig {
endpoint?: string
posthogHost?: string
posthogProjectToken?: string
configuredInstanceId?: string
}
@@ -27,7 +28,6 @@ export interface InstanceTelemetryParams {
cron: string
runtime: InstanceTelemetryRuntime
now?: Date
fetchFn?: typeof fetch
}
export interface InstanceTelemetryResult {
@@ -35,48 +35,42 @@ export interface InstanceTelemetryResult {
reason?: 'disabled'
}
interface TelemetryCapturePayload {
api_key: string
event: string
distinct_id: string
properties: Record<string, string>
timestamp: string
}
export async function reportInstanceTelemetry(params: InstanceTelemetryParams): Promise<InstanceTelemetryResult> {
const endpoint = (params.config.endpoint ?? INSTANCE_TELEMETRY_ENDPOINT).trim()
const posthogHost = (params.config.posthogHost ?? INSTANCE_TELEMETRY_POSTHOG_HOST).trim()
const posthogProjectToken = (params.config.posthogProjectToken ?? INSTANCE_TELEMETRY_POSTHOG_PROJECT_TOKEN).trim()
if (!endpoint || !posthogProjectToken) return { reported: false, reason: 'disabled' }
if (!posthogHost || !posthogProjectToken) return { reported: false, reason: 'disabled' }
const instanceId = await getOrCreateInstanceId(params.db, params.config.configuredInstanceId)
const timestamp = (params.now ?? new Date()).toISOString()
const payload = buildTelemetryPayload({
instanceId,
cron: params.cron,
runtime: params.runtime,
timestamp,
posthogProjectToken,
const client = new PostHog(posthogProjectToken, {
host: posthogHost,
flushAt: 1,
flushInterval: 0,
disableCompression: true,
})
const res = await (params.fetchFn ?? fetch)(endpoint, {
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(payload),
await client.captureImmediate({
distinctId: instanceId,
event: INSTANCE_TELEMETRY_EVENT,
timestamp: new Date(timestamp),
properties: buildTelemetryProperties({
instanceId,
cron: params.cron,
runtime: params.runtime,
timestamp,
}),
})
await client.shutdown()
if (!res.ok) throw new Error(`instance_telemetry_failed_${res.status}`)
return { reported: true }
}
function buildTelemetryPayload(params: {
function buildTelemetryProperties(params: {
instanceId: string
cron: string
runtime: InstanceTelemetryRuntime
timestamp: string
posthogProjectToken: string
}): TelemetryCapturePayload {
}): Record<string, string> {
const properties: Record<string, string> = {
instance_id: params.instanceId,
app_version: packageJson.version,
@@ -90,13 +84,7 @@ function buildTelemetryPayload(params: {
addOptionalProperty(properties, 'os_arch', params.runtime.osArch)
addOptionalProperty(properties, 'os_release', params.runtime.osRelease)
return {
api_key: params.posthogProjectToken,
event: INSTANCE_TELEMETRY_EVENT,
distinct_id: params.instanceId,
properties,
timestamp: params.timestamp,
}
return properties
}
function addOptionalProperty(properties: Record<string, string>, key: string, value: string | undefined): void {