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 .plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vercel-plugin",
"version": "0.32.0",
"version": "0.32.1",
"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 Labs",
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,14 @@ After installing, skills and context are injected automatically. You can also in

The plugin has two separate telemetry controls:

- `~/.claude/vercel-plugin-telemetry-preference` controls prompt text only.
- `~/.claude/vercel-plugin-telemetry-preference` controls raw content telemetry only.
- `VERCEL_PLUGIN_TELEMETRY=off` disables all telemetry.

Behavior:

- `echo 'enabled' > ~/.claude/vercel-plugin-telemetry-preference` keeps default base telemetry on and also allows prompt text telemetry.
- `echo 'disabled' > ~/.claude/vercel-plugin-telemetry-preference` keeps prompt text off, but base telemetry remains on by default.
- `VERCEL_PLUGIN_TELEMETRY=off` disables all telemetry, including prompt text, session metadata, tool events, bash command telemetry, and skill-injection telemetry.
- `echo 'enabled' > ~/.claude/vercel-plugin-telemetry-preference` keeps default base telemetry on and also allows raw content telemetry for prompt text and full bash commands.
- `echo 'disabled' > ~/.claude/vercel-plugin-telemetry-preference` keeps prompt text and full bash commands off, but base telemetry remains on by default.
- `VERCEL_PLUGIN_TELEMETRY=off` disables all telemetry, including prompt text, full bash command telemetry, session metadata, tool names, and skill-injection telemetry.

Where to set `VERCEL_PLUGIN_TELEMETRY=off`:

Expand Down
4 changes: 2 additions & 2 deletions hooks/posttooluse-telemetry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

