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.42.1",
"version": "0.43.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.42.1",
"version": "0.43.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.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",
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
3 changes: 2 additions & 1 deletion hooks/session-start-profiler.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"] },
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion hooks/src/session-start-profiler.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -619,6 +619,7 @@ async function main(): Promise<void> {
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);
Expand Down
49 changes: 47 additions & 2 deletions hooks/src/telemetry.mts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
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";

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;
Expand All @@ -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<boolean> {
if (events.length === 0) return false;

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
// ---------------------------------------------------------------------------
Expand All @@ -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)
// ---------------------------------------------------------------------------
Expand Down
39 changes: 37 additions & 2 deletions hooks/telemetry.mjs
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -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
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.42.1",
"version": "0.43.0",
"private": true,
"bin": {
"vercel-plugin": "src/cli/index.ts"
Expand Down
26 changes: 24 additions & 2 deletions tests/telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ async function runTelemetryProbe(options: {
calls: number;
stampPath: string;
firstUseStampPath: string;
activeSessionMarkerPath: string;
activeSessionMarker: unknown;
dauPayloads: unknown[];
}> {
const mergedEnv: Record<string, string> = {
Expand Down Expand Up @@ -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], {
Expand All @@ -68,6 +77,8 @@ async function runTelemetryProbe(options: {
calls: number;
stampPath: string;
firstUseStampPath: string;
activeSessionMarkerPath: string;
activeSessionMarker: unknown;
dauPayloads: unknown[];
};
}
Expand All @@ -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 () => {
Expand All @@ -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({
Expand All @@ -109,7 +131,7 @@ describe("telemetry controls", () => {
}),
expect.objectContaining({
key: "plugin:version",
value: "0.42.0",
value: "0.43.0",
}),
],
]);
Expand Down
Loading