Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "0.40.1",
"version": "0.42.0",
"description": "Build and deploy web apps and agents",
"author": {
"name": "Vercel",
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vercel",
"version": "0.40.1",
"version": "0.42.0",
"description": "Build and deploy web apps and agents",
"author": {
"name": "Vercel",
Expand Down
2 changes: 1 addition & 1 deletion .plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vercel-plugin",
"version": "0.40.1",
"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",
Expand Down
36 changes: 33 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:

Expand Down
77 changes: 66 additions & 11 deletions hooks/src/telemetry.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,7 +19,7 @@ export interface TelemetryEvent {
value: string;
}

async function sendDau(events: TelemetryEvent[]): Promise<boolean> {
async function sendTelemetry(events: TelemetryEvent[]): Promise<boolean> {
if (events.length === 0) return false;

const controller = new AbortController();
Expand All @@ -26,6 +30,8 @@ async function sendDau(events: TelemetryEvent[]): Promise<boolean> {
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,
Expand All @@ -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);
}
Expand All @@ -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 {
Expand All @@ -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
// ---------------------------------------------------------------------------
Expand All @@ -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";
Expand All @@ -92,17 +120,44 @@ export function isDauTelemetryEnabled(env: NodeJS.ProcessEnv = process.env): boo
// ---------------------------------------------------------------------------

export async function trackDauActiveToday(now: Date = new Date()): Promise<void> {
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();
}
}
}
68 changes: 58 additions & 10 deletions hooks/telemetry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.42.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);
Expand All @@ -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
Expand All @@ -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);
}
Expand All @@ -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 {
Expand All @@ -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;
Expand All @@ -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
};
7 changes: 6 additions & 1 deletion hooks/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -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")
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vercel-plugin",
"version": "0.40.1",
"version": "0.42.0",
"private": true,
"bin": {
"vercel-plugin": "src/cli/index.ts"
Expand Down
Loading
Loading