@debugg-ai/debugg-ai-mcp 1.0.32 → 1.0.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,6 +43,10 @@ const configSchema = z.object({
43
43
  level: z.enum(['error', 'warn', 'info', 'debug']).default('info'),
44
44
  format: z.enum(['json', 'simple']).default('simple'),
45
45
  }),
46
+ telemetry: z.object({
47
+ posthogApiKey: z.string().optional(),
48
+ posthogHost: z.string().optional(),
49
+ }),
46
50
  });
47
51
  export function loadConfig() {
48
52
  const rawConfig = {
@@ -67,6 +71,10 @@ export function loadConfig() {
67
71
  level: process.env.LOG_LEVEL || 'info',
68
72
  format: process.env.LOG_FORMAT || 'simple',
69
73
  },
74
+ telemetry: {
75
+ posthogApiKey: process.env.POSTHOG_API_KEY || undefined,
76
+ posthogHost: process.env.POSTHOG_HOST || undefined,
77
+ },
70
78
  };
71
79
  try {
72
80
  return configSchema.parse(rawConfig);
@@ -86,7 +94,8 @@ export const config = {
86
94
  get server() { return getConfig().server; },
87
95
  get api() { return getConfig().api; },
88
96
  get defaults() { return getConfig().defaults; },
89
- get logging() { return getConfig().logging; }
97
+ get logging() { return getConfig().logging; },
98
+ get telemetry() { return getConfig().telemetry; },
90
99
  };
91
100
  function getConfig() {
92
101
  if (!_config) {
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
21
21
  import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
22
22
  import { config } from "./config/index.js";
23
23
  import { tools, getTool } from "./tools/index.js";
24
- import { Logger, validateInput, createErrorResponse, toMCPError, handleConfigurationError } from "./utils/index.js";
24
+ import { Logger, validateInput, createErrorResponse, toMCPError, handleConfigurationError, Telemetry, TelemetryEvents, } from "./utils/index.js";
25
25
  // Initialize logger
26
26
  const logger = new Logger({ module: 'main' });
27
27
  /**
@@ -108,8 +108,11 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
108
108
  const progressCallback = createProgressCallback(typeof progressToken === 'string' || typeof progressToken === 'number' ? String(progressToken) : undefined);
109
109
  // Execute tool handler with progress callback
110
110
  requestLogger.info(`Executing tool: ${name}`);
111
+ const toolStart = Date.now();
111
112
  const result = await tool.handler(validatedInput, context, progressCallback);
113
+ const toolDuration = Date.now() - toolStart;
112
114
  requestLogger.info(`Tool execution completed: ${name}`);
115
+ Telemetry.capture(TelemetryEvents.TOOL_EXECUTED, { toolName: name, durationMs: toolDuration, success: true });
113
116
  return result;
114
117
  }
115
118
  catch (error) {
@@ -121,6 +124,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
121
124
  message: mcpError.message,
122
125
  data: mcpError.data
123
126
  });
127
+ Telemetry.capture(TelemetryEvents.TOOL_FAILED, { toolName: name, errorCode: mcpError.code });
124
128
  return createErrorResponse(mcpError, typedReq.params.name);
125
129
  }
126
130
  });
@@ -149,6 +153,15 @@ async function main() {
149
153
  if (!config.api.key) {
150
154
  throw new Error('Missing required environment variable: DEBUGGAI_API_KEY');
151
155
  }
156
+ // Initialize telemetry (PostHog when key is set, Noop otherwise)
157
+ Telemetry.setDistinctId(config.api.key);
158
+ if (config.telemetry.posthogApiKey) {
159
+ const { PostHogProvider } = await import('./services/posthogProvider.js');
160
+ Telemetry.configure(new PostHogProvider(config.telemetry.posthogApiKey, {
161
+ host: config.telemetry.posthogHost,
162
+ }));
163
+ logger.info('Telemetry enabled (PostHog)');
164
+ }
152
165
  // Create and connect transport
153
166
  const transport = new StdioServerTransport();
154
167
  await server.connect(transport);
@@ -168,12 +181,14 @@ async function main() {
168
181
  /**
169
182
  * Handle graceful shutdown
170
183
  */
171
- process.on('SIGINT', () => {
184
+ process.on('SIGINT', async () => {
172
185
  logger.info('Received SIGINT, shutting down gracefully');
186
+ await Telemetry.shutdown();
173
187
  process.exit(0);
174
188
  });
175
- process.on('SIGTERM', () => {
189
+ process.on('SIGTERM', async () => {
176
190
  logger.info('Received SIGTERM, shutting down gracefully');
191
+ await Telemetry.shutdown();
177
192
  process.exit(0);
178
193
  });
179
194
  /**
@@ -17,6 +17,7 @@
17
17
  * process recently touched the entry the timer resets instead of stopping.
18
18
  */
19
19
  import { Logger } from '../../utils/logger.js';
20
+ import { Telemetry, TelemetryEvents } from '../../utils/telemetry.js';
20
21
  import { isLocalhostUrl, extractLocalhostPort, generateTunnelUrl } from '../../utils/urlParser.js';
21
22
  import { v4 as uuidv4 } from 'uuid';
22
23
  import { getDefaultRegistry, } from './tunnelRegistry.js';
@@ -128,6 +129,7 @@ class TunnelManager {
128
129
  if (!tunnelInfo.isOwned) {
129
130
  // Borrowed — just drop the local reference; owner manages the real tunnel
130
131
  logger.info(`Released borrowed tunnel reference: ${tunnelInfo.publicUrl}`);
132
+ Telemetry.capture(TelemetryEvents.TUNNEL_STOPPED, { port: tunnelInfo.port, reason: 'released', isOwned: false });
131
133
  return;
132
134
  }
133
135
  // Owned — remove from shared registry, then disconnect + revoke
@@ -183,6 +185,7 @@ class TunnelManager {
183
185
  const existing = this.getTunnelForPort(port);
184
186
  if (existing) {
185
187
  logger.info(`Reusing existing tunnel for port ${port}: ${existing.publicUrl}`);
188
+ Telemetry.capture(TelemetryEvents.TUNNEL_PROVISIONED, { port, how: 'reused' });
186
189
  return { url: existing.publicUrl, tunnelId: existing.tunnelId, isLocalhost: true };
187
190
  }
188
191
  // 2. Deduplicate concurrent creation requests for the same port
@@ -212,6 +215,7 @@ class TunnelManager {
212
215
  regEntry.lastAccessedAt = now;
213
216
  this.reg.write(registry);
214
217
  this.resetTunnelTimer(borrowed);
218
+ Telemetry.capture(TelemetryEvents.TUNNEL_PROVISIONED, { port, how: 'borrowed' });
215
219
  return { url: regEntry.publicUrl, tunnelId: regEntry.tunnelId, isLocalhost: true };
216
220
  }
217
221
  // 4. Create a new tunnel (this process becomes the owner)
@@ -290,6 +294,7 @@ class TunnelManager {
290
294
  }
291
295
  this.resetTunnelTimer(tunnelInfo);
292
296
  logger.info(`Tunnel created: ${publicUrl} → localhost:${port}`);
297
+ Telemetry.capture(TelemetryEvents.TUNNEL_PROVISIONED, { port, how: 'created' });
293
298
  return tunnelInfo;
294
299
  }
295
300
  catch (error) {
@@ -334,6 +339,7 @@ class TunnelManager {
334
339
  }
335
340
  }
336
341
  logger.info(`Auto-shutting down tunnel ${tunnelInfo.tunnelId} after inactivity`);
342
+ Telemetry.capture(TelemetryEvents.TUNNEL_STOPPED, { port: tunnelInfo.port, reason: 'auto-shutoff', isOwned: tunnelInfo.isOwned });
337
343
  await this.stopTunnel(tunnelInfo.tunnelId).catch((err) => logger.error(`Failed to auto-shutdown tunnel ${tunnelInfo.tunnelId}:`, err));
338
344
  }, this.TUNNEL_TIMEOUT_MS);
339
345
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * PostHog telemetry provider.
3
+ * Enabled when POSTHOG_API_KEY is set in the environment.
4
+ */
5
+ import { PostHog } from 'posthog-node';
6
+ export class PostHogProvider {
7
+ client;
8
+ constructor(apiKey, options) {
9
+ this.client = new PostHog(apiKey, {
10
+ host: options?.host ?? 'https://us.i.posthog.com',
11
+ flushAt: 20,
12
+ flushInterval: 10000,
13
+ });
14
+ }
15
+ capture(event) {
16
+ this.client.capture({
17
+ distinctId: event.distinctId,
18
+ event: event.event,
19
+ properties: event.properties,
20
+ timestamp: event.timestamp,
21
+ });
22
+ }
23
+ identify(distinctId, properties) {
24
+ this.client.identify({ distinctId, properties });
25
+ }
26
+ async flush() {
27
+ await this.client.flush();
28
+ }
29
+ async shutdown() {
30
+ await this.client.shutdown();
31
+ }
32
+ }
@@ -2,6 +2,7 @@
2
2
  * Workflows Service
3
3
  * 4-step integration: find template → execute → poll → result
4
4
  */
5
+ import { Telemetry, TelemetryEvents } from '../utils/telemetry.js';
5
6
  const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled']);
6
7
  const POLL_INTERVAL_MS = 3000;
7
8
  const EXECUTION_TIMEOUT_MS = 10 * 60 * 1000; // 10 min
@@ -44,15 +45,26 @@ export const createWorkflowsService = (tx) => {
44
45
  },
45
46
  async pollExecution(executionUuid, onUpdate, signal) {
46
47
  const deadline = Date.now() + EXECUTION_TIMEOUT_MS;
48
+ const pollStart = Date.now();
49
+ let pollCount = 0;
47
50
  while (Date.now() < deadline) {
48
51
  if (signal?.aborted) {
49
52
  throw new Error(`Polling cancelled for execution ${executionUuid}`);
50
53
  }
51
54
  const execution = await service.getExecution(executionUuid);
55
+ pollCount++;
52
56
  if (onUpdate) {
53
57
  await onUpdate(execution).catch(() => { });
54
58
  }
55
59
  if (TERMINAL_STATUSES.has(execution.status)) {
60
+ Telemetry.capture(TelemetryEvents.WORKFLOW_EXECUTED, {
61
+ status: execution.status,
62
+ success: execution.state?.success ?? false,
63
+ outcome: execution.state?.outcome ?? null,
64
+ stepsTaken: execution.state?.stepsTaken ?? 0,
65
+ durationMs: Date.now() - pollStart,
66
+ pollCount,
67
+ });
56
68
  return execution;
57
69
  }
58
70
  await new Promise((resolve, reject) => {
@@ -6,3 +6,4 @@ export * from './validation.js';
6
6
  export * from './errors.js';
7
7
  export * from './projectAnalyzer.js';
8
8
  export * from './imageUtils.js';
9
+ export * from './telemetry.js';
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Generic telemetry abstraction layer.
3
+ * Providers implement TelemetryProvider; call sites use Telemetry.capture().
4
+ * Falls back to NoopProvider when no provider is configured.
5
+ */
6
+ import { createHash } from 'crypto';
7
+ class NoopProvider {
8
+ capture(_event) { }
9
+ identify(_distinctId, _properties) { }
10
+ async flush() { }
11
+ async shutdown() { }
12
+ }
13
+ let _provider = new NoopProvider();
14
+ let _distinctId = 'anonymous';
15
+ export const Telemetry = {
16
+ configure(provider) {
17
+ _provider = provider;
18
+ },
19
+ /**
20
+ * Derive a stable, anonymous identifier from the API key.
21
+ * Uses SHA-256 so the raw key is never stored or transmitted.
22
+ */
23
+ setDistinctId(apiKey) {
24
+ _distinctId = createHash('sha256').update(apiKey).digest('hex').slice(0, 16);
25
+ },
26
+ capture(event, properties) {
27
+ try {
28
+ _provider.capture({ event, distinctId: _distinctId, properties, timestamp: new Date() });
29
+ }
30
+ catch {
31
+ // never let telemetry crash the app
32
+ }
33
+ },
34
+ async flush() {
35
+ try {
36
+ await _provider.flush();
37
+ }
38
+ catch {
39
+ // best-effort
40
+ }
41
+ },
42
+ async shutdown() {
43
+ try {
44
+ await _provider.shutdown();
45
+ }
46
+ catch {
47
+ // best-effort
48
+ }
49
+ },
50
+ };
51
+ export const TelemetryEvents = {
52
+ TOOL_EXECUTED: 'tool.executed',
53
+ TOOL_FAILED: 'tool.failed',
54
+ WORKFLOW_EXECUTED: 'workflow.executed',
55
+ TUNNEL_PROVISIONED: 'tunnel.provisioned',
56
+ TUNNEL_STOPPED: 'tunnel.stopped',
57
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@debugg-ai/debugg-ai-mcp",
3
- "version": "1.0.32",
3
+ "version": "1.0.33",
4
4
  "description": "Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,8 +25,8 @@
25
25
  "version:major": "npm version major --no-git-tag-version",
26
26
  "publish:check": "npm pack --dry-run",
27
27
  "prepublishOnly": "npm test && npm run build",
28
- "mcp:local": "npm run build && claude mcp remove debugg-ai 2>/dev/null; claude mcp add debugg-ai -s user -e DEBUGGAI_API_KEY=KrvXlzVFXVZO82UErXye5N7CtnmBTu1GKULrJnwXRRU -- node /Users/qosha/Repos/debugg-ai/debugg-ai-mcp/dist/index.js",
29
- "mcp:npm": "claude mcp remove debugg-ai 2>/dev/null; claude mcp add debugg-ai -s user -e DEBUGGAI_API_KEY=KrvXlzVFXVZO82UErXye5N7CtnmBTu1GKULrJnwXRRU -- npx -y @debugg-ai/debugg-ai-mcp"
28
+ "mcp:local": "npm run build && claude mcp remove debugg-ai 2>/dev/null; claude mcp add debugg-ai -s user -e DEBUGGAI_API_KEY=KrvXlzVFXVZO82UErXye5N7CtnmBTu1GKULrJnwXRRU -e POSTHOG_API_KEY=phc_4h2Yov2P0Vc9UMqfKf3dYKSQ6THOs7N6LZR0VKYopZN -- node /Users/qosha/Repos/debugg-ai/debugg-ai-mcp/dist/index.js",
29
+ "mcp:npm": "claude mcp remove debugg-ai 2>/dev/null; claude mcp add debugg-ai -s user -e DEBUGGAI_API_KEY=KrvXlzVFXVZO82UErXye5N7CtnmBTu1GKULrJnwXRRU -e POSTHOG_API_KEY=phc_4h2Yov2P0Vc9UMqfKf3dYKSQ6THOs7N6LZR0VKYopZN -- npx -y @debugg-ai/debugg-ai-mcp"
30
30
  },
31
31
  "keywords": [
32
32
  "debugg",
@@ -49,6 +49,7 @@
49
49
  "axios": "^1.9.0",
50
50
  "mkdirp": "^3.0.1",
51
51
  "ngrok": "^5.0.0-beta.2",
52
+ "posthog-node": "^5.26.0",
52
53
  "uuid": "^11.1.0",
53
54
  "winston": "^3.15.0",
54
55
  "zod": "^3.25.32"