diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 1709634..1c84de2 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "vercel", - "version": "0.42.1", + "version": "0.43.0", "description": "Build and deploy web apps and agents", "author": { "name": "Vercel", diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index f9b6e9d..4f83adc 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "vercel", - "version": "0.42.1", + "version": "0.43.0", "description": "Build and deploy web apps and agents", "author": { "name": "Vercel", diff --git a/.plugin/plugin.json b/.plugin/plugin.json index d28d14f..efd977f 100644 --- a/.plugin/plugin.json +++ b/.plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "vercel-plugin", - "version": "0.42.1", + "version": "0.43.0", "description": "Comprehensive Vercel ecosystem plugin — relational knowledge graph, skills for every major product, specialized agents, and Vercel conventions. Turns any AI agent into a Vercel expert.", "author": { "name": "Vercel", diff --git a/README.md b/README.md index c27d67f..1aa92d6 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,7 @@ How it is tracked: - `dau-stamp` prevents sending `dau:active_today` more than once per UTC day. - `first-use-stamp` prevents sending `plugin:first_use` more than once. - Stamp files are written only after the telemetry bridge returns a successful response, so failed sends can retry later. +- `active-session.json` is refreshed on session start with the plugin version and expiry timestamp. It lets Vercel CLI telemetry identify commands run while a recent Vercel plugin session marker is present. It contains no prompt text, file paths, project names, account IDs, tool-call contents, or skill-injection details. Behavior: diff --git a/hooks/session-start-profiler.mjs b/hooks/session-start-profiler.mjs index 3f4c7b2..ab637be 100644 --- a/hooks/session-start-profiler.mjs +++ b/hooks/session-start-profiler.mjs @@ -18,7 +18,7 @@ import { pluginRoot, safeReadJson, writeSessionFile } from "./hook-env.mjs"; import { createLogger, logCaughtError } from "./logger.mjs"; import { hasSessionStartActivationMarkers } from "./session-start-activation.mjs"; import { buildSkillMap } from "./skill-map-frontmatter.mjs"; -import { trackDauActiveToday } from "./telemetry.mjs"; +import { refreshActiveSessionMarker, trackDauActiveToday } from "./telemetry.mjs"; var FILE_MARKERS = [ { file: "next.config.js", skills: ["nextjs", "turbopack"] }, { file: "next.config.mjs", skills: ["nextjs", "turbopack"] }, @@ -409,6 +409,7 @@ async function main() { const platform = detectSessionStartPlatform(hookInput); const sessionId = normalizeSessionStartSessionId(hookInput); const projectRoot = resolveSessionStartProjectRoot(); + refreshActiveSessionMarker(); const greenfield = checkGreenfield(projectRoot); const shouldActivate = greenfield !== null || !existsSync(projectRoot) || hasSessionStartActivationMarkers(projectRoot); if (!shouldActivate) { diff --git a/hooks/src/session-start-profiler.mts b/hooks/src/session-start-profiler.mts index de3b9f0..45f7c54 100644 --- a/hooks/src/session-start-profiler.mts +++ b/hooks/src/session-start-profiler.mts @@ -32,7 +32,7 @@ import { pluginRoot, safeReadJson, writeSessionFile } from "./hook-env.mjs"; import { createLogger, logCaughtError, type Logger } from "./logger.mjs"; import { hasSessionStartActivationMarkers } from "./session-start-activation.mjs"; import { buildSkillMap } from "./skill-map-frontmatter.mjs"; -import { trackDauActiveToday } from "./telemetry.mjs"; +import { refreshActiveSessionMarker, trackDauActiveToday } from "./telemetry.mjs"; // --------------------------------------------------------------------------- // Types @@ -619,6 +619,7 @@ async function main(): Promise { const platform = detectSessionStartPlatform(hookInput); const sessionId = normalizeSessionStartSessionId(hookInput); const projectRoot = resolveSessionStartProjectRoot(); + refreshActiveSessionMarker(); // Greenfield check — seed defaults and skip repository exploration. const greenfield: GreenfieldResult | null = checkGreenfield(projectRoot); diff --git a/hooks/src/telemetry.mts b/hooks/src/telemetry.mts index 050fdcb..e451bc1 100644 --- a/hooks/src/telemetry.mts +++ b/hooks/src/telemetry.mts @@ -1,5 +1,5 @@ import { randomUUID } from "node:crypto"; -import { mkdirSync, statSync, writeFileSync } from "node:fs"; +import { mkdirSync, rmSync, statSync, writeFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { homedir } from "node:os"; @@ -7,10 +7,12 @@ declare const __VERCEL_PLUGIN_VERSION__: string; const BRIDGE_ENDPOINT = "https://telemetry.vercel.com/api/vercel-plugin/v1/events"; const FLUSH_TIMEOUT_MS = 3_000; -const PLUGIN_VERSION = __VERCEL_PLUGIN_VERSION__; +export const PLUGIN_VERSION = __VERCEL_PLUGIN_VERSION__; +const ACTIVE_SESSION_TTL_MS = 60 * 60 * 1000; const DAU_STAMP_PATH = join(homedir(), ".config", "vercel-plugin", "dau-stamp"); const FIRST_USE_STAMP_PATH = join(homedir(), ".config", "vercel-plugin", "first-use-stamp"); +const ACTIVE_SESSION_MARKER_PATH = join(homedir(), ".config", "vercel-plugin", "active-session.json"); export interface TelemetryEvent { id: string; @@ -19,6 +21,14 @@ export interface TelemetryEvent { value: string; } +export interface ActiveSessionMarker { + schema: 1; + active: true; + pluginVersion: string; + updatedAt: number; + expiresAt: number; +} + async function sendTelemetry(events: TelemetryEvent[]): Promise { if (events.length === 0) return false; @@ -56,6 +66,10 @@ export function getFirstUseStampPath(): string { return FIRST_USE_STAMP_PATH; } +export function getActiveSessionMarkerPath(): string { + return ACTIVE_SESSION_MARKER_PATH; +} + function utcDayStamp(date: Date): string { return date.toISOString().slice(0, 10); } @@ -97,6 +111,14 @@ export function markFirstUsePingSent(): void { } } +export function removeActiveSessionMarker(): void { + try { + rmSync(ACTIVE_SESSION_MARKER_PATH, { force: true }); + } catch { + // Best-effort + } +} + // --------------------------------------------------------------------------- // Telemetry controls // --------------------------------------------------------------------------- @@ -115,6 +137,29 @@ export function isDauTelemetryEnabled(env: NodeJS.ProcessEnv = process.env): boo return getTelemetryOverride(env) !== "off"; } +export function refreshActiveSessionMarker(now: Date = new Date()): void { + if (!isDauTelemetryEnabled()) { + removeActiveSessionMarker(); + return; + } + + const updatedAt = now.getTime(); + const marker: ActiveSessionMarker = { + schema: 1, + active: true, + pluginVersion: PLUGIN_VERSION, + updatedAt, + expiresAt: updatedAt + ACTIVE_SESSION_TTL_MS, + }; + + try { + mkdirSync(dirname(ACTIVE_SESSION_MARKER_PATH), { recursive: true }); + writeFileSync(ACTIVE_SESSION_MARKER_PATH, `${JSON.stringify(marker)}\n`, { flag: "w" }); + } catch { + // Best-effort + } +} + // --------------------------------------------------------------------------- // DAU telemetry (default-on, opt-out via VERCEL_PLUGIN_TELEMETRY=off) // --------------------------------------------------------------------------- diff --git a/hooks/telemetry.mjs b/hooks/telemetry.mjs index 1804ee4..9a052ff 100644 --- a/hooks/telemetry.mjs +++ b/hooks/telemetry.mjs @@ -1,13 +1,15 @@ // hooks/src/telemetry.mts import { randomUUID } from "crypto"; -import { mkdirSync, statSync, writeFileSync } from "fs"; +import { mkdirSync, rmSync, statSync, writeFileSync } from "fs"; import { join, dirname } from "path"; import { homedir } from "os"; var BRIDGE_ENDPOINT = "https://telemetry.vercel.com/api/vercel-plugin/v1/events"; var FLUSH_TIMEOUT_MS = 3e3; -var PLUGIN_VERSION = "0.42.0"; +var PLUGIN_VERSION = "0.43.0"; +var ACTIVE_SESSION_TTL_MS = 60 * 60 * 1e3; var DAU_STAMP_PATH = join(homedir(), ".config", "vercel-plugin", "dau-stamp"); var FIRST_USE_STAMP_PATH = join(homedir(), ".config", "vercel-plugin", "first-use-stamp"); +var ACTIVE_SESSION_MARKER_PATH = join(homedir(), ".config", "vercel-plugin", "active-session.json"); async function sendTelemetry(events) { if (events.length === 0) return false; const controller = new AbortController(); @@ -37,6 +39,9 @@ function getDauStampPath() { function getFirstUseStampPath() { return FIRST_USE_STAMP_PATH; } +function getActiveSessionMarkerPath() { + return ACTIVE_SESSION_MARKER_PATH; +} function utcDayStamp(date) { return date.toISOString().slice(0, 10); } @@ -71,6 +76,12 @@ function markFirstUsePingSent() { } catch { } } +function removeActiveSessionMarker() { + try { + rmSync(ACTIVE_SESSION_MARKER_PATH, { force: true }); + } catch { + } +} function getTelemetryOverride(env = process.env) { const value = env.VERCEL_PLUGIN_TELEMETRY?.trim().toLowerCase(); if (value === "off") return value; @@ -79,6 +90,26 @@ function getTelemetryOverride(env = process.env) { function isDauTelemetryEnabled(env = process.env) { return getTelemetryOverride(env) !== "off"; } +function refreshActiveSessionMarker(now = /* @__PURE__ */ new Date()) { + if (!isDauTelemetryEnabled()) { + removeActiveSessionMarker(); + return; + } + const updatedAt = now.getTime(); + const marker = { + schema: 1, + active: true, + pluginVersion: PLUGIN_VERSION, + updatedAt, + expiresAt: updatedAt + ACTIVE_SESSION_TTL_MS + }; + try { + mkdirSync(dirname(ACTIVE_SESSION_MARKER_PATH), { recursive: true }); + writeFileSync(ACTIVE_SESSION_MARKER_PATH, `${JSON.stringify(marker)} +`, { flag: "w" }); + } catch { + } +} async function trackDauActiveToday(now = /* @__PURE__ */ new Date()) { if (!isDauTelemetryEnabled()) return; const eventTime = now.getTime(); @@ -116,12 +147,16 @@ async function trackDauActiveToday(now = /* @__PURE__ */ new Date()) { } } export { + PLUGIN_VERSION, + getActiveSessionMarkerPath, getDauStampPath, getFirstUseStampPath, getTelemetryOverride, isDauTelemetryEnabled, markDauPingSent, markFirstUsePingSent, + refreshActiveSessionMarker, + removeActiveSessionMarker, shouldSendDauPing, shouldSendFirstUsePing, trackDauActiveToday diff --git a/package.json b/package.json index a68238b..3e356bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vercel-plugin", - "version": "0.42.1", + "version": "0.43.0", "private": true, "bin": { "vercel-plugin": "src/cli/index.ts" diff --git a/tests/telemetry.test.ts b/tests/telemetry.test.ts index 144a821..7871c37 100644 --- a/tests/telemetry.test.ts +++ b/tests/telemetry.test.ts @@ -16,6 +16,8 @@ async function runTelemetryProbe(options: { calls: number; stampPath: string; firstUseStampPath: string; + activeSessionMarkerPath: string; + activeSessionMarker: unknown; dauPayloads: unknown[]; }> { const mergedEnv: Record = { @@ -46,7 +48,14 @@ async function runTelemetryProbe(options: { const stampPath = telemetry.getDauStampPath(); const firstUseStampPath = telemetry.getFirstUseStampPath(); - console.log(JSON.stringify({ dauEnabled, calls, stampPath, firstUseStampPath, dauPayloads })); + const activeSessionMarkerPath = telemetry.getActiveSessionMarkerPath(); + telemetry.refreshActiveSessionMarker(new Date("2026-05-15T12:00:00.000Z")); + const activeSessionMarker = await import("node:fs").then((fs) => + fs.existsSync(activeSessionMarkerPath) + ? JSON.parse(fs.readFileSync(activeSessionMarkerPath, "utf-8")) + : null + ); + console.log(JSON.stringify({ dauEnabled, calls, stampPath, firstUseStampPath, activeSessionMarkerPath, activeSessionMarker, dauPayloads })); `; const proc = Bun.spawn([NODE_BIN, "--input-type=module", "-e", script], { @@ -68,6 +77,8 @@ async function runTelemetryProbe(options: { calls: number; stampPath: string; firstUseStampPath: string; + activeSessionMarkerPath: string; + activeSessionMarker: unknown; dauPayloads: unknown[]; }; } @@ -87,6 +98,8 @@ describe("telemetry controls", () => { expect(result.calls).toBe(0); expect(existsSync(result.stampPath)).toBe(false); expect(existsSync(result.firstUseStampPath)).toBe(false); + expect(existsSync(result.activeSessionMarkerPath)).toBe(false); + expect(result.activeSessionMarker).toBeNull(); }); test("default telemetry sends DAU and first-use once", async () => { @@ -95,8 +108,17 @@ describe("telemetry controls", () => { expect(result.calls).toBe(1); expect(result.stampPath).toBe(join(tempHome, ".config", "vercel-plugin", "dau-stamp")); expect(result.firstUseStampPath).toBe(join(tempHome, ".config", "vercel-plugin", "first-use-stamp")); + expect(result.activeSessionMarkerPath).toBe(join(tempHome, ".config", "vercel-plugin", "active-session.json")); expect(existsSync(result.stampPath)).toBe(true); expect(existsSync(result.firstUseStampPath)).toBe(true); + expect(existsSync(result.activeSessionMarkerPath)).toBe(true); + expect(result.activeSessionMarker).toEqual({ + schema: 1, + active: true, + pluginVersion: "0.43.0", + updatedAt: Date.parse("2026-05-15T12:00:00.000Z"), + expiresAt: Date.parse("2026-05-15T13:00:00.000Z"), + }); expect(result.dauPayloads).toEqual([ [ expect.objectContaining({ @@ -109,7 +131,7 @@ describe("telemetry controls", () => { }), expect.objectContaining({ key: "plugin:version", - value: "0.42.0", + value: "0.43.0", }), ], ]);