// hooks/src/posttooluse-telemetry.mts
import { readFileSync } from "fs";
import { trackBaseEvents } from "./telemetry.mjs";
import { trackContentEvents } from "./telemetry.mjs";
function parseStdin() {
try {
const raw = readFileSync(0, "utf-8").trim();
Expand Down Expand Up @@ -32,7 +32,7 @@ async function main() {
);
}
if (entries.length > 0) {
await trackBaseEvents(sessionId, entries);
await trackContentEvents(sessionId, entries);
}
process.stdout.write("{}");
process.exit(0);
Expand Down
13 changes: 7 additions & 6 deletions hooks/pretooluse-skill-inject.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
} from "./patterns.mjs";
import { resolveVercelJsonSkills, isVercelJsonPath, VERCEL_JSON_SKILLS } from "./vercel-config.mjs";
import { createLogger, logDecision } from "./logger.mjs";
import { trackBaseEvents } from "./telemetry.mjs";
import { trackBaseEvents, trackContentEvents } from "./telemetry.mjs";
import { selectManagedContextChunk } from "./vercel-context.mjs";
var MAX_SKILLS = 3;
var DEFAULT_INJECTION_BUDGET_BYTES = 18e3;
Expand Down Expand Up @@ -606,13 +606,14 @@ function run() {
const runtimeEnvBefore = captureRuntimeEnvSnapshot();
if (sessionId) {
const toolEntries = [
{ key: "tool_call:tool_name", value: toolName },
{ key: "tool_call:target", value: toolTarget }
{ key: "tool_call:tool_name", value: toolName }
];
if (toolName === "Bash") {
toolEntries.push({ key: "tool_call:command", value: toolInput.command || "" });
} else {
toolEntries.push({ key: "tool_call:file_path", value: toolInput.file_path || "" });
trackContentEvents(sessionId, [
{ key: "tool_call:target", value: toolTarget },
{ key: "tool_call:command", value: toolInput.command || "" }
]).catch(() => {
});
}
trackBaseEvents(sessionId, toolEntries).catch(() => {
});
Expand Down
8 changes: 4 additions & 4 deletions hooks/setup-telemetry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function main() {
const telemetryOverride = getTelemetryOverride();
if (telemetryOverride === "off") {
process.stdout.write(
"Telemetry is fully disabled via VERCEL_PLUGIN_TELEMETRY=off.\nRemove or change that env var before changing the prompt-sharing preference.\n"
"Telemetry is fully disabled via VERCEL_PLUGIN_TELEMETRY=off.\nRemove or change that env var before changing the content-sharing preference.\n"
);
process.exit(0);
}
Expand All @@ -21,16 +21,16 @@ function main() {
}
if (existing === "enabled" || existing === "disabled" || existing === "asked") {
process.stdout.write(
`Prompt telemetry is currently ${existing}. This setting only controls prompt telemetry.
To change prompt sharing, edit ~/.claude/vercel-plugin-telemetry-preference. To disable all telemetry, set VERCEL_PLUGIN_TELEMETRY=off in the environment that launches your agent.
`Content telemetry is currently ${existing}. This setting controls prompt text and full bash command sharing only.
To change content sharing, edit ~/.claude/vercel-plugin-telemetry-preference. To disable all telemetry, set VERCEL_PLUGIN_TELEMETRY=off in the environment that launches your agent.
`
);
process.exit(0);
}
process.stdout.write(
[
"The Vercel plugin collects anonymous usage data such as skill injection patterns and tools used by default.",
"Ask the user if they would also like to share their prompt text to help improve skill matching. This choice only controls prompt telemetry.",
"Ask the user if they would also like to share raw content telemetry, including prompt text and full bash commands, to help improve skill matching and command understanding. This choice only controls content telemetry.",
"To disable all telemetry, set VERCEL_PLUGIN_TELEMETRY=off in the environment that launches your agent.",
"If they agree, run `echo 'enabled' > ~/.claude/vercel-plugin-telemetry-preference`.",
"If they decline, run `echo 'disabled' > ~/.claude/vercel-plugin-telemetry-preference`.",
Expand Down
7 changes: 3 additions & 4 deletions hooks/src/posttooluse-telemetry.mts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
#!/usr/bin/env node

import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { trackBaseEvents } from "./telemetry.mjs";
import { trackContentEvents } from "./telemetry.mjs";

function parseStdin(): Record<string, unknown> | null {
try {
Expand All @@ -15,7 +14,7 @@ function parseStdin(): Record<string, unknown> | null {
}

async function main(): Promise<void> {
// Base telemetry — enabled by default unless VERCEL_PLUGIN_TELEMETRY=off
// Content telemetry — opt-in only unless VERCEL_PLUGIN_TELEMETRY=off disables all telemetry

const input = parseStdin();
if (!input) {
Expand Down Expand Up @@ -65,7 +64,7 @@ async function main(): Promise<void> {
}

if (entries.length > 0) {
await trackBaseEvents(sessionId, entries);
await trackContentEvents(sessionId, entries);
}

process.stdout.write("{}");
Expand Down
10 changes: 5 additions & 5 deletions hooks/src/pretooluse-skill-inject.mts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ import { resolveVercelJsonSkills, isVercelJsonPath, VERCEL_JSON_SKILLS } from ".
import type { VercelJsonRouting } from "./vercel-config.mjs";
import { createLogger, logDecision } from "./logger.mjs";
import type { Logger } from "./logger.mjs";
import { trackBaseEvents } from "./telemetry.mjs";
import { trackBaseEvents, trackContentEvents } from "./telemetry.mjs";
import { selectManagedContextChunk } from "./vercel-context.mjs";

const MAX_SKILLS = 3;
Expand Down Expand Up @@ -967,12 +967,12 @@ function run(): string {
if (sessionId) {
const toolEntries: Array<{ key: string; value: string }> = [
{ key: "tool_call:tool_name", value: toolName },
{ key: "tool_call:target", value: toolTarget },
];
if (toolName === "Bash") {
toolEntries.push({ key: "tool_call:command", value: (toolInput.command as string) || "" });
} else {
toolEntries.push({ key: "tool_call:file_path", value: (toolInput.file_path as string) || "" });
trackContentEvents(sessionId, [
{ key: "tool_call:target", value: toolTarget },
{ key: "tool_call:command", value: (toolInput.command as string) || "" },
]).catch(() => {});
}
trackBaseEvents(sessionId, toolEntries).catch(() => {});
}
Expand Down
6 changes: 3 additions & 3 deletions hooks/src/setup-telemetry.mts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function main(): void {
const telemetryOverride = getTelemetryOverride();
if (telemetryOverride === "off") {
process.stdout.write(
"Telemetry is fully disabled via VERCEL_PLUGIN_TELEMETRY=off.\nRemove or change that env var before changing the prompt-sharing preference.\n",
"Telemetry is fully disabled via VERCEL_PLUGIN_TELEMETRY=off.\nRemove or change that env var before changing the content-sharing preference.\n",
);
process.exit(0);
}
Expand All @@ -25,15 +25,15 @@ function main(): void {

if (existing === "enabled" || existing === "disabled" || existing === "asked") {
process.stdout.write(
`Prompt telemetry is currently ${existing}. This setting only controls prompt telemetry.\nTo change prompt sharing, edit ~/.claude/vercel-plugin-telemetry-preference. To disable all telemetry, set VERCEL_PLUGIN_TELEMETRY=off in the environment that launches your agent.\n`,
`Content telemetry is currently ${existing}. This setting controls prompt text and full bash command sharing only.\nTo change content sharing, edit ~/.claude/vercel-plugin-telemetry-preference. To disable all telemetry, set VERCEL_PLUGIN_TELEMETRY=off in the environment that launches your agent.\n`,
);
process.exit(0);
}

process.stdout.write(
[
"The Vercel plugin collects anonymous usage data such as skill injection patterns and tools used by default.",
"Ask the user if they would also like to share their prompt text to help improve skill matching. This choice only controls prompt telemetry.",
"Ask the user if they would also like to share raw content telemetry, including prompt text and full bash commands, to help improve skill matching and command understanding. This choice only controls content telemetry.",
"To disable all telemetry, set VERCEL_PLUGIN_TELEMETRY=off in the environment that launches your agent.",
"If they agree, run `echo 'enabled' > ~/.claude/vercel-plugin-telemetry-preference`.",
"If they decline, run `echo 'disabled' > ~/.claude/vercel-plugin-telemetry-preference`.",
Expand Down
39 changes: 30 additions & 9 deletions hooks/src/telemetry.mts
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ export function getOrCreateDeviceId(): string {
// ---------------------------------------------------------------------------

/**
* Prompt-level telemetry (opt-in): requires explicit user consent.
* Gates collection of prompt:text — actual user prompt content.
* Content-level telemetry (opt-in): requires explicit user consent.
* Gates collection of raw content such as prompt:text and bash:command.
*/
export function getTelemetryOverride(env: NodeJS.ProcessEnv = process.env): "off" | null {
const value = env.VERCEL_PLUGIN_TELEMETRY?.trim().toLowerCase();
Expand All @@ -99,10 +99,10 @@ export function isBaseTelemetryEnabled(env: NodeJS.ProcessEnv = process.env): bo
}

/**
* Prompt-level telemetry (opt-in): requires explicit user consent.
* Content-level telemetry (opt-in): requires explicit user consent.
* VERCEL_PLUGIN_TELEMETRY=off disables it entirely.
*/
export function isPromptTelemetryEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
export function isContentTelemetryEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
const override = getTelemetryOverride(env);
if (override === "off") return false;

Expand All @@ -115,6 +115,13 @@ export function isPromptTelemetryEnabled(env: NodeJS.ProcessEnv = process.env):
}
}

/**
* Backward-compatible alias for older callers that still refer to prompt telemetry.
*/
export function isPromptTelemetryEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
return isContentTelemetryEnabled(env);
}

// ---------------------------------------------------------------------------
// Always-on base telemetry (session, tool, skill injection events)
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -150,11 +157,11 @@ export async function trackBaseEvents(
}

// ---------------------------------------------------------------------------
// Opt-in telemetry (prompt text)
// Opt-in telemetry (raw content)
// ---------------------------------------------------------------------------

export async function trackEvent(sessionId: string, key: string, value: string): Promise<void> {
if (!isPromptTelemetryEnabled()) return;
export async function trackContentEvent(sessionId: string, key: string, value: string): Promise<void> {
if (!isContentTelemetryEnabled()) return;

const event: TelemetryEvent = {
id: randomUUID(),
Expand All @@ -166,11 +173,11 @@ export async function trackEvent(sessionId: string, key: string, value: string):
await send(sessionId, [event]);
}

export async function trackEvents(
export async function trackContentEvents(
sessionId: string,
entries: Array<{ key: string; value: string }>,
): Promise<void> {
if (!isPromptTelemetryEnabled() || entries.length === 0) return;
if (!isContentTelemetryEnabled() || entries.length === 0) return;

const now = Date.now();
const events: TelemetryEvent[] = entries.map((entry) => ({
Expand All @@ -182,3 +189,17 @@ export async function trackEvents(

await send(sessionId, events);
}

/**
* Backward-compatible aliases for older callers that still refer to prompt telemetry.
*/
export async function trackEvent(sessionId: string, key: string, value: string): Promise<void> {
await trackContentEvent(sessionId, key, value);
}

export async function trackEvents(
sessionId: string,
entries: Array<{ key: string; value: string }>,
): Promise<void> {
await trackContentEvents(sessionId, entries);
}
20 changes: 10 additions & 10 deletions hooks/src/user-prompt-submit-telemetry.mts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
#!/usr/bin/env node
/**
* UserPromptSubmit hook: prompt telemetry opt-in + prompt text tracking.
* UserPromptSubmit hook: content telemetry opt-in + prompt text tracking.
*
* Fires on every user message. Two responsibilities:
*
* 1. Track prompt:text telemetry (awaited) for every prompt >= 10 chars
* when prompt telemetry is enabled. This runs independently of skill
* when content telemetry is enabled. This runs independently of skill
* matching so prompts are never silently dropped.
*
* 2. On the first message of a session where the user hasn't recorded a
* prompt telemetry preference, return additionalContext asking the model
* content telemetry preference, return additionalContext asking the model
* to prompt the user for opt-in. Writes "asked" immediately so the user
* is never re-prompted. session-end-cleanup converts "asked" → "disabled".
*
* Note: Base telemetry is enabled by default, but users can disable all
* telemetry with VERCEL_PLUGIN_TELEMETRY=off. This hook only gates prompt
* text collection when telemetry is otherwise enabled.
* text and full bash command collection when telemetry is otherwise enabled.
*
* Input: JSON on stdin with { session_id, prompt }
* Output: JSON on stdout with { hookSpecificOutput: { hookEventName, additionalContext } } or {}
Expand All @@ -25,7 +25,7 @@ import type { SyncHookJSONOutput } from "@anthropic-ai/claude-agent-sdk";
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { homedir, tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { getTelemetryOverride, isPromptTelemetryEnabled, trackEvents } from "./telemetry.mjs";
import { getTelemetryOverride, isContentTelemetryEnabled, trackContentEvents } from "./telemetry.mjs";

const PREF_PATH = join(homedir(), ".claude", "vercel-plugin-telemetry-preference");
const MIN_PROMPT_LENGTH = 10;
Expand Down Expand Up @@ -55,8 +55,8 @@ async function main(): Promise<void> {
const telemetryOverride = getTelemetryOverride();

// Prompt text tracking — opt-in only
if (isPromptTelemetryEnabled() && sessionId && prompt.length >= MIN_PROMPT_LENGTH) {
await trackEvents(sessionId, [
if (isContentTelemetryEnabled() && sessionId && prompt.length >= MIN_PROMPT_LENGTH) {
await trackContentEvents(sessionId, [
{ key: "prompt:text", value: prompt },
]).catch(() => {});
}
Expand Down Expand Up @@ -106,13 +106,13 @@ async function main(): Promise<void> {
const message = [
"After responding to the user's message, use the AskUserQuestion tool to ask about telemetry.",
"Use this exact question configuration:",
'- question: "The Vercel plugin collects anonymous usage data such as skill injection patterns and tools used by default. Would you like to also share your prompt text to help improve skill matching? This choice only controls prompt telemetry."',
'- question: "The Vercel plugin collects anonymous usage data such as skill injection patterns and tools used by default. Would you like to also share raw content telemetry, including your prompt text and full bash commands, to help improve skill matching and command understanding? This choice only controls content telemetry."',
'- header: "Telemetry"',
'- options: [{"label": "Share prompts", "description": "Share prompt text in addition to default anonymous usage data"}, {"label": "No thanks", "description": "Keep prompt text private. This only disables prompt telemetry"}]',
'- options: [{"label": "Share content", "description": "Share prompt text and full bash commands in addition to default anonymous usage data"}, {"label": "No thanks", "description": "Keep prompt text and full bash commands private. This only disables content telemetry"}]',
"- multiSelect: false",
"",
"After the user responds:",
"- If they chose \"Share prompts\", run: `echo 'enabled' > ~/.claude/vercel-plugin-telemetry-preference`",
"- If they chose \"Share content\", run: `echo 'enabled' > ~/.claude/vercel-plugin-telemetry-preference`",
"- If they chose \"No thanks\" or anything else, run: `echo 'disabled' > ~/.claude/vercel-plugin-telemetry-preference`",
].join("\n");

Expand Down
22 changes: 17 additions & 5 deletions hooks/telemetry.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ function getTelemetryOverride(env = process.env) {
function isBaseTelemetryEnabled(env = process.env) {
return getTelemetryOverride(env) !== "off";
}
function isPromptTelemetryEnabled(env = process.env) {
function isContentTelemetryEnabled(env = process.env) {
const override = getTelemetryOverride(env);
if (override === "off") return false;
try {
Expand All @@ -68,6 +68,9 @@ function isPromptTelemetryEnabled(env = process.env) {
return false;
}
}
function isPromptTelemetryEnabled(env = process.env) {
return isContentTelemetryEnabled(env);
}
async function trackBaseEvent(sessionId, key, value) {
if (!isBaseTelemetryEnabled()) return;
const event = {
Expand All @@ -89,8 +92,8 @@ async function trackBaseEvents(sessionId, entries) {
}));
await send(sessionId, events);
}
async function trackEvent(sessionId, key, value) {
if (!isPromptTelemetryEnabled()) return;
async function trackContentEvent(sessionId, key, value) {
if (!isContentTelemetryEnabled()) return;
const event = {
id: randomUUID(),
event_time: Date.now(),
Expand All @@ -99,8 +102,8 @@ async function trackEvent(sessionId, key, value) {
};
await send(sessionId, [event]);
}
async function trackEvents(sessionId, entries) {
if (!isPromptTelemetryEnabled() || entries.length === 0) return;
async function trackContentEvents(sessionId, entries) {
if (!isContentTelemetryEnabled() || entries.length === 0) return;
const now = Date.now();
const events = entries.map((entry) => ({
id: randomUUID(),
Expand All @@ -110,13 +113,22 @@ async function trackEvents(sessionId, entries) {
}));
await send(sessionId, events);
}
async function trackEvent(sessionId, key, value) {
await trackContentEvent(sessionId, key, value);
}
async function trackEvents(sessionId, entries) {
await trackContentEvents(sessionId, entries);
}
export {
getOrCreateDeviceId,
getTelemetryOverride,
isBaseTelemetryEnabled,
isContentTelemetryEnabled,
isPromptTelemetryEnabled,
trackBaseEvent,
trackBaseEvents,
trackContentEvent,
trackContentEvents,
trackEvent,
trackEvents
};
Loading
Loading