scanwarp 0.3.3 → 0.4.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.
- package/dist/commands/dev.js +86 -17
- package/dist/commands/mcp.js +38 -1
- package/dist/dev/browser-monitor-script.js +113 -0
- package/dist/integrations/mcp.js +3 -1
- package/package.json +1 -1
package/dist/commands/dev.js
CHANGED
|
@@ -14,7 +14,23 @@ const chokidar_1 = require("chokidar");
|
|
|
14
14
|
const detector_js_1 = require("../detector.js");
|
|
15
15
|
const analysis_engine_js_1 = require("../dev/analysis-engine.js");
|
|
16
16
|
const schema_drift_js_1 = require("../dev/analyzers/schema-drift.js");
|
|
17
|
+
const browser_monitor_script_js_1 = require("../dev/browser-monitor-script.js");
|
|
17
18
|
// ─── Main dev command ───
|
|
19
|
+
// ─── Helper functions ───
|
|
20
|
+
function hasInstrumentInPackageJson(cwd) {
|
|
21
|
+
const packageJsonPath = path_1.default.join(cwd, 'package.json');
|
|
22
|
+
if (!fs_1.default.existsSync(packageJsonPath)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf-8'));
|
|
27
|
+
return !!(packageJson.dependencies?.['@scanwarp/instrument'] ||
|
|
28
|
+
packageJson.devDependencies?.['@scanwarp/instrument']);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
18
34
|
// ─── Production setup detection ───
|
|
19
35
|
function checkProductionSetup(cwd) {
|
|
20
36
|
// Check if they've deployed or are using hosting platforms
|
|
@@ -29,19 +45,7 @@ function checkProductionSetup(cwd) {
|
|
|
29
45
|
// Check if instrumentation is set up
|
|
30
46
|
const hasInstrumentationFile = fs_1.default.existsSync(path_1.default.join(cwd, 'instrumentation.ts')) ||
|
|
31
47
|
fs_1.default.existsSync(path_1.default.join(cwd, 'instrumentation.js'));
|
|
32
|
-
|
|
33
|
-
let hasInstrumentPackage = false;
|
|
34
|
-
const packageJsonPath = path_1.default.join(cwd, 'package.json');
|
|
35
|
-
if (fs_1.default.existsSync(packageJsonPath)) {
|
|
36
|
-
try {
|
|
37
|
-
const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf-8'));
|
|
38
|
-
hasInstrumentPackage = !!(packageJson.dependencies?.['@scanwarp/instrument'] ||
|
|
39
|
-
packageJson.devDependencies?.['@scanwarp/instrument']);
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
// Ignore parse errors
|
|
43
|
-
}
|
|
44
|
-
}
|
|
48
|
+
const hasInstrumentPackage = hasInstrumentInPackageJson(cwd);
|
|
45
49
|
return hasInstrumentationFile || hasInstrumentPackage;
|
|
46
50
|
}
|
|
47
51
|
async function devCommand(options = {}) {
|
|
@@ -87,12 +91,18 @@ async function devCommand(options = {}) {
|
|
|
87
91
|
console.log(chalk_1.default.white(` }`));
|
|
88
92
|
console.log(chalk_1.default.gray(` }`));
|
|
89
93
|
console.log(chalk_1.default.gray(` }\n`));
|
|
94
|
+
// Print browser monitoring instructions
|
|
95
|
+
console.log(chalk_1.default.bold(' 🔍 Browser error monitoring:\n'));
|
|
96
|
+
console.log(chalk_1.default.gray(` Add this script tag to your HTML <head> for frontend error capture:`));
|
|
97
|
+
console.log(chalk_1.default.cyan(` <script src="http://localhost:${scanwarpPort}/monitor.js"></script>\n`));
|
|
98
|
+
console.log(chalk_1.default.gray(` This will detect blank screens, console errors, and React issues.\n`));
|
|
90
99
|
// Step 5: Start the user's dev server
|
|
91
100
|
const isNextJs = detected.framework === 'Next.js';
|
|
101
|
+
const hasInstrumentPackage = hasInstrumentInPackageJson(cwd);
|
|
92
102
|
const devServerPort = detectDevServerPort(devCmd);
|
|
93
103
|
console.log(chalk_1.default.bold(` Starting: ${devCmd}\n`));
|
|
94
104
|
console.log(chalk_1.default.gray('─'.repeat(60)));
|
|
95
|
-
const child = startDevServer(devCmd, cwd, scanwarpPort, isNextJs);
|
|
105
|
+
const child = startDevServer(devCmd, cwd, scanwarpPort, isNextJs, hasInstrumentPackage);
|
|
96
106
|
// Use store's maps for tracking (shared with MCP API)
|
|
97
107
|
const previousResults = store.previousResults;
|
|
98
108
|
const baselines = store.baselines;
|
|
@@ -313,6 +323,7 @@ async function startLocalServer(port) {
|
|
|
313
323
|
const store = {
|
|
314
324
|
spans: [],
|
|
315
325
|
events: [],
|
|
326
|
+
browserErrors: [],
|
|
316
327
|
liveLogEnabled: false,
|
|
317
328
|
analysisEngine: new analysis_engine_js_1.AnalysisEngine(),
|
|
318
329
|
routes: { pages: [], apiRoutes: [] },
|
|
@@ -351,6 +362,55 @@ async function startLocalServer(port) {
|
|
|
351
362
|
res.end(JSON.stringify({ partialSuccess: {} }));
|
|
352
363
|
return;
|
|
353
364
|
}
|
|
365
|
+
// POST /dev/errors - Browser error reporting
|
|
366
|
+
if (req.method === 'POST' && req.url === '/dev/errors') {
|
|
367
|
+
try {
|
|
368
|
+
const data = JSON.parse(body);
|
|
369
|
+
const error = {
|
|
370
|
+
type: data.error?.type || 'unknown',
|
|
371
|
+
message: data.error?.message || 'No message',
|
|
372
|
+
stack: data.error?.stack,
|
|
373
|
+
timestamp: data.error?.timestamp || Date.now(),
|
|
374
|
+
url: data.url,
|
|
375
|
+
userAgent: data.userAgent,
|
|
376
|
+
filename: data.error?.filename,
|
|
377
|
+
lineno: data.error?.lineno,
|
|
378
|
+
colno: data.error?.colno,
|
|
379
|
+
html: data.error?.html,
|
|
380
|
+
};
|
|
381
|
+
store.browserErrors.push(error);
|
|
382
|
+
// Keep only last 100 errors
|
|
383
|
+
if (store.browserErrors.length > 100) {
|
|
384
|
+
store.browserErrors.shift();
|
|
385
|
+
}
|
|
386
|
+
// Print to console for visibility
|
|
387
|
+
if (error.type === 'blank_screen') {
|
|
388
|
+
console.log(chalk_1.default.red('\n🚨 [Browser] Blank screen detected!'));
|
|
389
|
+
console.log(chalk_1.default.yellow(` URL: ${error.url}`));
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
console.log(chalk_1.default.red(`\n🚨 [Browser] ${error.type}: ${error.message}`));
|
|
393
|
+
if (error.filename) {
|
|
394
|
+
console.log(chalk_1.default.gray(` ${error.filename}:${error.lineno}:${error.colno}`));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
res.writeHead(200);
|
|
398
|
+
res.end(JSON.stringify({ success: true }));
|
|
399
|
+
}
|
|
400
|
+
catch (e) {
|
|
401
|
+
res.writeHead(400);
|
|
402
|
+
res.end(JSON.stringify({ error: 'Invalid request' }));
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
// GET /monitor.js - Serve browser monitoring script
|
|
407
|
+
if (req.method === 'GET' && req.url === '/monitor.js') {
|
|
408
|
+
const monitorScript = browser_monitor_script_js_1.BROWSER_MONITOR_SCRIPT.replace('__SCANWARP_SERVER__', `'http://localhost:${port}'`);
|
|
409
|
+
res.setHeader('Content-Type', 'application/javascript');
|
|
410
|
+
res.writeHead(200);
|
|
411
|
+
res.end(monitorScript);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
354
414
|
// GET /health
|
|
355
415
|
if (req.method === 'GET' && req.url === '/health') {
|
|
356
416
|
res.writeHead(200);
|
|
@@ -384,6 +444,15 @@ async function startLocalServer(port) {
|
|
|
384
444
|
res.end(JSON.stringify({ issues }));
|
|
385
445
|
return;
|
|
386
446
|
}
|
|
447
|
+
// GET /api/browser-errors
|
|
448
|
+
if (req.method === 'GET' && req.url === '/api/browser-errors') {
|
|
449
|
+
res.writeHead(200);
|
|
450
|
+
res.end(JSON.stringify({
|
|
451
|
+
errors: store.browserErrors.slice(-20), // Last 20 errors
|
|
452
|
+
total: store.browserErrors.length
|
|
453
|
+
}));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
387
456
|
// GET /api/routes
|
|
388
457
|
if (req.method === 'GET' && req.url === '/api/routes') {
|
|
389
458
|
const allRoutes = [
|
|
@@ -686,7 +755,7 @@ function flattenAttributes(attrs) {
|
|
|
686
755
|
return result;
|
|
687
756
|
}
|
|
688
757
|
// ─── Child process management ───
|
|
689
|
-
function startDevServer(devCmd, cwd, scanwarpPort, isNextJs) {
|
|
758
|
+
function startDevServer(devCmd, cwd, scanwarpPort, isNextJs, hasInstrumentPackage) {
|
|
690
759
|
const [cmd, ...args] = parseCommand(devCmd);
|
|
691
760
|
// Build env vars
|
|
692
761
|
const env = {
|
|
@@ -695,8 +764,8 @@ function startDevServer(devCmd, cwd, scanwarpPort, isNextJs) {
|
|
|
695
764
|
SCANWARP_PROJECT_ID: 'local-dev',
|
|
696
765
|
SCANWARP_SERVICE_NAME: 'dev',
|
|
697
766
|
};
|
|
698
|
-
// For non-Next.js, inject NODE_OPTIONS to auto-load instrumentation
|
|
699
|
-
if (!isNextJs) {
|
|
767
|
+
// For non-Next.js, inject NODE_OPTIONS to auto-load instrumentation (if installed)
|
|
768
|
+
if (!isNextJs && hasInstrumentPackage) {
|
|
700
769
|
const existingNodeOpts = process.env.NODE_OPTIONS || '';
|
|
701
770
|
env.NODE_OPTIONS = `--require @scanwarp/instrument ${existingNodeOpts}`.trim();
|
|
702
771
|
}
|
package/dist/commands/mcp.js
CHANGED
|
@@ -20,7 +20,7 @@ async function mcpCommand(options = {}) {
|
|
|
20
20
|
// Create MCP server
|
|
21
21
|
const server = new index_js_1.Server({
|
|
22
22
|
name: 'scanwarp',
|
|
23
|
-
version: '0.3.
|
|
23
|
+
version: '0.3.3',
|
|
24
24
|
}, {
|
|
25
25
|
capabilities: {
|
|
26
26
|
tools: {},
|
|
@@ -376,5 +376,42 @@ async function mcpCommand(options = {}) {
|
|
|
376
376
|
console.error(`Connected to: ${serverUrl}`);
|
|
377
377
|
if (projectId) {
|
|
378
378
|
console.error(`Default project: ${projectId}`);
|
|
379
|
+
// Poll for new incidents and send notifications
|
|
380
|
+
startIncidentPolling(server, api, projectId);
|
|
379
381
|
}
|
|
380
382
|
}
|
|
383
|
+
// Poll for new incidents and log alerts
|
|
384
|
+
function startIncidentPolling(_server, api, projectId) {
|
|
385
|
+
const seenIncidents = new Set();
|
|
386
|
+
const poll = async () => {
|
|
387
|
+
try {
|
|
388
|
+
const incidents = await api.getIncidents({
|
|
389
|
+
projectId,
|
|
390
|
+
status: 'open'
|
|
391
|
+
});
|
|
392
|
+
for (const incident of incidents) {
|
|
393
|
+
// New incident detected
|
|
394
|
+
if (!seenIncidents.has(incident.id)) {
|
|
395
|
+
seenIncidents.add(incident.id);
|
|
396
|
+
// Log alert to stderr (visible in Claude Code output)
|
|
397
|
+
const message = incident.diagnosis_text
|
|
398
|
+
? `🚨 New incident: ${incident.diagnosis_text.slice(0, 100)}...`
|
|
399
|
+
: `🚨 New ${incident.severity} incident detected with ${incident.events?.length || 0} event(s)`;
|
|
400
|
+
console.error(`\n[ALERT] ${message}`);
|
|
401
|
+
console.error(`[ALERT] Incident ID: ${incident.id}`);
|
|
402
|
+
console.error(`[ALERT] Severity: ${incident.severity}`);
|
|
403
|
+
if (incident.diagnosis_text) {
|
|
404
|
+
console.error(`[ALERT] Diagnosis: ${incident.diagnosis_text}\n`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
catch (error) {
|
|
410
|
+
console.error('[Polling] Error checking incidents:', error instanceof Error ? error.message : error);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
// Poll every 30 seconds
|
|
414
|
+
setInterval(poll, 30000);
|
|
415
|
+
// Initial poll
|
|
416
|
+
poll();
|
|
417
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Browser monitoring script - embedded as string for easy serving
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.BROWSER_MONITOR_SCRIPT = void 0;
|
|
7
|
+
exports.BROWSER_MONITOR_SCRIPT = `
|
|
8
|
+
/**
|
|
9
|
+
* ScanWarp Browser Monitor
|
|
10
|
+
* Injected into user's app during dev to capture frontend errors
|
|
11
|
+
* Reports to local ScanWarp dev server
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
(function() {
|
|
15
|
+
const SCANWARP_SERVER = '__SCANWARP_SERVER__';
|
|
16
|
+
const errors = [];
|
|
17
|
+
|
|
18
|
+
// Capture console.error
|
|
19
|
+
const originalError = console.error;
|
|
20
|
+
console.error = function(...args) {
|
|
21
|
+
captureError({
|
|
22
|
+
type: 'console.error',
|
|
23
|
+
message: args.map(arg => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' '),
|
|
24
|
+
timestamp: Date.now(),
|
|
25
|
+
stack: new Error().stack
|
|
26
|
+
});
|
|
27
|
+
originalError.apply(console, args);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Capture unhandled errors
|
|
31
|
+
window.addEventListener('error', (event) => {
|
|
32
|
+
captureError({
|
|
33
|
+
type: 'unhandled_error',
|
|
34
|
+
message: event.message,
|
|
35
|
+
filename: event.filename,
|
|
36
|
+
lineno: event.lineno,
|
|
37
|
+
colno: event.colno,
|
|
38
|
+
stack: event.error?.stack,
|
|
39
|
+
timestamp: Date.now()
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Capture unhandled promise rejections
|
|
44
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
45
|
+
captureError({
|
|
46
|
+
type: 'unhandled_rejection',
|
|
47
|
+
message: event.reason?.message || String(event.reason),
|
|
48
|
+
stack: event.reason?.stack,
|
|
49
|
+
timestamp: Date.now()
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Capture React errors (if React is present)
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
if (window.React && window.ReactDOM) {
|
|
56
|
+
const originalErrorHandler = window.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactDebugCurrentFrame?.setExtraStackFrame;
|
|
57
|
+
if (originalErrorHandler) {
|
|
58
|
+
window.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactDebugCurrentFrame.setExtraStackFrame = function(stack) {
|
|
59
|
+
captureError({
|
|
60
|
+
type: 'react_error',
|
|
61
|
+
message: 'React rendering error',
|
|
62
|
+
stack: stack,
|
|
63
|
+
timestamp: Date.now()
|
|
64
|
+
});
|
|
65
|
+
originalErrorHandler.call(this, stack);
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}, 100);
|
|
70
|
+
|
|
71
|
+
// Check if app rendered (detect blank screen)
|
|
72
|
+
setTimeout(() => {
|
|
73
|
+
const body = document.body;
|
|
74
|
+
const hasContent = body && (body.children.length > 1 || body.textContent.trim().length > 0);
|
|
75
|
+
|
|
76
|
+
if (!hasContent) {
|
|
77
|
+
captureError({
|
|
78
|
+
type: 'blank_screen',
|
|
79
|
+
message: 'Page rendered but appears blank - no content in body',
|
|
80
|
+
timestamp: Date.now(),
|
|
81
|
+
html: document.documentElement.outerHTML.slice(0, 500)
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}, 2000);
|
|
85
|
+
|
|
86
|
+
// Send error to ScanWarp server
|
|
87
|
+
function captureError(error) {
|
|
88
|
+
errors.push(error);
|
|
89
|
+
|
|
90
|
+
// Send to server
|
|
91
|
+
fetch(SCANWARP_SERVER + '/dev/errors', {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: { 'Content-Type': 'application/json' },
|
|
94
|
+
body: JSON.stringify({
|
|
95
|
+
url: window.location.href,
|
|
96
|
+
userAgent: navigator.userAgent,
|
|
97
|
+
error: error
|
|
98
|
+
})
|
|
99
|
+
}).catch(() => {
|
|
100
|
+
// Silently fail if server is down
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Expose for debugging
|
|
105
|
+
window.__scanwarp = {
|
|
106
|
+
errors: errors,
|
|
107
|
+
getErrors: () => errors,
|
|
108
|
+
clearErrors: () => errors.length = 0
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
console.log('%c[ScanWarp] %cMonitoring for errors...', 'color: #3b82f6; font-weight: bold', 'color: #6b7280');
|
|
112
|
+
})();
|
|
113
|
+
`;
|
package/dist/integrations/mcp.js
CHANGED
|
@@ -95,7 +95,9 @@ async function setupMCP(serverUrl) {
|
|
|
95
95
|
fs_1.default.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
96
96
|
console.log(chalk_1.default.green(`✓ MCP server added to ${toolName} configuration`));
|
|
97
97
|
console.log(chalk_1.default.gray(` Config: ${configPath}\n`));
|
|
98
|
-
console.log(chalk_1.default.yellow(
|
|
98
|
+
console.log(chalk_1.default.bold.yellow(' 📌 One more step:'));
|
|
99
|
+
console.log(chalk_1.default.yellow(` Restart ${toolName} to activate the MCP connection`));
|
|
100
|
+
console.log(chalk_1.default.gray(` (${toolName} loads MCP servers at startup)\n`));
|
|
99
101
|
}
|
|
100
102
|
catch (error) {
|
|
101
103
|
console.log(chalk_1.default.red('✗ Failed to update MCP config'));
|