mirror of
https://github.com/saltbo/zpan.git
synced 2026-08-29 00:01:42 +08:00
fix(telemetry): use PostHog SDK for instance reports
This commit is contained in:
@@ -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",
|
||||
|
||||
Generated
+28
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user