Skip to content

Commit dfffb7d

Browse files
authored
add telemetry tracking (#85)
1 parent 3b50c33 commit dfffb7d

14 files changed

+970
-41
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,3 @@ _*.md
1616
test/fixtures/**/pnpm-lock.yaml
1717
test/fixtures/**/node_modules/
1818
test/fixtures/**/.next/
19-

CLAUDE.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,15 +75,25 @@ All tools, prompts, and resources are explicitly imported and registered in `src
7575
- `browser-eval-manager.ts`: Manages Playwright MCP server lifecycle
7676
- `nextjs-runtime-manager.ts`: Discovers and connects to Next.js dev servers with MCP enabled
7777

78+
**Telemetry System** (`src/telemetry/`):
79+
- `mcp-telemetry-tracker.ts`: Singleton tracker for MCP tool invocations
80+
- `telemetry-events.ts`: Event schema definitions and factory functions
81+
- `telemetry-storage.ts`: Handles anonymous ID, session tracking, and API submission
82+
- `event-queue.ts`: In-memory aggregation of events during session
83+
- `flush-events.ts`: Background process that sends events after server shutdown
84+
- `logger.ts`: Synchronous file logging for debugging
85+
- Telemetry can be disabled via `NEXT_TELEMETRY_DISABLED=1` environment variable
86+
- Data stored in `~/.next-devtools-mcp/` (telemetry-id, telemetry-salt, mcp.log)
87+
7888
**Resources Architecture**:
7989
- Knowledge base split into focused sections (12 sections for Cache Components, 2 for Next.js 16, 1 for fundamentals)
8090
- Each resource exports: `metadata` (uri, name, description, mimeType) and `handler` (function returning content)
8191
- Resources use URI-based addressing (e.g., `cache-components://overview`)
82-
- Markdown files in `src/resources/` are copied to `dist/resources/` during build via `scripts/copy-resources.js`
92+
- Markdown files in `src/resources/` and `src/prompts/` are copied during build via `scripts/copy-resources.js` (to `dist/resources/` and `dist/resources/prompts/` respectively)
8393

8494
### TypeScript Configuration
8595

86-
- Target: ES2022, ES modules (Node16 module resolution)
96+
- Target: ES2022, ES modules (NodeNext module resolution)
8797
- Strict mode enabled
8898
- Output directory: `dist/`
8999
- Declaration files generated
@@ -92,7 +102,7 @@ All tools, prompts, and resources are explicitly imported and registered in `src
92102
## Build Process
93103

94104
1. TypeScript compilation: `tsc` compiles all TypeScript files from `src/` to `dist/`
95-
2. Resource copying: `scripts/copy-resources.js` copies markdown files from `src/resources/` and `src/prompts/` to `dist/resources/`
105+
2. Resource copying: `scripts/copy-resources.js` copies markdown files from `src/resources/` and `src/prompts/` (to `dist/resources/` and `dist/resources/prompts/` respectively)
96106

97107
The `dist/index.js` file is the entry point for the MCP server and includes a shebang for CLI execution.
98108

README.md

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -294,25 +294,31 @@ proper context and ensures all Next.js queries use official documentation.
294294

295295
## MCP Resources
296296

297-
Next.js 16 knowledge base resources are automatically available to your coding agent.
298-
299-
These resources provide comprehensive documentation split into focused sections for efficient context management:
297+
The knowledge base resources are automatically available to your coding agent and are split into focused sections for efficient context management. Current resource URIs:
300298

301299
<details>
302300
<summary>📚 Available Knowledge Base Resources (click to expand)</summary>
303301

304-
- **`nextjs16://knowledge/overview`** - Overview and critical errors AI agents make
305-
- **`nextjs16://knowledge/core-mechanics`** - Fundamental paradigm shift and how cacheComponents works
306-
- **`nextjs16://knowledge/public-caches`** - Public cache mechanics with 'use cache'
307-
- **`nextjs16://knowledge/private-caches`** - Private cache patterns with 'use cache: private'
308-
- **`nextjs16://knowledge/runtime-prefetching`** - Runtime prefetch configuration and patterns
309-
- **`nextjs16://knowledge/request-apis`** - Async params, searchParams, cookies, headers APIs
310-
- **`nextjs16://knowledge/cache-invalidation`** - updateTag, revalidateTag, and refresh patterns
311-
- **`nextjs16://knowledge/advanced-patterns`** - cacheLife, cacheTag, draft mode, and more
312-
- **`nextjs16://knowledge/build-behavior`** - Prerendering, resume data cache, and metadata
313-
- **`nextjs16://knowledge/error-patterns`** - Common errors and how to fix them
314-
- **`nextjs16://knowledge/test-patterns`** - E2E patterns from 125+ test fixtures
315-
- **`nextjs16://knowledge/reference`** - API reference, checklists, and comprehensive nuances
302+
- Cache Components (12 sections):
303+
- `cache-components://overview`
304+
- `cache-components://core-mechanics`
305+
- `cache-components://public-caches`
306+
- `cache-components://private-caches`
307+
- `cache-components://runtime-prefetching`
308+
- `cache-components://request-apis`
309+
- `cache-components://cache-invalidation`
310+
- `cache-components://advanced-patterns`
311+
- `cache-components://build-behavior`
312+
- `cache-components://error-patterns`
313+
- `cache-components://test-patterns`
314+
- `cache-components://reference`
315+
316+
- Next.js 16 migration:
317+
- `nextjs16://migration/beta-to-stable`
318+
- `nextjs16://migration/examples`
319+
320+
- Next.js fundamentals:
321+
- `nextjs-fundamentals://use-client`
316322

317323
</details>
318324

@@ -325,7 +331,6 @@ Pre-configured prompts to help with common Next.js development tasks:
325331
<details>
326332
<summary>💡 Available Prompts (click to expand)</summary>
327333

328-
- **`init`** - Initialize context for Next.js development with MCP tools and documentation requirements
329334
- **`upgrade-nextjs-16`** - Guide for upgrading to Next.js 16
330335
- **`enable-cache-components`** - Migrate and enable Cache Components mode for Next.js 16
331336

@@ -548,6 +553,40 @@ With other agents or programmatically:
548553

549554
</details>
550555

556+
## Privacy & Telemetry
557+
558+
### What Data is Collected
559+
560+
`next-devtools-mcp` collects anonymous usage telemetry to help improve the tool. The following data is collected:
561+
562+
- **Tool usage**: Which MCP tools are invoked (e.g., `nextjs_runtime`, `browser_eval`, `upgrade_nextjs_16`)
563+
- **Error events**: Anonymous error messages when tools fail
564+
- **Session metadata**: Session ID, timestamps, and basic environment info (OS, Node.js version)
565+
566+
**What is NOT collected:**
567+
- Your project code, file contents, or file paths
568+
- Personal information or identifiable data
569+
- API keys, credentials, or sensitive configuration
570+
- Arguments passed to tools (except tool names)
571+
572+
Local files are written under `~/.next-devtools-mcp/` (anonymous `telemetry-id`, `telemetry-salt`, and a debug log `mcp.log`). Events are sent to the telemetry endpoint in the background to help us understand usage patterns and improve reliability.
573+
574+
### Opt-Out
575+
576+
To disable telemetry completely, set the environment variable:
577+
578+
```bash
579+
export NEXT_TELEMETRY_DISABLED=1
580+
```
581+
582+
Add this to your shell configuration file (e.g., `~/.bashrc`, `~/.zshrc`) to make it permanent.
583+
584+
You can also delete your local telemetry data at any time:
585+
586+
```bash
587+
rm -rf ~/.next-devtools-mcp
588+
```
589+
551590
## Local Development
552591

553592
To run the MCP server locally for development:
@@ -580,4 +619,3 @@ To run the MCP server locally for development:
580619
## License
581620

582621
MIT License
583-

src/index.ts

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,26 @@ import {
1010
ReadResourceRequestSchema,
1111
} from "@modelcontextprotocol/sdk/types.js"
1212
import { z } from "zod"
13+
import { spawn } from "child_process"
14+
import { fileURLToPath } from "url"
15+
import { dirname, join } from "path"
1316
import pkg from "../package.json" with { type: "json" }
17+
import type { McpToolName } from "./telemetry/mcp-telemetry-tracker.js"
18+
import { queueEvent, getSessionAggregationJSON } from "./telemetry/event-queue.js"
19+
import { log } from "./telemetry/logger.js"
20+
21+
const __filename = fileURLToPath(import.meta.url)
22+
const __dirname = dirname(__filename)
1423

15-
// Import tools
1624
import * as browserEval from "./tools/browser-eval.js"
1725
import * as enableCacheComponents from "./tools/enable-cache-components.js"
1826
import * as init from "./tools/init.js"
1927
import * as nextjsDocs from "./tools/nextjs-docs.js"
2028
import * as nextjsRuntime from "./tools/nextjs-runtime.js"
2129
import * as upgradeNextjs16 from "./tools/upgrade-nextjs-16.js"
2230

23-
// Import prompts
2431
import * as upgradeNextjs16Prompt from "./prompts/upgrade-nextjs-16.js"
2532
import * as enableCacheComponentsPrompt from "./prompts/enable-cache-components.js"
26-
27-
// Import resources
2833
import * as cacheComponentsOverview from "./resources/(cache-components)/overview.js"
2934
import * as cacheComponentsCoreMechanics from "./resources/(cache-components)/core-mechanics.js"
3035
import * as cacheComponentsPublicCaches from "./resources/(cache-components)/public-caches.js"
@@ -42,13 +47,19 @@ import * as nextjsFundamentalsUseClient from "./resources/(nextjs-fundamentals)/
4247
import * as nextjs16BetaToStable from "./resources/(nextjs16)/migration/beta-to-stable.js"
4348
import * as nextjs16Examples from "./resources/(nextjs16)/migration/examples.js"
4449

45-
// Tool registry
4650
const tools = [browserEval, enableCacheComponents, init, nextjsDocs, nextjsRuntime, upgradeNextjs16]
4751

48-
// Prompt registry
52+
const toolNameToTelemetryName: Record<string, McpToolName> = {
53+
browser_eval: "mcp/browser_eval",
54+
enable_cache_components: "mcp/enable_cache_components",
55+
init: "mcp/init",
56+
nextjs_docs: "mcp/nextjs_docs",
57+
nextjs_runtime: "mcp/nextjs_runtime",
58+
upgrade_nextjs_16: "mcp/upgrade_nextjs_16",
59+
}
60+
4961
const prompts = [upgradeNextjs16Prompt, enableCacheComponentsPrompt]
5062

51-
// Resource registry
5263
const resources = [
5364
cacheComponentsOverview,
5465
cacheComponentsCoreMechanics,
@@ -117,11 +128,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
117128
throw new Error(`Tool not found: ${name}`)
118129
}
119130

120-
// Validate arguments with Zod schema
131+
// Queue telemetry event for later batch sending
132+
const telemetryName = toolNameToTelemetryName[name]
133+
if (telemetryName) {
134+
const event = {
135+
eventName: "NEXT_MCP_TOOL_USAGE",
136+
fields: {
137+
toolName: telemetryName,
138+
invocationCount: 1,
139+
},
140+
}
141+
queueEvent(event)
142+
}
143+
121144
const parsedArgs = parseToolArgs(tool.inputSchema, args || {})
122145

123-
// Call the tool handler
124-
const result = await tool.handler(parsedArgs as never)
146+
const result = await (tool.handler as (args: Record<string, unknown>) => Promise<string>)(parsedArgs)
125147

126148
return {
127149
content: [
@@ -193,7 +215,6 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
193215
throw new Error(`Resource not found: ${uri}`)
194216
}
195217

196-
// Get the resource content
197218
const content = await resource.handler()
198219

199220
return {
@@ -207,12 +228,9 @@ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
207228
}
208229
})
209230

