trackly-cli 0.21.1 → 0.21.2

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.
package/README.md CHANGED
@@ -353,7 +353,9 @@ a missing capability. Run `trackly mcp` or use
353
353
 
354
354
  MCP usage analytics are on by default and relay through Trackly so the free
355
355
  service can be debugged and improved. They exclude résumé text, profile answers,
356
- demographic or work-authorization answers, and application notes. Turn off future
356
+ demographic or work-authorization answers, and application notes. Client attribution
357
+ uses a fixed family label; errors use categories, and search text is retained only
358
+ as length. Raw queries and conversation text are excluded. Turn off future
357
359
  collection with **Share usage analytics** in Account Settings. Operators may also
358
360
  disable local MCP instrumentation with `TRACKLY_MCP_ANALYTICS_DISABLED=1` (or
359
361
  `TRACKLY_MCP_ANALYTICS_ENABLED=0`); the account setting remains authoritative
@@ -37,3 +37,17 @@ Verification: `node --test test/mcp-analytics.test.js test/mcp-schema.test.js`
37
37
  and `npm test`. The real transport regression failed with the old instrumentation
38
38
  and passed after the fix. Keyword telemetry continues to retain length only;
39
39
  this change does not permit raw query or context storage.
40
+
41
+ Error and client attribution also need real SDK coverage. The SDK's error type
42
+ may be a JavaScript constructor name, and its vendor field comes from HTTP
43
+ headers that stdio does not have. Normalize both at the privacy boundary:
44
+ derive a closed error category from the failure and a closed client family from
45
+ available metadata, with explicit `none`, `other` and `unknown` states.
46
+
47
+ The HTTP client rejects response records containing numeric status and an
48
+ `error` field. Capturing only `message` loses that status and substitutes a
49
+ generic fallback. Preserve the status in the telemetry-only exception and give
50
+ it precedence over endpoint words such as "network brief". Verify job detail
51
+ and brief calls through a local HTTP fixture with 404, 429 and 500 responses;
52
+ assert unchanged tool results and classified, content-free telemetry. Sanitize
53
+ the result twice because the SDK hook and backend relay both project events.
package/mcp/analytics.js CHANGED
@@ -118,6 +118,28 @@ const SAFE_ENUM_KEYS = new Set([
118
118
  'region', 'regiontag', 'locationfilter', 'sort', 'sponsorship',
119
119
  ]);
120
120
  const SDK_FAILURE_WARNING = /Warning: (?:Failed to instrument server|Failed to setup tool call instrumentation|No PostHog client passed)/i;
121
+ const ERROR_CATEGORIES = new Set([
122
+ 'authentication', 'authorization', 'rate_limit', 'timeout', 'not_found',
123
+ 'validation', 'conflict', 'network', 'server', 'unknown',
124
+ ]);
125
+ const HTTP_ERROR_CATEGORIES = {
126
+ 400: 'validation', 401: 'authentication', 403: 'authorization',
127
+ 404: 'not_found', 408: 'timeout', 409: 'conflict', 410: 'not_found',
128
+ 413: 'validation', 422: 'validation', 429: 'rate_limit',
129
+ };
130
+ const CLIENT_FAMILIES = [
131
+ ['chatgpt', /\bchatgpt\b/],
132
+ ['claude', /\bclaude(?: desktop)?\b/],
133
+ ['codex', /\bcodex\b/],
134
+ ['cursor', /\bcursor\b/],
135
+ ['windsurf', /\bwindsurf\b/],
136
+ ['vscode', /\b(?:vscode|visual studio code)\b/],
137
+ ['continue', /\bcontinue\b/],
138
+ ['cline', /\bcline\b/],
139
+ ['roo', /\broo(?: code)?\b/],
140
+ ['zed', /\bzed\b/],
141
+ ['mcp-inspector', /\bmcp inspector\b/],
142
+ ];
121
143
 
