@scanwarp/core 0.2.0 → 0.3.0

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.
@@ -1,8 +1,14 @@
1
- import type { Event, Monitor, DiagnosisResult } from './types.js';
1
+ import type { Event, Monitor, DiagnosisResult, TraceSpan } from './types.js';
2
2
  interface DiagnoserConfig {
3
3
  apiKey: string;
4
4
  model?: string;
5
5
  }
6
+ interface ProviderStatusContext {
7
+ provider: string;
8
+ displayName: string;
9
+ status: string;
10
+ description: string | null;
11
+ }
6
12
  interface DiagnosisContext {
7
13
  events: Event[];
8
14
  monitor?: Monitor;
@@ -11,7 +17,10 @@ interface DiagnosisContext {
11
17
  status: string;
12
18
  message: string;
13
19
  }>;
20
+ traces?: TraceSpan[];
21
+ providerStatuses?: ProviderStatusContext[];
14
22
  }
23
+ export { type DiagnosisContext };
15
24
  export declare class Diagnoser {
16
25
  private client;
17
26
  private model;
@@ -23,4 +32,3 @@ export declare class Diagnoser {
23
32
  private parseResponse;
24
33
  private normalizeSeverity;
25
34
  }
26
- export {};
package/dist/diagnoser.js CHANGED
@@ -20,7 +20,7 @@ class Diagnoser {
20
20
  model: this.model,
21
21
  max_tokens: 2000,
22
22
  temperature: 0.3,
23
- system: this.getSystemPrompt(),
23
+ system: this.getSystemPrompt(context),
24
24
  messages: [
25
25
  {
26
26
  role: 'user',
@@ -34,43 +34,69 @@ class Diagnoser {
34
34
  }
35
35
  return this.parseResponse(content.text);
36
36
  }
37
- getSystemPrompt() {
38
- return `You are a senior engineering mentor helping developers who built their application using AI coding tools like Cursor or Claude Code. These developers may not have deep infrastructure knowledge or be familiar with reading stack traces.
39
-
40
- Your job is to:
41
- 1. Explain what went wrong in plain, conversational English (no jargon)
42
- 2. Explain WHY it happened in a way a non-expert can understand
43
- 3. Provide a clear, actionable fix in plain language
44
- 4. Write a ready-to-paste prompt they can give to their AI coding assistant to fix the issue
45
-
46
- Think of yourself as a patient mentor who's explaining a production issue to someone smart but new to production systems.
47
-
48
- IMPORTANT RULES:
49
- - NO technical jargon without explanation
50
- - NO raw stack traces in your response
51
- - Use analogies when helpful
52
- - Be encouraging, not condescending
53
- - Focus on "what to do" not "what you did wrong"
54
-
55
- Respond in this exact JSON format:
56
- {
57
- "root_cause": "1-2 sentence plain English explanation of what broke",
58
- "severity": "critical|warning|info",
59
- "suggested_fix": "Plain English explanation of how to fix it (2-4 sentences)",
60
- "fix_prompt": "A complete, copy-pasteable prompt for Cursor/Claude Code that will fix this issue"
61
- }
62
-
63
- The fix_prompt should be detailed and include:
64
- - What file(s) to modify
65
- - What specific changes to make
66
- - Any environment variables or config needed
67
- - How to test the fix
68
-
37
+ getSystemPrompt(context) {
38
+ const hasTraces = context.traces && context.traces.length > 0;
39
+ const hasProviderIssues = context.providerStatuses && context.providerStatuses.length > 0;
40
+ let prompt = `You are a senior engineering mentor helping developers who built their application using AI coding tools like Cursor or Claude Code. These developers may not have deep infrastructure knowledge or be familiar with reading stack traces.
41
+
42
+ Your job is to:
43
+ 1. Explain what went wrong in plain, conversational English (no jargon)
44
+ 2. Explain WHY it happened in a way a non-expert can understand
45
+ 3. Provide a clear, actionable fix in plain language
46
+ 4. Write a ready-to-paste prompt they can give to their AI coding assistant to fix the issue${hasTraces ? '\n5. Identify the specific span (operation) that is the bottleneck or root cause' : ''}
47
+
48
+ Think of yourself as a patient mentor who's explaining a production issue to someone smart but new to production systems.
49
+
50
+ IMPORTANT RULES:
51
+ - NO technical jargon without explanation
52
+ - NO raw stack traces in your response
53
+ - Use analogies when helpful
54
+ - Be encouraging, not condescending
55
+ - Focus on "what to do" not "what you did wrong"${hasTraces ? '\n- When trace data is available, use it to pinpoint the EXACT operation that failed or is slow' : ''}`;
56
+ if (hasProviderIssues) {
57
+ prompt += `
58
+
59
+ PROVIDER OUTAGE RULES (CRITICAL follow these when provider status data shows a non-operational provider):
60
+ - If the issue correlates with a provider that is currently experiencing an outage or degraded performance, clearly state that the issue is CAUSED BY the provider outage, NOT a bug in the user's code
61
+ - The root_cause MUST mention the provider by name and their current status (e.g. "This is caused by a Vercel outage, not a bug in your code")
62
+ - The suggested_fix should focus on: (1) waiting for the provider to recover, (2) checking the provider's status page, and (3) any temporary workarounds
63
+ - Do NOT suggest code fixes for issues caused by provider outages — it's not the user's fault
64
+ - The fix_prompt should suggest adding resilience improvements (retry logic, fallbacks, circuit breakers) as an OPTIONAL improvement, not as a bug fix`;
65
+ }
66
+ prompt += `
67
+
68
+ Respond in this exact JSON format:
69
+ {
70
+ "root_cause": "1-2 sentence plain English explanation of what broke",
71
+ "severity": "critical|warning|info",
72
+ "suggested_fix": "Plain English explanation of how to fix it (2-4 sentences)",
73
+ "fix_prompt": "A complete, copy-pasteable prompt for Cursor/Claude Code that will fix this issue"${hasTraces ? `,
74
+ "bottleneck_span": "name of the span that is the root cause (e.g. 'stripe: payment_intents.create' or 'pg: SELECT * FROM users')",
75
+ "trace_id": "the trace_id of the most relevant trace"` : ''}
76
+ }
77
+
78
+ The fix_prompt should be detailed and include:
79
+ - What file(s) to modify
80
+ - What specific changes to make
81
+ - Any environment variables or config needed
82
+ - How to test the fix
83
+
69
84
  Make the fix_prompt actionable enough that an AI coding assistant can implement it without asking follow-up questions.`;
85
+ return prompt;
70
86
  }
71
87
  buildPrompt(context) {
72
- const { events, monitor, recentHistory } = context;
88
+ const { events, monitor, recentHistory, traces, providerStatuses } = context;
73
89
  let prompt = '## Production Issue Detected\n\n';
90
+ // Provider status section — show this first so the AI sees it immediately
91
+ if (providerStatuses && providerStatuses.length > 0) {
92
+ prompt += '**⚠️ Provider Status (current):**\n';
93
+ for (const ps of providerStatuses) {
94
+ const statusLabel = ps.status === 'operational' ? '✅ operational' : `🔴 ${ps.status}`;
95
+ const detail = ps.description ? ` — ${ps.description}` : '';
96
+ prompt += `- ${ps.displayName}: ${statusLabel}${detail}\n`;
97
+ }
98
+ prompt += '\nNote: One or more infrastructure providers are experiencing issues. Consider whether this incident is caused by the provider outage rather than a code bug.\n\n';
99
+ }
74
100
  // Add monitor context if available
75
101
  if (monitor) {
76
102
  prompt += `**Service:** ${monitor.url}\n`;
@@ -89,6 +115,16 @@ Make the fix_prompt actionable enough that an AI coding assistant can implement
89
115
  }
90
116
  prompt += '\n';
91
117
  }
118
+ // Add trace waterfall if available
119
+ if (traces && traces.length > 0) {
120
+ const waterfall = buildTraceWaterfall(traces);
121
+ if (waterfall) {
122
+ prompt += `\n**Request Traces (from OpenTelemetry instrumentation):**\n`;
123
+ prompt += `These traces show the exact sequence of operations your app performed during the failing request(s).\n\n`;
124
+ prompt += waterfall;
125
+ prompt += '\n';
126
+ }
127
+ }
92
128
  // Add recent history if available
93
129
  if (recentHistory && recentHistory.length > 0) {
94
130
  prompt += `\n**Recent History (last 24 hours):**\n`;
@@ -113,6 +149,14 @@ Make the fix_prompt actionable enough that an AI coding assistant can implement
113
149
  'message',
114
150
  'type',
115
151
  'source',
152
+ 'trace_id',
153
+ 'span_id',
154
+ 'service_name',
155
+ 'operation_name',
156
+ 'duration_ms',
157
+ 'status_message',
158
+ 'db_system',
159
+ 'db_statement',
116
160
  ];
117
161
  for (const field of relevantFields) {
118
162
  if (field in data) {
@@ -134,6 +178,8 @@ Make the fix_prompt actionable enough that an AI coding assistant can implement
134
178
  severity: this.normalizeSeverity(parsed.severity),
135
179
  suggested_fix: parsed.suggested_fix || 'No fix suggested',
136
180
  fix_prompt: parsed.fix_prompt || 'No fix prompt provided',
181
+ bottleneck_span: parsed.bottleneck_span || undefined,
182
+ trace_id: parsed.trace_id || undefined,
137
183
  };
138
184
  }
139
185
  catch (error) {
@@ -157,3 +203,126 @@ Make the fix_prompt actionable enough that an AI coding assistant can implement
157
203
  }
158
204
  }
159
205
  exports.Diagnoser = Diagnoser;
206
+ /**
207
+ * Build a human-readable waterfall view of traces, grouped by trace_id.
208
+ * Each trace shows the root span and its children as an indented tree.
209
+ */
210
+ function buildTraceWaterfall(spans) {
211
+ // Group spans by trace_id
212
+ const traceMap = new Map();
213
+ for (const span of spans) {
214
+ const group = traceMap.get(span.trace_id) || [];
215
+ group.push(span);
216
+ traceMap.set(span.trace_id, group);
217
+ }
218
+ const sections = [];
219
+ for (const [traceId, traceSpans] of traceMap) {
220
+ // Sort by start_time
221
+ traceSpans.sort((a, b) => a.start_time - b.start_time);
222
+ // Build a parent → children index
223
+ const childrenMap = new Map();
224
+ for (const span of traceSpans) {
225
+ const parentKey = span.parent_span_id;
226
+ const siblings = childrenMap.get(parentKey) || [];
227
+ siblings.push(span);
228
+ childrenMap.set(parentKey, siblings);
229
+ }
230
+ // Find root spans (no parent or parent not in this trace)
231
+ const spanIds = new Set(traceSpans.map((s) => s.span_id));
232
+ const roots = traceSpans.filter((s) => !s.parent_span_id || !spanIds.has(s.parent_span_id));
233
+ if (roots.length === 0)
234
+ continue;
235
+ let section = `\`\`\`\nTrace ${traceId}\n`;
236
+ for (const root of roots) {
237
+ section += renderSpanTree(root, childrenMap, '', true);
238
+ }
239
+ section += '```\n';
240
+ sections.push(section);
241
+ // Limit to 5 traces to keep prompt size reasonable
242
+ if (sections.length >= 5)
243
+ break;
244
+ }
245
+ return sections.join('\n');
246
+ }
247
+ /**
248
+ * Render a single span and its children as an indented tree.
249
+ */
250
+ function renderSpanTree(span, childrenMap, prefix, isLast) {
251
+ const status = formatSpanStatus(span);
252
+ const label = formatSpanLabel(span);
253
+ const connector = prefix === '' ? '' : isLast ? '└─ ' : '├─ ';
254
+ let line = `${prefix}${connector}${label} (${span.duration_ms}ms) ${status}\n`;
255
+ // Add key attributes on a sub-line for context
256
+ const detail = formatSpanDetail(span);
257
+ if (detail) {
258
+ const detailPrefix = prefix === '' ? ' ' : prefix + (isLast ? ' ' : '│ ');
259
+ line += `${detailPrefix}${detail}\n`;
260
+ }
261
+ // Render children
262
+ const children = childrenMap.get(span.span_id) || [];
263
+ children.sort((a, b) => a.start_time - b.start_time);
264
+ const childPrefix = prefix === '' ? '' : prefix + (isLast ? ' ' : '│ ');
265
+ for (let i = 0; i < children.length; i++) {
266
+ line += renderSpanTree(children[i], childrenMap, childPrefix, i === children.length - 1);
267
+ }
268
+ return line;
269
+ }
270
+ function formatSpanLabel(span) {
271
+ const attrs = span.attributes;
272
+ // Database spans: show db.system + operation
273
+ if (attrs['db.system']) {
274
+ const stmt = attrs['db.statement'];
275
+ if (typeof stmt === 'string') {
276
+ const truncated = stmt.length > 80 ? stmt.substring(0, 80) + '...' : stmt;
277
+ return `${attrs['db.system']}: ${truncated}`;
278
+ }
279
+ return `${attrs['db.system']}: ${span.operation_name}`;
280
+ }
281
+ // HTTP spans: show method + route/target
282
+ const method = attrs['http.method'] || attrs['http.request.method'];
283
+ const route = attrs['http.route'] || attrs['http.target'] || attrs['url.path'];
284
+ if (method && route) {
285
+ return `${method} ${route}`;
286
+ }
287
+ return span.operation_name;
288
+ }
289
+ function formatSpanStatus(span) {
290
+ if (span.status_code === 'ERROR') {
291
+ const msg = span.status_message || 'error';
292
+ return `✗ ${msg}`;
293
+ }
294
+ if (span.status_code === 'OK') {
295
+ return '✓';
296
+ }
297
+ // UNSET — infer from http.status_code if available
298
+ const httpStatus = span.attributes['http.status_code'] || span.attributes['http.response.status_code'];
299
+ if (typeof httpStatus === 'number' && httpStatus >= 400) {
300
+ return `✗ HTTP ${httpStatus}`;
301
+ }
302
+ return '✓';
303
+ }
304
+ function formatSpanDetail(span) {
305
+ const parts = [];
306
+ const attrs = span.attributes;
307
+ // Error message from span events
308
+ if (span.status_code === 'ERROR') {
309
+ const exceptionEvent = span.events.find((e) => e.name === 'exception');
310
+ if (exceptionEvent?.attributes) {
311
+ const msg = exceptionEvent.attributes['exception.message'];
312
+ if (typeof msg === 'string') {
313
+ parts.push(`error: ${msg.length > 120 ? msg.substring(0, 120) + '...' : msg}`);
314
+ }
315
+ }
316
+ }
317
+ // HTTP status
318
+ const httpStatus = attrs['http.status_code'] || attrs['http.response.status_code'];
319
+ if (httpStatus) {
320
+ parts.push(`status: ${httpStatus}`);
321
+ }
322
+ // DB statement (if not already in label, show here for children)
323
+ if (attrs['db.statement'] && !attrs['db.system']) {
324
+ const stmt = String(attrs['db.statement']);
325
+ parts.push(stmt.length > 80 ? stmt.substring(0, 80) + '...' : stmt);
326
+ }
327
+ return parts.join(' | ');
328
+ }
package/dist/types.d.ts CHANGED
@@ -7,12 +7,12 @@ export interface Monitor {
7
7
  status: 'up' | 'down' | 'unknown';
8
8
  created_at: Date;
9
9
  }
10
- export type EventSource = 'monitor' | 'vercel' | 'stripe' | 'supabase' | 'github' | 'provider-status';
10
+ export type EventSource = 'monitor' | 'vercel' | 'stripe' | 'supabase' | 'github' | 'provider-status' | 'otel';
11
11
  export interface Event {
12
12
  id: string;
13
13
  project_id: string;
14
14
  monitor_id?: string;
15
- type: 'error' | 'slow' | 'down' | 'up';
15
+ type: 'error' | 'slow' | 'down' | 'up' | 'trace_error' | 'slow_query';
16
16
  source: EventSource;
17
17
  message: string;
18
18
  raw_data?: Record<string, unknown>;
@@ -60,6 +60,25 @@ export interface DiagnosisResult {
60
60
  severity: 'critical' | 'warning' | 'info';
61
61
  suggested_fix: string;
62
62
  fix_prompt: string;
63
+ bottleneck_span?: string;
64
+ trace_id?: string;
65
+ }
66
+ export interface TraceSpan {
67
+ trace_id: string;
68
+ span_id: string;
69
+ parent_span_id: string | null;
70
+ service_name: string;
71
+ operation_name: string;
72
+ kind: string;
73
+ start_time: number;
74
+ duration_ms: number;
75
+ status_code: string | null;
76
+ status_message: string | null;
77
+ attributes: Record<string, unknown>;
78
+ events: Array<{
79
+ name: string;
80
+ attributes?: Record<string, unknown>;
81
+ }>;
63
82
  }
64
83
  export interface ProviderStatus {
65
84
  provider: string;
package/package.json CHANGED
@@ -1,44 +1,47 @@
1
- {
2
- "name": "@scanwarp/core",
3
- "version": "0.2.0",
4
- "description": "Shared types and logic for ScanWarp monitoring",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "files": [
8
- "dist"
9
- ],
10
- "scripts": {
11
- "build": "tsc",
12
- "dev": "tsc --watch",
13
- "typecheck": "tsc --noEmit",
14
- "clean": "rm -rf dist",
15
- "prepublishOnly": "npm run build"
16
- },
17
- "keywords": [
18
- "monitoring",
19
- "observability",
20
- "ai",
21
- "claude",
22
- "diagnosis",
23
- "production",
24
- "incidents"
25
- ],
26
- "author": "ScanWarp",
27
- "license": "MIT",
28
- "repository": {
29
- "type": "git",
30
- "url": "https://github.com/scanwarp/scanwarp.git",
31
- "directory": "packages/core"
32
- },
33
- "bugs": {
34
- "url": "https://github.com/scanwarp/scanwarp/issues"
35
- },
36
- "homepage": "https://scanwarp.com",
37
- "devDependencies": {
38
- "@types/node": "^20.11.0",
39
- "typescript": "^5.3.3"
40
- },
41
- "dependencies": {
42
- "@anthropic-ai/sdk": "^0.74.0"
43
- }
44
- }
1
+ {
2
+ "name": "@scanwarp/core",
3
+ "version": "0.3.0",
4
+ "description": "Shared types and logic for ScanWarp monitoring",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "dev": "tsc --watch",
13
+ "typecheck": "tsc --noEmit",
14
+ "clean": "rm -rf dist",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "keywords": [
18
+ "monitoring",
19
+ "observability",
20
+ "ai",
21
+ "claude",
22
+ "diagnosis",
23
+ "production",
24
+ "incidents"
25
+ ],
26
+ "author": "ScanWarp",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/scanwarp/scanwarp.git",
31
+ "directory": "packages/core"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/scanwarp/scanwarp/issues"
35
+ },
36
+ "homepage": "https://scanwarp.com",
37
+ "publishConfig": {
38
+ "access": "public"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20.11.0",
42
+ "typescript": "^5.3.3"
43
+ },
44
+ "dependencies": {
45
+ "@anthropic-ai/sdk": "^0.74.0"
46
+ }
47
+ }