UNPKG

browse

Version:

Unified Browserbase CLI for browser automation and cloud APIs.

281 lines (280 loc) 9.53 kB
import { randomUUID } from "node:crypto"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; const browserbaseTelemetrySource = "cli"; const browserbaseTelemetryHost = "https://us.i.posthog.com"; const browserbaseTelemetryTimeoutMs = 400; const browserbaseTelemetryProjectToken = "phc_CKQBSpdeU2GGyBgcBhW8ZbDnhEVbZbuzMsqhMb9YRs5x"; let telemetryState; let telemetryClient; let telemetryClientVersion; export function startTelemetryInvocation(startedAtMs = Date.now()) { telemetryState = { startedAtMs }; } export function captureCommandInvoked(CommandClass, cliVersion) { const state = getTelemetryState(); const command = createCommandInvocation(CommandClass, state.startedAtMs); state.command = command; state.pendingInvokedCapture = getCliTelemetry(cliVersion) .capture("cli.command_invoked", commandInvokedProperties(command)) .catch(() => { // Best-effort telemetry should never affect CLI behavior. }); } export function recordCommandError(type, code) { const state = getTelemetryState(); state.recordedError = { type, code }; } export async function captureCommandCompleted(cliVersion, error) { const state = telemetryState; const command = state?.command; if (!state || !command) { return; } const exitCode = resolveExitCode(error); const success = exitCode === 0; const completionCapture = getCliTelemetry(cliVersion) .capture("cli.command_completed", commandCompletedProperties(command, { error, exitCode, recordedError: state.recordedError, success, })) .catch(() => { // Best-effort telemetry should never affect CLI behavior. }); await Promise.allSettled([ state.pendingInvokedCapture ?? Promise.resolve(), completionCapture, ]); } function createCliTelemetry(options) { const env = options.env ?? process.env; const transport = resolveTransportConfig(env); const telemetryEnabled = !isTelemetryDisabled(env); const distinctIdPromise = telemetryEnabled ? resolveAnonymousInstallId(env, options.sessionId) : Promise.resolve(""); const baseProperties = { source: browserbaseTelemetrySource, cli_version: options.cliVersion, node_version: process.version, platform: process.platform, arch: process.arch, $process_person_profile: false, }; return { async capture(event, properties) { if (!telemetryEnabled) { return; } const distinctId = await distinctIdPromise; await posthogCapture(transport, { api_key: transport.projectToken, distinct_id: distinctId, event, timestamp: new Date().toISOString(), properties: { ...baseProperties, ...properties, }, }); }, }; } function getCliTelemetry(cliVersion) { if (!telemetryClient || telemetryClientVersion !== cliVersion) { telemetryClient = createCliTelemetry({ cliVersion }); telemetryClientVersion = cliVersion; } return telemetryClient; } function getTelemetryState() { telemetryState ??= { startedAtMs: Date.now() }; return telemetryState; } function createCommandInvocation(CommandClass, startedAtMs) { const commandPath = resolveCommandPath(CommandClass.id); const pathTokens = commandPath.split(".").filter(Boolean); return { commandPath, topLevelCommand: pathTokens[0] ?? CommandClass.id, leafCommand: pathTokens.at(-1) ?? CommandClass.id, startedAtMs, }; } function commandInvokedProperties(invocation) { return { command_path: invocation.commandPath, top_level_command: invocation.topLevelCommand, leaf_command: invocation.leafCommand, command_depth: invocation.commandPath.split(".").filter(Boolean).length, }; } function commandCompletedProperties(invocation, completion) { const durationMs = Date.now() - invocation.startedAtMs; const errorType = completion.success ? null : (completion.recordedError?.type ?? inferErrorType(completion.error)); return { command_path: invocation.commandPath, top_level_command: invocation.topLevelCommand, leaf_command: invocation.leafCommand, command_depth: invocation.commandPath.split(".").filter(Boolean).length, duration_ms: Math.max(0, durationMs), exit_code: completion.exitCode, success: completion.success, error_type: errorType, error_code: completion.success ? null : (completion.recordedError?.code ?? inferErrorCode(completion.error)), }; } function resolveCommandPath(commandId) { return commandId.split(":").filter(Boolean).join("."); } function resolveExitCode(error) { if (error) { const oclifExit = error.oclif?.exit; if (typeof oclifExit === "number") { return oclifExit; } const exitCode = error.exitCode; if (typeof exitCode === "number") { return exitCode; } return typeof process.exitCode === "number" ? process.exitCode : 1; } return typeof process.exitCode === "number" ? process.exitCode : 0; } function inferErrorType(error) { if (!error) { return null; } const oclifExit = error.oclif?.exit; return typeof oclifExit === "number" ? "oclif" : "runtime"; } function inferErrorCode(error) { if (!error) { return null; } const code = error.code; if (typeof code === "string" && code.trim()) { return sanitizeErrorCode(code); } return sanitizeErrorCode(error.name || "Error"); } function sanitizeErrorCode(value) { return value.replaceAll(/[^A-Za-z0-9_.:-]/g, "_").slice(0, 80) || "Error"; } function resolveTransportConfig(env) { const host = normalizeHost(env.BROWSERBASE_TELEMETRY_HOST ?? browserbaseTelemetryHost); const timeoutMs = parseTimeoutMs(env.BROWSERBASE_TELEMETRY_TIMEOUT_MS); return { host, timeoutMs, projectToken: browserbaseTelemetryProjectToken, }; } function parseTimeoutMs(value) { if (!value) { return browserbaseTelemetryTimeoutMs; } const parsed = Number.parseInt(value, 10); if (!Number.isFinite(parsed) || parsed <= 0) { return browserbaseTelemetryTimeoutMs; } return parsed; } function normalizeHost(host) { return host.endsWith("/") ? host.slice(0, -1) : host; } async function resolveAnonymousInstallId(env, fallbackId) { const installIdPath = resolveInstallIdPath(env); try { const existing = (await readFile(installIdPath, "utf8")).trim(); if (existing) { return existing; } } catch { // Fall through and create a new anonymous install ID. } const installId = fallbackId ?? randomUUID(); try { await mkdir(dirname(installIdPath), { recursive: true }); await writeFile(installIdPath, `${installId}\n`, "utf8"); } catch { // If persistence fails, continue with an in-memory anonymous ID. } return installId; } function resolveInstallIdPath(env) { const overriddenPath = env.BROWSERBASE_TELEMETRY_INSTALL_ID_FILE; if (overriddenPath) { return overriddenPath; } if (process.platform === "win32") { const baseDir = env.APPDATA ?? env.LOCALAPPDATA ?? join(homedir(), "AppData", "Roaming"); return join(baseDir, "Browserbase", "cli", "telemetry-id"); } if (process.platform === "darwin") { return join(homedir(), "Library", "Application Support", "Browserbase", "cli", "telemetry-id"); } const baseDir = env.XDG_CONFIG_HOME ?? join(homedir(), ".config"); return join(baseDir, "browserbase", "cli", "telemetry-id"); } function isTelemetryDisabled(env) { return (envFlagEnabled(env.DO_NOT_TRACK) || envFlagEnabled(env.BROWSERBASE_TELEMETRY_DISABLED) || isCiEnvironment(env) || isUnconfiguredTestEnvironment(env)); } function isCiEnvironment(env) { const value = env.CI; if (!value) { return false; } return !isExplicitFalse(value); } function isUnconfiguredTestEnvironment(env) { return env.NODE_ENV === "test" && !env.BROWSERBASE_TELEMETRY_HOST; } function envFlagEnabled(value) { if (!value) { return false; } return !isExplicitFalse(value); } function isExplicitFalse(value) { const normalized = value.trim().toLowerCase(); return (normalized === "" || normalized === "0" || normalized === "false" || normalized === "no" || normalized === "off"); } async function posthogCapture(transport, payload) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), transport.timeoutMs); timeout.unref?.(); const endpoint = `${transport.host}/i/v0/e/`; try { await fetch(endpoint, { method: "POST", signal: controller.signal, headers: { "content-type": "application/json", }, body: JSON.stringify(payload), }); } catch { // Best-effort telemetry should never affect CLI behavior. } finally { clearTimeout(timeout); } }