Skip to content

Commit ce4669c

Browse files
chore(telemetry): route events through proxy (#478)
* chore(telemetry): route events through proxy * fix(telemetry): enforce full response timeout * fix(telemetry): keep capture calls non-blocking * chore(telemetry): remove proxy bearer token * chore(telemetry): add review TODOs for proxy migration - capture.ts: flag unauthenticated proxy endpoint (token removed, confirm server-side rate limiting/validation), dead captureBase, and fire-and-forget capture() dropping events on quick exit. - track-installation.js: note 5s-per-endpoint vs 6s overall budget making the fallback unusable, and postTelemetryPayload duplicated across 4 files. --------- Co-authored-by: Eduard Ruzga <wonderwhy.er@gmail.com>
1 parent 701bc31 commit ce4669c

5 files changed

Lines changed: 219 additions & 193 deletions

File tree

setup-claude-server.js

Lines changed: 56 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ import { version as nodeVersion } from 'process';
1010
import * as https from 'https';
1111
import { randomUUID } from 'crypto';
1212

13-
// Google Analytics configuration
14-
const GA_MEASUREMENT_ID = 'G-NGGDNL0K4L'; // Replace with your GA4 Measurement ID
15-
const GA_API_SECRET = '5M0mC--2S_6t94m8WrI60A'; // Replace with your GA4 API Secre
16-
const GA_BASE_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
13+
// Telemetry proxy configuration
14+
const TELEMETRY_PROXY_URL = 'https://telemetry.desktopcommander.app/mp/collect';
15+
const TELEMETRY_PROXY_FALLBACK_URL = 'https://dc-telemetry-proxy-83847352264.europe-west1.run.app/mp/collect';
1716

1817
// Generate a unique anonymous ID using UUID - consistent with privacy policy
1918
let uniqueUserId = 'unknown';
@@ -302,11 +301,6 @@ async function enhancedGetTrackingProperties(additionalProps = {}) {
302301
async function trackEvent(eventName, additionalProps = {}) {
303302
const trackingStep = addSetupStep(`track_event_${eventName}`);
304303

305-
if (!GA_MEASUREMENT_ID || !GA_API_SECRET) {
306-
updateSetupStep(trackingStep, 'skipped', new Error('GA not configured'));
307-
return;
308-
}
309-
310304
// Add retry capability
311305
const maxRetries = 2;
312306
let attempt = 0;
@@ -319,7 +313,7 @@ async function trackEvent(eventName, additionalProps = {}) {
319313
// Get enriched properties
320314
const eventProperties = await enhancedGetTrackingProperties(additionalProps);
321315

322-
// Prepare GA4 payload
316+
// Prepare telemetry payload
323317
const payload = {
324318
client_id: uniqueUserId,
325319
non_personalized_ads: false,
@@ -330,7 +324,7 @@ async function trackEvent(eventName, additionalProps = {}) {
330324
}]
331325
};
332326

333-
// Send to Google Analytics
327+
// Send to telemetry proxy
334328
const postData = JSON.stringify(payload);
335329

336330
const options = {
@@ -341,44 +335,8 @@ async function trackEvent(eventName, additionalProps = {}) {
341335
}
342336
};
343337

344-
const result = await new Promise((resolve, reject) => {
345-
const req = https.request(GA_BASE_URL, options);
346-
347-
// Set timeout to prevent blocking
348-
const timeoutId = setTimeout(() => {
349-
req.destroy();
350-
reject(new Error('Request timeout'));
351-
}, 5000); // Increased timeout to 5 seconds
352-
353-
req.on('error', (error) => {
354-
clearTimeout(timeoutId);
355-
reject(error);
356-
});
357-
358-
req.on('response', (res) => {
359-
clearTimeout(timeoutId);
360-
let data = '';
361-
362-
res.on('data', (chunk) => {
363-
data += chunk;
364-
});
365-
366-
res.on('error', (error) => {
367-
reject(error);
368-
});
369-
370-
res.on('end', () => {
371-
if (res.statusCode >= 200 && res.statusCode < 300) {
372-
resolve({ success: true, data });
373-
} else {
374-
reject(new Error(`HTTP error ${res.statusCode}: ${data}`));
375-
}
376-
});
377-
});
378-
379-
req.write(postData);
380-
req.end();
381-
});
338+
const result = await postTelemetryPayload(postData, options);
339+
if (!result.success) throw new Error('Telemetry proxy request failed');
382340

383341
updateSetupStep(trackingStep, 'completed');
384342
return result;
@@ -397,6 +355,54 @@ async function trackEvent(eventName, additionalProps = {}) {
397355
return false;
398356
}
399357

358+
async function postTelemetryPayload(postData, options) {
359+
for (const endpoint of [TELEMETRY_PROXY_URL, TELEMETRY_PROXY_FALLBACK_URL]) {
360+
const result = await new Promise((resolve) => {
361+
let settled = false;
362+
let timeoutId;
363+
const finish = (result) => {
364+
if (settled) return;
365+
settled = true;
366+
clearTimeout(timeoutId);
367+
resolve(result);
368+
};
369+
const req = https.request(endpoint, options);
370+
371+
timeoutId = setTimeout(() => {
372+
req.destroy();
373+
finish({ success: false, data: '' });
374+
}, 5000);
375+
376+
req.on('error', () => {
377+
finish({ success: false, data: '' });
378+
});
379+
380+
req.on('response', (res) => {
381+
let data = '';
382+
res.on('data', (chunk) => {
383+
data += chunk;
384+
});
385+
res.on('error', () => {
386+
finish({ success: false, data: '' });
387+
});
388+
res.on('end', () => {
389+
finish({ success: res.statusCode >= 200 && res.statusCode < 300, data });
390+
});
391+
res.on('close', () => {
392+
finish({ success: false, data: '' });
393+
});
394+
});
395+
396+
req.write(postData);
397+
req.end();
398+
});
399+
400+
if (result.success) return result;
401+
}
402+
403+
return { success: false, data: '' };
404+
}
405+
400406
// Ensure tracking completes before process exits
401407
async function ensureTrackingCompleted(eventName, additionalProps = {}, timeoutMs = 6000) {
402408
return new Promise(async (resolve) => {
@@ -933,4 +939,4 @@ if (process.argv.length >= 2 && process.argv[1] === fileURLToPath(import.meta.ur
933939
process.exit(1);
934940
}, 1000);
935941
});
936-
}
942+
}

src/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1180,7 +1180,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest)
11801180
// Extract metadata from _meta field if present
11811181
const metadata = request.params._meta as any;
11821182
if (metadata && typeof metadata === 'object') {
1183-
// add remote flag if present (convert to string for GA4)
1183+
// add remote flag if present (convert to string for telemetry)
11841184
if (metadata.remote) {
11851185
telemetryData.remote = String(metadata.remote);
11861186
}

src/utils/capture.ts

Lines changed: 51 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@ try {
1515
let uniqueUserId = 'unknown';
1616

1717
// --- Telemetry Proxy (direct BigQuery ingestion) ---
18-
const TELEMETRY_PROXY_URL = 'https://dc-telemetry-proxy-83847352264.europe-west1.run.app/mp/collect';
19-
const TELEMETRY_PROXY_TOKEN = 'Od44UB_fTrVfGPGRPLr5QdVgFhuKdiGaBmvazTdxVdQ';
18+
// TODO: Move proxy endpoints, auth header setup, request retry/fallback, and
19+
// transport code into a dedicated telemetry utility once this migration lands.
20+
// TODO(security): bearer token was removed, so this endpoint is now unauthenticated.
21+
// Confirm the proxy enforces rate limiting / payload validation server-side,
22+
// otherwise anyone can POST arbitrary events straight into BigQuery ingestion.
23+
const TELEMETRY_PROXY_URL = 'https://telemetry.desktopcommander.app/mp/collect';
24+
const TELEMETRY_PROXY_FALLBACK_URL = 'https://dc-telemetry-proxy-83847352264.europe-west1.run.app/mp/collect';
2025

2126

2227
/**
@@ -54,10 +59,13 @@ export function sanitizeError(error: any): { message: string, code?: string } {
5459
}
5560

5661
/**
57-
* Send an event to Google Analytics
62+
* Send an event to telemetry
5863
* @param event Event name
5964
* @param properties Optional event properties
6065
*/
66+
// TODO(cleanup): captureBase is now dead code — no caller remains after the GA
67+
// removal (only referenced in a comment). It still carries the full GA4-flavored
68+
// send path. Remove it, or repurpose it as the shared proxy transport.
6169
export const captureBase = async (captureURL: string, event: string, properties?: any) => {
6270
try {
6371
// Check if telemetry is enabled in config (defaults to true if not set)
@@ -199,7 +207,7 @@ export const captureBase = async (captureURL: string, event: string, properties?
199207
...sanitizedProperties
200208
};
201209

202-
// Prepare GA4 payload
210+
// Prepare telemetry payload
203211
const payload = {
204212
client_id: uniqueUserId,
205213
non_personalized_ads: false,
@@ -210,7 +218,7 @@ export const captureBase = async (captureURL: string, event: string, properties?
210218
}]
211219
};
212220

213-
// Send data to Google Analytics
221+
// Send data to telemetry endpoint
214222
const postData = JSON.stringify(payload);
215223

216224
const options = {
@@ -232,7 +240,7 @@ export const captureBase = async (captureURL: string, event: string, properties?
232240
const success = res.statusCode === 200 || res.statusCode === 204;
233241
if (!success) {
234242
// Optional debug logging
235-
// console.debug(`GA tracking error: ${res.statusCode} ${data}`);
243+
// console.debug(`Telemetry tracking error: ${res.statusCode} ${data}`);
236244
}
237245
});
238246
});
@@ -256,7 +264,7 @@ export const captureBase = async (captureURL: string, event: string, properties?
256264
};
257265

258266
/**
259-
* Build the standard event properties used by both GA4 and the telemetry proxy.
267+
* Build the standard event properties used by the telemetry proxy.
260268
* Extracted from captureBase so both paths get identical data.
261269
*/
262270
const buildEventProperties = async (properties?: any) => {
@@ -361,8 +369,7 @@ const buildEventProperties = async (properties?: any) => {
361369

362370
/**
363371
* Send event to the telemetry proxy (direct BigQuery ingestion).
364-
* Runs in parallel with GA4 — used for high-volume events to avoid
365-
* the 1M/day GA4 BigQuery export limit.
372+
* Uses the custom domain first and retries the generated Cloud Run URL on failure.
366373
*/
367374
const sendToTelemetryProxy = async (event: string, eventProperties: any) => {
368375
try {
@@ -378,74 +385,61 @@ const sendToTelemetryProxy = async (event: string, eventProperties: any) => {
378385
}]
379386
});
380387

381-
const url = new URL(TELEMETRY_PROXY_URL);
388+
const sent = await postTelemetryPayload(TELEMETRY_PROXY_URL, payload);
389+
if (!sent) {
390+
await postTelemetryPayload(TELEMETRY_PROXY_FALLBACK_URL, payload);
391+
}
392+
} catch {
393+
// Silent fail — telemetry should never break functionality
394+
}
395+
};
396+
397+
const postTelemetryPayload = async (endpoint: string, payload: string): Promise<boolean> => {
398+
return await new Promise((resolve) => {
399+
const url = new URL(endpoint);
382400
const options = {
383401
hostname: url.hostname,
384402
port: 443,
385403
path: url.pathname,
386404
method: 'POST',
387405
headers: {
388406
'Content-Type': 'application/json',
389-
'Authorization': `Bearer ${TELEMETRY_PROXY_TOKEN}`,
390407
'Content-Length': Buffer.byteLength(payload)
391408
}
392409
};
393410

394411
const req = https.request(options, (res) => {
395-
res.resume(); // drain response
412+
res.resume();
413+
res.on('end', () => resolve(res.statusCode !== undefined && res.statusCode >= 200 && res.statusCode < 300));
414+
});
415+
req.on('error', () => resolve(false));
416+
req.setTimeout(3000, () => {
417+
req.destroy();
418+
resolve(false);
396419
});
397-
req.on('error', () => { }); // silent fail
398-
req.setTimeout(3000, () => req.destroy());
399420
req.write(payload);
400421
req.end();
401-
} catch {
402-
// Silent fail — telemetry should never break functionality
403-
}
422+
});
404423
};
405424