210-
// Helper function to convert Zod schema to JSON Schema (simplified)
211231
function zodSchemaToJsonSchema(zodSchema: z.ZodTypeAny): JSONSchema {
212-
// Get the description
213232
const description = zodSchema._def?.description
214233

215-
// Handle different Zod types
216234
if (zodSchema._def?.typeName === "ZodString") {
217235
return { type: "string", description }
218236
}
@@ -244,18 +262,15 @@ function zodSchemaToJsonSchema(zodSchema: z.ZodTypeAny): JSONSchema {
244262
return zodSchemaToJsonSchema(zodSchema._def.innerType)
245263
}
246264
if (zodSchema._def?.typeName === "ZodUnion") {
247-
// Handle union types (for boolean | string transforms)
248265
const options = zodSchema._def.options
249266
if (options.length === 2) {
250267
return zodSchemaToJsonSchema(options[0])
251268
}
252269
}
253270

254-
// Default fallback
255271
return { type: "string", description }
256272
}
257273

258-
// Helper function to validate tool arguments with Zod
259274
function parseToolArgs(
260275
schema: Record<string, z.ZodTypeAny>,
261276
args: Record<string, unknown>
@@ -264,26 +279,55 @@ function parseToolArgs(
264279

265280
for (const [key, zodSchema] of Object.entries(schema)) {
266281
if (args[key] !== undefined) {
267-
// Let Zod handle the validation and transformation
268282
const parsed = zodSchema.safeParse(args[key])
269283
if (parsed.success) {
270284
result[key] = parsed.data
271285
} else {
272286
throw new Error(`Invalid argument '${key}': ${parsed.error.message}`)
273287
}
274288
} else if (!zodSchema.isOptional()) {
275-
// Check if required
276289
throw new Error(`Missing required argument: ${key}`)
277290
}
278291
}
279292

280293
return result
281294
}
282295

283-
// Start the server
284296
async function main() {
285297
const transport = new StdioServerTransport()
286298
await server.connect(transport)
299+
300+
log('Server started')
301+
302+
const shutdown = () => {
303+
log('Server terminated')
304+
305+
const aggregationJSON = getSessionAggregationJSON()
306+
307+
if (aggregationJSON) {
308+
const flushEventsScript = join(__dirname, "telemetry", "flush-events.js")
309+
const child = spawn(
310+
process.execPath,
311+
[flushEventsScript, aggregationJSON],
312+
{
313+
detached: true,
314+
stdio: 'ignore',
315+
windowsHide: true
316+
}
317+
)
318+
319+
child.unref()
320+
321+
log('Event flusher spawned with aggregation data')
322+
} else {
323+
log('No events to flush')
324+
}
325+
326+
process.exit(0)
327+
}
328+
329+
process.on('SIGINT', shutdown)
330+
process.on('SIGTERM', shutdown)
287331
}
288332

289333
main().catch((error) => {

src/telemetry/event-queue.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { log } from "./logger.js"
2+
import type { TelemetryEvent } from "./telemetry-events.js"
3+
4+
const sessionAggregation = new Map<string, number>()
5+
6+
export function queueEvent(event: TelemetryEvent): void {
7+
log("TOOL_INVOCATION", event)
8+
9+
if (event.eventName === "NEXT_MCP_TOOL_USAGE" && event.fields.toolName) {
10+
const toolName = event.fields.toolName
11+
const currentCount = sessionAggregation.get(toolName) || 0
12+
sessionAggregation.set(toolName, currentCount + event.fields.invocationCount)
13+
}
14+
}
15+
16+
export function getSessionAggregationJSON(): string | null {
17+
if (sessionAggregation.size === 0) {
18+
return null
19+
}
20+
21+
const aggregationObject = Object.fromEntries(sessionAggregation)
22+
return JSON.stringify(aggregationObject)
23+
}
24+

0 commit comments

Comments
 (0)