mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-12 07:09:59 +00:00
feat(telemetry): fingerprint sandbox runtime and agent vendor
Add two new properties to every CLI telemetry event so we can tell
managed-sandbox traffic (Codex Cloud, Claude Code Web, etc.) apart from
real developer laptops without geolocation guesswork:
- sandbox_runtime: 'gvisor' | 'firecracker' | 'docker' | 'kvm' | 'wsl' | null
gVisor detected via kernel string ('4.19.0-gvisor' or legacy Sentry
'4.4.0') + /proc/version. Firecracker via /dev/vsock + DMI sys_vendor.
Docker reuses the existing /.dockerenv + cgroup probe.
- agent_runtime: claude_code | codex | cursor | copilot_agent | jules
| replit | devin | aider | gemini_cli | hermes | openclaw | null
Detected by the EXISTENCE of well-known vendor env vars only — values
are never read. Hermes rule keys on HERMES_QUIET=1 (set unconditionally
at hermes-agent/cli.py:50). openclaw rule keys on OPENCLAW_STATE_DIR
or OPENCLAW_CONFIG_PATH (set explicitly in the spawned child env at
openclaw/extensions/qa-matrix/src/runners/contract/scenario-runtime-cli.ts).
Drive-by cleanups required by fallow because system.ts and client.ts
fall into the audit scope of this PR:
- Extract detectWSL into platform.ts to break the system.ts ↔ agent_runtime.ts cycle.
- Refactor detectCI / getCIName into a single CI_PROVIDERS table.
- Dedupe flush / flushSync via a shared drainQueueToPayload helper.
Privacy posture unchanged: HYPERFRAMES_NO_TELEMETRY=1 still opts out;
disclosure in docs/packages/cli.mdx updated to enumerate the new fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
James Russo
co-authored by
Claude Opus 4.7
parent
e9c515dedd
commit
0c6012a2ec
@@ -17,7 +17,7 @@ const FLUSH_TIMEOUT_MS = 5_000;
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface EventProperties {
|
||||
[key: string]: string | number | boolean | undefined;
|
||||
[key: string]: string | number | boolean | null | undefined;
|
||||
}
|
||||
|
||||
let eventQueue: Array<{
|
||||
@@ -81,30 +81,41 @@ export function trackEvent(event: string, properties: EventProperties = {}): voi
|
||||
ci_name: sys.ci_name ?? undefined,
|
||||
is_wsl: sys.is_wsl,
|
||||
is_tty: sys.is_tty,
|
||||
sandbox_runtime: sys.sandbox_runtime ?? undefined,
|
||||
agent_runtime: sys.agent_runtime ?? undefined,
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Drain the in-memory queue into a PostHog `/batch/` payload string.
|
||||
* Returns null when there's nothing to send. Resets the queue as a side effect
|
||||
* so callers can fire-and-forget the resulting payload.
|
||||
*
|
||||
* $ip:null tells PostHog not to record the request IP for any of these events.
|
||||
* Server-side "Discard client IP data" is also enabled in project settings.
|
||||
*/
|
||||
function drainQueueToPayload(): string | null {
|
||||
if (eventQueue.length === 0) return null;
|
||||
const config = readConfig();
|
||||
const batch = eventQueue.map((e) => ({
|
||||
event: e.event,
|
||||
properties: { ...e.properties, $ip: null },
|
||||
distinct_id: config.anonymousId,
|
||||
timestamp: e.timestamp,
|
||||
}));
|
||||
eventQueue = [];
|
||||
return JSON.stringify({ api_key: POSTHOG_API_KEY, batch });
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush all queued events to PostHog via async HTTP POST.
|
||||
* Called before normal process exit via `beforeExit`.
|
||||
*/
|
||||
export async function flush(): Promise<void> {
|
||||
if (eventQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = readConfig();
|
||||
const batch = eventQueue.map((e) => ({
|
||||
event: e.event,
|
||||
// $ip: null tells PostHog to not record the request IP for this event.
|
||||
// Server-side "Discard client IP data" is also enabled in project settings.
|
||||
properties: { ...e.properties, $ip: null },
|
||||
distinct_id: config.anonymousId,
|
||||
timestamp: e.timestamp,
|
||||
}));
|
||||
eventQueue = [];
|
||||
const payload = drainQueueToPayload();
|
||||
if (payload == null) return;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS);
|
||||
@@ -113,7 +124,7 @@ export async function flush(): Promise<void> {
|
||||
await fetch(`${POSTHOG_HOST}/batch/`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json", Connection: "close" },
|
||||
body: JSON.stringify({ api_key: POSTHOG_API_KEY, batch }),
|
||||
body: payload,
|
||||
signal: controller.signal,
|
||||
});
|
||||
} catch {
|
||||
@@ -129,20 +140,8 @@ export async function flush(): Promise<void> {
|
||||
* so the parent process exits immediately without waiting.
|
||||
*/
|
||||
export function flushSync(): void {
|
||||
if (eventQueue.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = readConfig();
|
||||
const batch = eventQueue.map((e) => ({
|
||||
event: e.event,
|
||||
properties: { ...e.properties, $ip: null },
|
||||
distinct_id: config.anonymousId,
|
||||
timestamp: e.timestamp,
|
||||
}));
|
||||
eventQueue = [];
|
||||
|
||||
const payload = JSON.stringify({ api_key: POSTHOG_API_KEY, batch });
|
||||
const payload = drainQueueToPayload();
|
||||
if (payload == null) return;
|
||||
|
||||
try {
|
||||
const { spawn } = require("node:child_process") as typeof import("node:child_process");
|
||||
|
||||
Reference in New Issue
Block a user