406-
export const capture_call_tool = async (event: string, properties?: any) => {
407-
// Old property (G-8L163XZ1CE) — keeps lower-volume tool events
408-
const GA_OLD_ID = 'G-8L163XZ1CE';
409-
const GA_OLD_SECRET = 'hNxh4TK2TnSy4oLZn4RwTA';
410-
const GA_OLD_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_OLD_ID}&api_secret=${GA_OLD_SECRET}`;
411-
412-
// New property (dc_high_volume) — receives highest-volume tool events to avoid 1M/day BQ export limit
413-
const GA_NEW_ID = 'G-ZDF1M5403Z';
414-
const GA_NEW_SECRET = 'cUEilpa0SpWfc2UjblDtKQ';
415-
const GA_NEW_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_NEW_ID}&api_secret=${GA_NEW_SECRET}`;
416-
417-
// Route highest-volume tools to new property, rest to old
418-
const HIGH_VOLUME_TOOLS = ['start_process', 'track_ui_event'];
419-
const toolName = properties?.tool_name ?? properties?.name;
420-
const gaUrl = HIGH_VOLUME_TOOLS.includes(toolName) ? GA_NEW_URL : GA_OLD_URL;
421-
422-
// Build properties once, send to GA4 + telemetry proxy in parallel
423-
const eventProperties = await buildEventProperties(properties);
424-
await Promise.all([
425-
captureBase(gaUrl, event, properties), // GA4 (routed by tool name)
426-
sendToTelemetryProxy(event, eventProperties), // direct BigQuery (all events)
427-
]);
428-
}
429-
425+
// TODO(behavior): capture() is now fire-and-forget — every `await capture(...)`
426+
// call site resolves before the network send completes. Fine for the long-running
427+
// MCP server, but events fired right before process exit (e.g. opt-out, feedback)
428+
// can be silently dropped. If we need delivery guarantees on short-lived paths,
429+
// expose an awaitable variant or flush-before-exit hook.
430430
export const capture = async (event: string, properties?: any) => {
431-
const GA_MEASUREMENT_ID = 'G-F3GK01G39Y';
432-
const GA_API_SECRET = 'SqdcIAweSQS1RQErURMdEA';
433-
const GA_BASE_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
434-
435-
// Build properties once, send to both GA4 and telemetry proxy in parallel
436-
const eventProperties = await buildEventProperties(properties);
437-
await Promise.all([
438-
captureBase(GA_BASE_URL, event, properties), // existing GA4
439-
sendToTelemetryProxy(event, eventProperties), // new: direct BigQuery
440-
]);
431+
void (async () => {
432+
try {
433+
const eventProperties = await buildEventProperties(properties);
434+
await sendToTelemetryProxy(event, eventProperties);
435+
} catch {
436+
// Silent fail — telemetry should never break functionality
437+
}
438+
})();
441439
}
442440

443-
export const capture_ui_event = async (event: string, properties?: any) => {
444-
const GA_MEASUREMENT_ID = 'G-MPFSWEGQ0T';
445-
const GA_API_SECRET = 'BeK3uyAOQ6-TK6wnaDG2Ww';
446-
const GA_BASE_URL = `https://www.google-analytics.com/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}`;
447-
return await captureBase(GA_BASE_URL, event, properties);
448-
}
441+
export const capture_call_tool = capture;
442+
export const capture_ui_event = capture;
449443

450444
/**
451445
* Wrapper for capture() that automatically adds remote flag for remote-device telemetry

0 commit comments

Comments
 (0)