From a2c094f03618cb2585c3e4dfaffb61fe2cdffd4b Mon Sep 17 00:00:00 2001 From: melkeydev Date: Tue, 28 Apr 2026 11:34:42 -0700 Subject: [PATCH 1/2] Adding back telemetry for vercel plugin --- .claude-plugin/plugin.json | 2 +- .cursor-plugin/plugin.json | 2 +- .plugin/plugin.json | 2 +- README.md | 36 ++++++++++++++++-- hooks/src/telemetry.mts | 77 ++++++++++++++++++++++++++++++++------ hooks/telemetry.mjs | 68 ++++++++++++++++++++++++++++----- hooks/tsup.config.ts | 7 +++- package.json | 2 +- tests/telemetry.test.ts | 18 ++++++++- 9 files changed, 183 insertions(+), 31 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 102d5a9..440b8ba 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "vercel", - "version": "0.40.0", + "version": "0.41.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 d33627c..b16b114 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "vercel", - "version": "0.40.0", + "version": "0.41.0", "description": "Build and deploy web apps and agents", "author": { "name": "Vercel", diff --git a/.plugin/plugin.json b/.plugin/plugin.json index 19330e4..d6949e4 100644 --- a/.plugin/plugin.json +++ b/.plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "vercel-plugin", - "version": "0.40.0", + "version": "0.41.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 ae824c9..c27d67f 100644 --- a/README.md +++ b/README.md @@ -109,12 +109,42 @@ After installing, session context is injected automatically only for empty direc ## Telemetry -Prompt text and bash/tool-call telemetry are not collected. +Telemetry is on by default and can be disabled with `VERCEL_PLUGIN_TELEMETRY=off`. + +What is collected: + +- `dau:active_today`: sent at most once per UTC day when the plugin runs. +- `plugin:first_use`: sent once per local user profile the first time the plugin successfully reports telemetry. +- `plugin:version`: sent with telemetry batches so usage can be grouped by plugin version. + +Each telemetry event contains only: + +- `id`: a random event UUID. +- `event_time`: the event timestamp. +- `key`: one of the event names listed above. +- `value`: currently `"1"`. + +The request also sends HTTP headers used by the telemetry bridge: + +- `x-vercel-plugin-topic-id: dau` +- `x-vercel-plugin-session-id`: a random UUID generated for that telemetry request. +- `x-vercel-plugin-version`: the plugin version embedded at build time. + +Prompt text, bash commands, tool-call contents, file paths, project names, account IDs, and skill-injection details are not collected. + +How it is tracked: + +- Events are sent to Vercel's public telemetry bridge at `https://telemetry.vercel.com/api/vercel-plugin/v1/events`. +- The bridge only forwards events from plugin versions `0.40.0` and newer. +- Local throttle files are stored under `~/.config/vercel-plugin/`: + - `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. Behavior: -- Unset `VERCEL_PLUGIN_TELEMETRY`: default DAU-only telemetry. Sends a once-per-day `dau:active_today` phone-home. -- `VERCEL_PLUGIN_TELEMETRY=off`: disables all telemetry, including the default DAU-only session-start event. +- Unset `VERCEL_PLUGIN_TELEMETRY`: telemetry is enabled. +- `VERCEL_PLUGIN_TELEMETRY=off`: disables all telemetry, including `dau:active_today` and `plugin:first_use`. Where to set `VERCEL_PLUGIN_TELEMETRY`: diff --git a/hooks/src/telemetry.mts b/hooks/src/telemetry.mts index f1c42bd..050fdcb 100644 --- a/hooks/src/telemetry.mts +++ b/hooks/src/telemetry.mts @@ -3,10 +3,14 @@ import { mkdirSync, statSync, writeFileSync } from "node:fs"; import { join, dirname } from "node:path"; import { homedir } from "node:os"; +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__; const DAU_STAMP_PATH = join(homedir(), ".config", "vercel-plugin", "dau-stamp"); +const FIRST_USE_STAMP_PATH = join(homedir(), ".config", "vercel-plugin", "first-use-stamp"); export interface TelemetryEvent { id: string; @@ -15,7 +19,7 @@ export interface TelemetryEvent { value: string; } -async function sendDau(events: TelemetryEvent[]): Promise { +async function sendTelemetry(events: TelemetryEvent[]): Promise { if (events.length === 0) return false; const controller = new AbortController(); @@ -26,6 +30,8 @@ async function sendDau(events: TelemetryEvent[]): Promise { headers: { "Content-Type": "application/json", "x-vercel-plugin-topic-id": "dau", + "x-vercel-plugin-session-id": randomUUID(), + "x-vercel-plugin-version": PLUGIN_VERSION, }, body: JSON.stringify(events), signal: controller.signal, @@ -46,6 +52,10 @@ export function getDauStampPath(): string { return DAU_STAMP_PATH; } +export function getFirstUseStampPath(): string { + return FIRST_USE_STAMP_PATH; +} + function utcDayStamp(date: Date): string { return date.toISOString().slice(0, 10); } @@ -59,6 +69,15 @@ export function shouldSendDauPing(now: Date = new Date()): boolean { } } +export function shouldSendFirstUsePing(): boolean { + try { + statSync(FIRST_USE_STAMP_PATH); + return false; + } catch { + return true; + } +} + export function markDauPingSent(now: Date = new Date()): void { void now; try { @@ -69,6 +88,15 @@ export function markDauPingSent(now: Date = new Date()): void { } } +export function markFirstUsePingSent(): void { + try { + mkdirSync(dirname(FIRST_USE_STAMP_PATH), { recursive: true }); + writeFileSync(FIRST_USE_STAMP_PATH, "", { flag: "w" }); + } catch { + // Best-effort + } +} + // --------------------------------------------------------------------------- // Telemetry controls // --------------------------------------------------------------------------- @@ -80,8 +108,8 @@ export function getTelemetryOverride(env: NodeJS.ProcessEnv = process.env): "off } /** - * DAU telemetry is enabled by default, but users can disable all telemetry with - * VERCEL_PLUGIN_TELEMETRY=off. + * Plugin telemetry is enabled by default, but users can disable all telemetry + * with VERCEL_PLUGIN_TELEMETRY=off. */ export function isDauTelemetryEnabled(env: NodeJS.ProcessEnv = process.env): boolean { return getTelemetryOverride(env) !== "off"; @@ -92,17 +120,44 @@ export function isDauTelemetryEnabled(env: NodeJS.ProcessEnv = process.env): boo // --------------------------------------------------------------------------- export async function trackDauActiveToday(now: Date = new Date()): Promise { - if (!isDauTelemetryEnabled() || !shouldSendDauPing(now)) return; + if (!isDauTelemetryEnabled()) return; const eventTime = now.getTime(); - const sent = await sendDau([{ - id: randomUUID(), - event_time: eventTime, - key: "dau:active_today", - value: "1", - }]); + const events: TelemetryEvent[] = []; + + if (shouldSendDauPing(now)) { + events.push({ + id: randomUUID(), + event_time: eventTime, + key: "dau:active_today", + value: "1", + }); + } + + if (shouldSendFirstUsePing()) { + events.push({ + id: randomUUID(), + event_time: eventTime, + key: "plugin:first_use", + value: "1", + }); + } + + if (events.length > 0) { + events.push({ + id: randomUUID(), + event_time: eventTime, + key: "plugin:version", + value: PLUGIN_VERSION, + }); + } + + const sent = await sendTelemetry(events); if (sent) { - markDauPingSent(now); + for (const event of events) { + if (event.key === "dau:active_today") markDauPingSent(now); + if (event.key === "plugin:first_use") markFirstUsePingSent(); + } } } diff --git a/hooks/telemetry.mjs b/hooks/telemetry.mjs index 344558a..3242443 100644 --- a/hooks/telemetry.mjs +++ b/hooks/telemetry.mjs @@ -5,8 +5,10 @@ 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.41.0"; var DAU_STAMP_PATH = join(homedir(), ".config", "vercel-plugin", "dau-stamp"); -async function sendDau(events) { +var FIRST_USE_STAMP_PATH = join(homedir(), ".config", "vercel-plugin", "first-use-stamp"); +async function sendTelemetry(events) { if (events.length === 0) return false; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), FLUSH_TIMEOUT_MS); @@ -15,7 +17,9 @@ async function sendDau(events) { method: "POST", headers: { "Content-Type": "application/json", - "x-vercel-plugin-topic-id": "dau" + "x-vercel-plugin-topic-id": "dau", + "x-vercel-plugin-session-id": randomUUID(), + "x-vercel-plugin-version": PLUGIN_VERSION }, body: JSON.stringify(events), signal: controller.signal @@ -30,6 +34,9 @@ async function sendDau(events) { function getDauStampPath() { return DAU_STAMP_PATH; } +function getFirstUseStampPath() { + return FIRST_USE_STAMP_PATH; +} function utcDayStamp(date) { return date.toISOString().slice(0, 10); } @@ -41,6 +48,14 @@ function shouldSendDauPing(now = /* @__PURE__ */ new Date()) { return true; } } +function shouldSendFirstUsePing() { + try { + statSync(FIRST_USE_STAMP_PATH); + return false; + } catch { + return true; + } +} function markDauPingSent(now = /* @__PURE__ */ new Date()) { void now; try { @@ -49,6 +64,13 @@ function markDauPingSent(now = /* @__PURE__ */ new Date()) { } catch { } } +function markFirstUsePingSent() { + try { + mkdirSync(dirname(FIRST_USE_STAMP_PATH), { recursive: true }); + writeFileSync(FIRST_USE_STAMP_PATH, "", { flag: "w" }); + } catch { + } +} function getTelemetryOverride(env = process.env) { const value = env.VERCEL_PLUGIN_TELEMETRY?.trim().toLowerCase(); if (value === "off") return value; @@ -58,23 +80,49 @@ function isDauTelemetryEnabled(env = process.env) { return getTelemetryOverride(env) !== "off"; } async function trackDauActiveToday(now = /* @__PURE__ */ new Date()) { - if (!isDauTelemetryEnabled() || !shouldSendDauPing(now)) return; + if (!isDauTelemetryEnabled()) return; const eventTime = now.getTime(); - const sent = await sendDau([{ - id: randomUUID(), - event_time: eventTime, - key: "dau:active_today", - value: "1" - }]); + const events = []; + if (shouldSendDauPing(now)) { + events.push({ + id: randomUUID(), + event_time: eventTime, + key: "dau:active_today", + value: "1" + }); + } + if (shouldSendFirstUsePing()) { + events.push({ + id: randomUUID(), + event_time: eventTime, + key: "plugin:first_use", + value: "1" + }); + } + if (events.length > 0) { + events.push({ + id: randomUUID(), + event_time: eventTime, + key: "plugin:version", + value: PLUGIN_VERSION + }); + } + const sent = await sendTelemetry(events); if (sent) { - markDauPingSent(now); + for (const event of events) { + if (event.key === "dau:active_today") markDauPingSent(now); + if (event.key === "plugin:first_use") markFirstUsePingSent(); + } } } export { getDauStampPath, + getFirstUseStampPath, getTelemetryOverride, isDauTelemetryEnabled, markDauPingSent, + markFirstUsePingSent, shouldSendDauPing, + shouldSendFirstUsePing, trackDauActiveToday }; diff --git a/hooks/tsup.config.ts b/hooks/tsup.config.ts index 7b99df2..f27819f 100644 --- a/hooks/tsup.config.ts +++ b/hooks/tsup.config.ts @@ -1,5 +1,7 @@ import { defineConfig } from "tsup"; -import { readdirSync } from "node:fs"; +import { readFileSync, readdirSync } from "node:fs"; + +const packageJson = JSON.parse(readFileSync("package.json", "utf-8")) as { version: string }; // Build each .mts source file as a separate .mjs output (no bundling) const discoveredEntries = readdirSync("hooks/src") @@ -36,6 +38,9 @@ export default defineConfig({ dts: false, clean: false, // don't wipe hooks/ — it has hooks.json, src/, etc. target: "node20", + define: { + __VERCEL_PLUGIN_VERSION__: JSON.stringify(packageJson.version), + }, esbuildPlugins: [ { name: "externalize-sibling-hooks", diff --git a/package.json b/package.json index a6389ef..d2846b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vercel-plugin", - "version": "0.40.0", + "version": "0.41.0", "private": true, "bin": { "vercel-plugin": "src/cli/index.ts" diff --git a/tests/telemetry.test.ts b/tests/telemetry.test.ts index bec70a9..15f3106 100644 --- a/tests/telemetry.test.ts +++ b/tests/telemetry.test.ts @@ -15,6 +15,7 @@ async function runTelemetryProbe(options: { dauEnabled: boolean; calls: number; stampPath: string; + firstUseStampPath: string; dauPayloads: unknown[]; }> { const mergedEnv: Record = { @@ -44,7 +45,8 @@ async function runTelemetryProbe(options: { await telemetry.trackDauActiveToday(); const stampPath = telemetry.getDauStampPath(); - console.log(JSON.stringify({ dauEnabled, calls, stampPath, dauPayloads })); + const firstUseStampPath = telemetry.getFirstUseStampPath(); + console.log(JSON.stringify({ dauEnabled, calls, stampPath, firstUseStampPath, dauPayloads })); `; const proc = Bun.spawn([NODE_BIN, "--input-type=module", "-e", script], { @@ -65,6 +67,7 @@ async function runTelemetryProbe(options: { dauEnabled: boolean; calls: number; stampPath: string; + firstUseStampPath: string; dauPayloads: unknown[]; }; } @@ -83,20 +86,31 @@ describe("telemetry controls", () => { expect(result.dauEnabled).toBe(false); expect(result.calls).toBe(0); expect(existsSync(result.stampPath)).toBe(false); + expect(existsSync(result.firstUseStampPath)).toBe(false); }); - test("default telemetry is DAU-only", async () => { + test("default telemetry sends DAU and first-use once", async () => { const result = await runTelemetryProbe({}); expect(result.dauEnabled).toBe(true); 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(existsSync(result.stampPath)).toBe(true); + expect(existsSync(result.firstUseStampPath)).toBe(true); expect(result.dauPayloads).toEqual([ [ expect.objectContaining({ key: "dau:active_today", value: "1", }), + expect.objectContaining({ + key: "plugin:first_use", + value: "1", + }), + expect.objectContaining({ + key: "plugin:version", + value: "0.41.0", + }), ], ]); }); From 16aaf2d68d262b82f610a55386f84499d5e18de0 Mon Sep 17 00:00:00 2001 From: melkeydev Date: Thu, 30 Apr 2026 11:14:18 -0700 Subject: [PATCH 2/2] upticking version number --- .claude-plugin/plugin.json | 2 +- .cursor-plugin/plugin.json | 2 +- .plugin/plugin.json | 2 +- hooks/telemetry.mjs | 2 +- package.json | 2 +- tests/telemetry.test.ts | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 440b8ba..9268daa 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "vercel", - "version": "0.41.0", + "version": "0.42.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 b16b114..57ae3ba 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "vercel", - "version": "0.41.0", + "version": "0.42.0", "description": "Build and deploy web apps and agents", "author": { "name": "Vercel", diff --git a/.plugin/plugin.json b/.plugin/plugin.json index d6949e4..4b18db1 100644 --- a/.plugin/plugin.json +++ b/.plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "vercel-plugin", - "version": "0.41.0", + "version": "0.42.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/hooks/telemetry.mjs b/hooks/telemetry.mjs index 3242443..1804ee4 100644 --- a/hooks/telemetry.mjs +++ b/hooks/telemetry.mjs @@ -5,7 +5,7 @@ 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.41.0"; +var PLUGIN_VERSION = "0.42.0"; var DAU_STAMP_PATH = join(homedir(), ".config", "vercel-plugin", "dau-stamp"); var FIRST_USE_STAMP_PATH = join(homedir(), ".config", "vercel-plugin", "first-use-stamp"); async function sendTelemetry(events) { diff --git a/package.json b/package.json index d2846b8..670345e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vercel-plugin", - "version": "0.41.0", + "version": "0.42.0", "private": true, "bin": { "vercel-plugin": "src/cli/index.ts" diff --git a/tests/telemetry.test.ts b/tests/telemetry.test.ts index 15f3106..144a821 100644 --- a/tests/telemetry.test.ts +++ b/tests/telemetry.test.ts @@ -109,7 +109,7 @@ describe("telemetry controls", () => { }), expect.objectContaining({ key: "plugin:version", - value: "0.41.0", + value: "0.42.0", }), ], ]);