import { randomBytes } from "node:crypto"; import { isCI } from "ci-info"; import { GlobalConfig } from "./config.js"; import * as KEY from "./config-keys.js"; import { post } from "./post.js"; import { getProjectInfo } from "./project-info.js"; import { getSystemInfo } from "./system-info.js"; const VALID_TELEMETRY_NOTICE_DATE = "2023-08-25"; function getDebug() { return globalThis._astroGlobalDebug; } class AstroTelemetry { opts; _anonymousSessionId; _anonymousProjectInfo; config = new GlobalConfig({ name: "astro" }); isCI = isCI; env = process.env; get astroVersion() { return this.opts.astroVersion; } get viteVersion() { return this.opts.viteVersion; } get ASTRO_TELEMETRY_DISABLED() { return this.env.ASTRO_TELEMETRY_DISABLED; } get TELEMETRY_DISABLED() { return this.env.TELEMETRY_DISABLED; } constructor(opts) { this.opts = opts; } /** * Get value from either the global config or the provided fallback. * If value is not set, the fallback is saved to the global config, * persisted for later sessions. */ getConfigWithFallback(key, getValue) { const currentValue = this.config.get(key); if (currentValue !== void 0) { return currentValue; } const newValue = getValue(); this.config.set(key, newValue); return newValue; } get enabled() { return this.getConfigWithFallback(KEY.TELEMETRY_ENABLED, () => true); } get notifyDate() { return this.getConfigWithFallback(KEY.TELEMETRY_NOTIFY_DATE, () => ""); } get anonymousId() { return this.getConfigWithFallback(KEY.TELEMETRY_ID, () => randomBytes(32).toString("hex")); } get anonymousSessionId() { this._anonymousSessionId = this._anonymousSessionId || randomBytes(32).toString("hex"); return this._anonymousSessionId; } get anonymousProjectInfo() { this._anonymousProjectInfo = this._anonymousProjectInfo || getProjectInfo(this.isCI); return this._anonymousProjectInfo; } get isDisabled() { if (Boolean(this.ASTRO_TELEMETRY_DISABLED || this.TELEMETRY_DISABLED)) { return true; } return this.enabled === false; } setEnabled(value) { this.config.set(KEY.TELEMETRY_ENABLED, value); } clear() { return this.config.clear(); } isValidNotice() { if (!this.notifyDate) return false; const current = Number(this.notifyDate); const valid = new Date(VALID_TELEMETRY_NOTICE_DATE).valueOf(); return current > valid; } async notify(callback) { const debug = getDebug(); if (this.isDisabled || this.isCI) { debug?.("telemetry", `[notify] telemetry has been disabled`); return; } if (this.isValidNotice()) { debug?.("telemetry", `[notify] last notified on ${this.notifyDate}`); return; } const enabled = await callback(); this.config.set(KEY.TELEMETRY_NOTIFY_DATE, (/* @__PURE__ */ new Date()).valueOf().toString()); this.config.set(KEY.TELEMETRY_ENABLED, enabled); debug?.("telemetry", `[notify] telemetry has been ${enabled ? "enabled" : "disabled"}`); } async record(event = []) { const events = Array.isArray(event) ? event : [event]; if (events.length < 1) { return Promise.resolve(); } const debug = getDebug(); if (this.isDisabled) { debug?.("telemetry", "[record] telemetry has been disabled"); return Promise.resolve(); } const meta = { ...getSystemInfo({ astroVersion: this.astroVersion, viteVersion: this.viteVersion }) }; const context = { ...this.anonymousProjectInfo, anonymousId: this.anonymousId, anonymousSessionId: this.anonymousSessionId }; if (meta.isCI) { context.anonymousId = `CI.${meta.ciName || "UNKNOWN"}`; } const debugOutput = process.env.DEBUG?.includes("astro:telemetry") || process.env.DEBUG?.includes("astro:*") || process.env.DEBUG === "*"; if (debugOutput && debug) { debug("telemetry", { context, meta }); debug("telemetry", JSON.stringify(events, null, 2)); return Promise.resolve(); } return post({ context, meta, events }).catch((err) => { debug?.("telemetry", `Error sending event: ${err.message}`); }); } } export { AstroTelemetry };