// Generated by scripts/sync-shared.mjs from shared/client/telemetry.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs". /** * Anonymous install telemetry shared by the DSH Web UI family plugins. * * Once per UTC day per browser, each wired plugin sends one heartbeat to * the dsh-market edge API listing its package name (and version when known). * The payload carries no conversation data and no identifiers beyond a random * UUID generated in localStorage; the server hashes it with a deployment salt * before storage and never persists IP addresses. Sends are fire-and-forget: * failures stay silent and simply retry on a later mount or day. The system * is documented in docs/telemetry.md. */ export type TelemetryChannel = 'market' | 'npm' | 'unknown' export interface TelemetryItem { /** npm package name or asset id, e.g. "@linxin666/dsh-pet" or "skin:harbor". */ name: string /** Installed version when known; omitted otherwise. */ version?: string /** Install channel when determinable (market = Workshop install). */ channel?: TelemetryChannel } const VISITOR_KEY = 'dsh-web-ui-telemetry-visitor' const DAY_KEY_PREFIX = 'dsh-web-ui-telemetry-day:' const ENDPOINT = 'https://dsh-market.com/api/telemetry/event' // Baked into each client bundle by shared/tsdown.client.ts (the package's own // package.json version at build time). Undefined in dev/test builds. declare const __DSH_PKG_VERSION__: string | undefined /** The building package's version, when the bundle carries it. */ function bakedVersion(): string | undefined { try { return typeof __DSH_PKG_VERSION__ === 'string' && __DSH_PKG_VERSION__ !== '' ? __DSH_PKG_VERSION__ : undefined } catch { return undefined } } /** Read or lazily create the anonymous visitor id; null when storage is unavailable. */ function visitorId(): string | null { try { const existing = localStorage.getItem(VISITOR_KEY) if (existing && /^[A-Za-z0-9_-]{16,64}$/.test(existing)) return existing const fresh = crypto.randomUUID().replaceAll('-', '') localStorage.setItem(VISITOR_KEY, fresh) return fresh } catch { // Storage unavailable (privacy mode, sandboxed frame): report nothing. return null } } /** Drop stale per-day dedup keys so localStorage does not grow forever. */ function pruneDayKeys(today: string): void { try { for (let index = localStorage.length - 1; index >= 0; index -= 1) { const key = localStorage.key(index) if (key !== null && key.startsWith(DAY_KEY_PREFIX) && key !== DAY_KEY_PREFIX + today) { localStorage.removeItem(key) } } } catch { /* best effort */ } } /** * Fire the daily heartbeat for the given items at most once per UTC day per * browser. Never throws and never blocks the caller. Items without an explicit * version inherit the bundle's baked build version. */ export function reportDailyHeartbeat(items: readonly TelemetryItem[]): void { try { if (items.length === 0) return const today = new Date().toISOString().slice(0, 10) // Automation contexts (CDP-driven QA browsers, fresh isolated profiles) // mint a new visitor id per run and would inflate instance counts. if (navigator.webdriver) return if (localStorage.getItem(DAY_KEY_PREFIX + today) !== null) return const visitor = visitorId() if (visitor === null) return pruneDayKeys(today) const payloadItems = items.map((item) => { const out: Record = { name: item.name } const version = item.version ?? bakedVersion() if (version !== undefined) out.version = version if (item.channel !== undefined) out.channel = item.channel return out }) const body = JSON.stringify({ kind: 'heartbeat', visitor, items: payloadItems }) void fetch(ENDPOINT, { method: 'POST', headers: { 'content-type': 'application/json' }, body, keepalive: true, }).then((response) => { // Mark the day only after an accepted send, so offline browsers retry // on a later mount instead of going dark until tomorrow. if (response.ok) localStorage.setItem(DAY_KEY_PREFIX + today, '1') }).catch(() => { /* unreachable endpoint: stay silent */ }) } catch { /* never break the host UI over telemetry */ } }