122
144
  const ALLOWED_EVENT_PROPERTY_KEYS = new Set([
123
145
  '$exception_list', '$lib', '$lib_version', '$mcp_client_name',
@@ -250,6 +272,13 @@ function classifyIntent(toolName, value) {
250
272
 
251
273
  function classifyError(value) {
252
274
  const text = typeof value === 'string' ? value.toLowerCase() : '';
275
+ if (ERROR_CATEGORIES.has(text)) return text;
276
+ // HTTP status is authoritative; endpoint words such as "network brief"
277
+ // must not turn an upstream 500 into a transport/network classification.
278
+ const status = /\bhttp\s+([45]\d\d)\b/.exec(text)?.[1];
279
+ if (status && HTTP_ERROR_CATEGORIES[status]) return HTTP_ERROR_CATEGORIES[status];
280
+ if (status?.startsWith('5')) return 'server';
281
+ if (status) return 'unknown';
253
282
  if (/\b(?:401|unauthenticated|authentication|invalid token|expired token)\b/.test(text)) {
254
283
  return 'authentication';
255
284
  }
@@ -264,6 +293,38 @@ function classifyError(value) {
264
293
  return 'unknown';
265
294
  }
266
295
 
296
+ function canonicalClientFamily(value) {
297
+ if (typeof value !== 'string' || !value.trim() || value.length > 240) return undefined;
298
+ const normalized = value.toLowerCase().replace(/[_-]/g, ' ');
299
+ return CLIENT_FAMILIES.find(([, pattern]) => pattern.test(normalized))?.[0];
300
+ }
301
+
302
+ function projectEventAttribution(properties, eventName) {
303
+ const clientValues = [
304
+ properties.$mcp_vendor_client,
305
+ properties.$mcp_client_name,
306
+ properties.$mcp_client_user_agent,
307
+ ];
308
+ const clientFamily = clientValues.map(canonicalClientFamily).find(Boolean)
309
+ || (clientValues.some((value) => (
310
+ typeof value === 'string' && value.trim() && value !== 'unknown'
311
+ )) ? 'other' : 'unknown');
312
+ properties.$mcp_vendor_client = clientFamily;
313
+ properties.$mcp_client_name = clientFamily;
314
+ if (properties.$mcp_client_user_agent !== undefined) {
315
+ properties.$mcp_client_user_agent = canonicalClientFamily(properties.$mcp_client_user_agent)
316
+ || 'other';
317
+ }
318
+
319
+ const errorMessage = properties.$mcp_error_message;
320
+ const exceptionMessage = properties.$exception_list?.[0]?.value;
321
+ const isError = eventName === '$exception' || properties.$mcp_is_error === true;
322
+ properties.$mcp_error_type = isError
323
+ ? classifyError(errorMessage ?? exceptionMessage ?? properties.$mcp_error_type)
324
+ : 'none';
325
+ return errorMessage;
326
+ }
327
+
267
328
  function safeFrameName(value, maxLength = 120) {
268
329
  if (typeof value !== 'string' || value === '<local-path>') return undefined;
269
330
  const basename = value.replace(/\\/g, '/').split('/').pop() || '';
@@ -311,7 +372,7 @@ function projectExceptionList(value, errorValue) {
311
372
  }).filter((frame) => Object.keys(frame).length > 0);
312
373
  return {
313
374
  type: safeFrameName(exception.type, 80) || 'Error',
314
- category: classifyError(exception.value ?? errorValue),
375
+ category: classifyError(exception.category ?? exception.value ?? errorValue),
315
376
  stacktrace: { frames: projectedFrames },
316
377
  };
317
378
  });
@@ -521,6 +582,7 @@ function sanitizeMcpAnalyticsEvent(event) {
521
582
  }
522
583
  const properties = sanitized.properties;
523
584
  if (!properties || typeof properties !== 'object') return sanitized;
585
+ const sourceErrorMessage = projectEventAttribution(properties, event.event);
524
586
 
525
587
  // Identity is backend-owned. The CLI may carry only its ephemeral anonymous
526
588
  // session identifier; verified user/person properties never cross this relay.
@@ -582,7 +644,6 @@ function sanitizeMcpAnalyticsEvent(event) {
582
644
  delete properties.$mcp_intent_source;
583
645
  }
584
646
 
585
- const sourceErrorMessage = properties.$mcp_error_message;
586
647
  if (typeof sourceErrorMessage === 'string') {
587
648
  properties.$mcp_error_message = classifyError(sourceErrorMessage);
588
649
  }
package/mcp/server.js CHANGED
@@ -232,9 +232,13 @@ function wrapTool(handler, fallbackMessage) {
232
232
  };
233
233
  } catch (error) {
234
234
  if (extra && typeof extra === 'object') {
235
+ const statusPrefix = Number.isInteger(error?.status) ? `HTTP ${error.status}: ` : '';
235
236
  extra.__mcp_analytics_error = error instanceof Error
236
237
  ? error
237
- : new Error(error?.message || fallbackMessage, { cause: error });
238
+ : new Error(
239
+ `${statusPrefix}${error?.error || error?.message || fallbackMessage}`,
240
+ { cause: error },
241
+ );
238
242
  }
239
243
  return createErrorResult(
240
244
  error,
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "trackly-cli",
3
- "version": "0.21.1",
3
+ "version": "0.21.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "trackly-cli",
9
- "version": "0.21.1",
9
+ "version": "0.21.2",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@modelcontextprotocol/sdk": "1.32.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackly-cli",
3
- "version": "0.21.1",
3
+ "version": "0.21.2",
4
4
  "mcpName": "io.github.trackly-app/trackly",
5
5
  "description": "Search 250K+ jobs across 5,400+ companies with the Trackly CLI and MCP server for Claude, ChatGPT, Codex and Cursor. Filter by role, location, salary and sponsorship; track jobs and prepare applications.",
6
6
  "bin": {
package/server.json CHANGED
@@ -17,12 +17,12 @@
17
17
  ]
18
18
  }
19
19
  ],
20
- "version": "0.21.1",
20
+ "version": "0.21.2",
21
21
  "packages": [
22
22
  {
23
23
  "registryType": "npm",
24
24
  "identifier": "trackly-cli",
25
- "version": "0.21.1",
25
+ "version": "0.21.2",
26
26
  "runtimeHint": "npx",
27
27
  "runtimeArguments": [
28
28
  {