mirror of
https://github.com/saltbo/zpan.git
synced 2026-11-01 12:01:28 +08:00
Flatten the instance `runtime` object into two fields: `runtime` (the JS engine,
node | workerd) and `platform` (the deployment host). The About page shows each
as its own row with friendly labels (e.g. "workerd" + "Cloudflare Workers",
"Node.js" + "Docker").
- Detect the platform from the entry file (entry === target): each serverless
entry declares it; entry-node sniffs Cloud Run (K_SERVICE) / Docker
(ZPAN_RUNTIME, set in the Dockerfile) / bare node. Cloudflare is detected from
the D1 binding.
- Decouple the cloud payload: zpan-cloud-sdk fixes runtime { provider, target },
so CloudInstanceInfo keeps that shape and buildCloudInstanceInfo maps to it;
buildInstanceInfo serves the richer flat shape to the About API.
- Migrate PostHog instance telemetry to the runtime/platform shape and merge the
duplicate runtimeInfo in licensing-admin into the shared one.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
159 lines
5.0 KiB
TypeScript
159 lines
5.0 KiB
TypeScript
import { PostHog } from 'posthog-node'
|
|
import { getOrCreateInstanceId } from '../licensing/instance-id'
|
|
import { getInstanceDisplayName } from '../licensing/instance-info'
|
|
import type { Database } from '../platform/interface'
|
|
import type { DeployPlatform } from '../runtime-platform'
|
|
import { getAppVersion } from '../version'
|
|
import { getSitePublicOrigin, normalizePublicOrigin } from './site-public-origin'
|
|
|
|
export const INSTANCE_TELEMETRY_CRON = '0 */12 * * *'
|
|
export const INSTANCE_TELEMETRY_EVENT = 'heartbeat'
|
|
export const INSTANCE_TELEMETRY_INTERVAL = '12h'
|
|
export const INSTANCE_TELEMETRY_POSTHOG_HOST = 'https://e.zpan.space'
|
|
export const INSTANCE_TELEMETRY_POSTHOG_PROJECT_TOKEN = 'phc_uh9AB5AqnpXpFfW2Ns7bDGHaofSTLcA7TeatP6HzmtpF'
|
|
|
|
export interface InstanceTelemetryConfig {
|
|
posthogHost?: string
|
|
posthogProjectToken?: string
|
|
siteUrl?: string
|
|
allowIp?: boolean
|
|
}
|
|
|
|
export interface InstanceTelemetryRuntime {
|
|
runtime: 'node' | 'workerd'
|
|
platform: DeployPlatform
|
|
osPlatform?: string
|
|
osArch?: string
|
|
osRelease?: string
|
|
nodeVersion?: string
|
|
}
|
|
|
|
export interface InstanceTelemetryParams {
|
|
db: Database
|
|
config: InstanceTelemetryConfig
|
|
cron: string
|
|
trigger?: 'deploy' | 'scheduled' | 'runtime'
|
|
runtime: InstanceTelemetryRuntime
|
|
now?: Date
|
|
}
|
|
|
|
export interface InstanceTelemetryResult {
|
|
reported: boolean
|
|
reason?: 'disabled'
|
|
}
|
|
|
|
export async function reportInstanceTelemetry(params: InstanceTelemetryParams): Promise<InstanceTelemetryResult> {
|
|
const posthogHost = (params.config.posthogHost ?? INSTANCE_TELEMETRY_POSTHOG_HOST).trim()
|
|
const posthogProjectToken = (params.config.posthogProjectToken ?? INSTANCE_TELEMETRY_POSTHOG_PROJECT_TOKEN).trim()
|
|
if (!posthogHost || !posthogProjectToken) return { reported: false, reason: 'disabled' }
|
|
|
|
const instanceId = await getOrCreateInstanceId(params.db)
|
|
const instanceName = await getInstanceDisplayName(params.db)
|
|
const instanceUrl =
|
|
normalizePublicOrigin(params.config.siteUrl) ?? (await getSitePublicOrigin(params.db)) ?? undefined
|
|
const appVersion = getAppVersion()
|
|
const timestamp = (params.now ?? new Date()).toISOString()
|
|
const disableGeoip = params.config.allowIp === false
|
|
const client = new PostHog(posthogProjectToken, {
|
|
host: posthogHost,
|
|
flushAt: 1,
|
|
flushInterval: 0,
|
|
disableCompression: true,
|
|
disableGeoip,
|
|
})
|
|
|
|
await client.captureImmediate({
|
|
distinctId: instanceId,
|
|
event: INSTANCE_TELEMETRY_EVENT,
|
|
disableGeoip,
|
|
timestamp: new Date(timestamp),
|
|
properties: buildTelemetryProperties({
|
|
instanceId,
|
|
instanceName,
|
|
instanceUrl,
|
|
appVersion,
|
|
cron: params.cron,
|
|
trigger: params.trigger ?? 'scheduled',
|
|
runtime: params.runtime,
|
|
timestamp,
|
|
}),
|
|
})
|
|
await client.shutdown()
|
|
|
|
return { reported: true }
|
|
}
|
|
|
|
function buildTelemetryProperties(params: {
|
|
instanceId: string
|
|
instanceName: string
|
|
instanceUrl?: string
|
|
appVersion: string
|
|
cron: string
|
|
trigger: 'deploy' | 'scheduled' | 'runtime'
|
|
runtime: InstanceTelemetryRuntime
|
|
timestamp: string
|
|
}): Record<string, unknown> {
|
|
const instance = compactObject({
|
|
id: params.instanceId,
|
|
name: params.instanceName,
|
|
url: params.instanceUrl,
|
|
version: params.appVersion,
|
|
runtime: params.runtime.runtime,
|
|
platform: params.runtime.platform,
|
|
server: optionalNestedObject({
|
|
os: optionalObject({
|
|
platform: params.runtime.osPlatform,
|
|
arch: params.runtime.osArch,
|
|
release: params.runtime.osRelease,
|
|
}),
|
|
}),
|
|
node: optionalObject({
|
|
version: params.runtime.nodeVersion,
|
|
}),
|
|
})
|
|
|
|
const properties: Record<string, unknown> = {
|
|
instance,
|
|
report_trigger: params.trigger,
|
|
report_interval: INSTANCE_TELEMETRY_INTERVAL,
|
|
report_schedule: params.cron,
|
|
reported_at: params.timestamp,
|
|
$set: compactObject({
|
|
instance,
|
|
$current_url: params.instanceUrl,
|
|
}),
|
|
$set_once: compactObject({
|
|
initialInstance: instance,
|
|
initialReportedAt: params.timestamp,
|
|
}),
|
|
}
|
|
|
|
addOptionalProperty(properties, '$current_url', params.instanceUrl)
|
|
|
|
return properties
|
|
}
|
|
|
|
function addOptionalProperty(properties: Record<string, unknown>, key: string, value: string | undefined): void {
|
|
if (value) properties[key] = value
|
|
}
|
|
|
|
function compactObject(input: Record<string, unknown>): Record<string, unknown> {
|
|
return Object.fromEntries(Object.entries(input).filter((entry) => entry[1] !== undefined))
|
|
}
|
|
|
|
function optionalObject(input: Record<string, string | undefined>): Record<string, string> | undefined {
|
|
const output = Object.fromEntries(
|
|
Object.entries(input).filter((entry): entry is [string, string] => entry[1] !== undefined),
|
|
)
|
|
return Object.keys(output).length > 0 ? output : undefined
|
|
}
|
|
|
|
function optionalNestedObject(
|
|
input: Record<string, Record<string, string> | undefined>,
|
|
): Record<string, Record<string, string>> | undefined {
|
|
const output = Object.fromEntries(
|
|
Object.entries(input).filter((entry): entry is [string, Record<string, string>] => entry[1] !== undefined),
|
|
)
|
|
return Object.keys(output).length > 0 ? output : undefined
|
|
}
|