@veraxhq/verax 0.1.0 → 0.2.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/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +14 -36
- package/src/cli/commands/default.js +523 -0
- package/src/cli/commands/doctor.js +165 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +402 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +296 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +34 -0
- package/src/cli/util/expectation-extractor.js +378 -0
- package/src/cli/util/findings-writer.js +31 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +366 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +29 -0
- package/src/cli/util/project-discovery.js +277 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/summary-writer.js +32 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +101 -0
- package/src/verax/cli/wizard.js +98 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +403 -0
- package/src/verax/core/incremental-store.js +237 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +521 -0
- package/src/verax/detect/comparison.js +2 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +177 -0
- package/src/verax/detect/expectation-model.js +194 -172
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +44 -8
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +172 -286
- package/src/verax/detect/interactive-findings.js +613 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/verdict-engine.js +563 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/index.js +90 -14
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +579 -0
- package/src/verax/intel/vue-router-extractor.js +323 -0
- package/src/verax/learn/action-contract-extractor.js +335 -101
- package/src/verax/learn/ast-contract-extractor.js +95 -5
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/manifest-writer.js +97 -47
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +27 -96
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +112 -4
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +10 -5
- package/src/verax/observe/console-sensor.js +1 -17
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +512 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +643 -275
- package/src/verax/observe/index.js +908 -27
- package/src/verax/observe/index.js.backup +1 -0
- package/src/verax/observe/interaction-discovery.js +365 -14
- package/src/verax/observe/interaction-runner.js +563 -198
- package/src/verax/observe/loading-sensor.js +139 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +37 -17
- package/src/verax/observe/state-sensor.js +389 -0
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +61 -20
- package/src/verax/observe/ui-signal-sensor.js +136 -17
- package/src/verax/scan-summary-writer.js +77 -15
- package/src/verax/shared/artifact-manager.js +110 -8
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +170 -0
- package/src/verax/shared/dynamic-route-utils.js +218 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +14 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +65 -0
- package/src/verax/validate/context-validator.js +244 -0
- package/src/verax/validate/context-validator.js.bak +0 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading State Sensor
|
|
3
|
+
* Detects and tracks loading indicators like spinners, aria-busy, and disabled buttons
|
|
4
|
+
* Deterministically detects unresolved loading states
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export class LoadingSensor {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.loadingTimeout = options.loadingTimeout || 5000; // 5s deterministic timeout
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Start monitoring loading state and return a window ID
|
|
14
|
+
*/
|
|
15
|
+
startWindow(page) {
|
|
16
|
+
const windowId = `loading_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
17
|
+
|
|
18
|
+
const state = {
|
|
19
|
+
id: windowId,
|
|
20
|
+
loadingStartTime: null,
|
|
21
|
+
isCurrentlyLoading: false,
|
|
22
|
+
loadingIndicators: [],
|
|
23
|
+
resolveTime: null,
|
|
24
|
+
unresolved: false,
|
|
25
|
+
maxLoadingDuration: 0
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Monitor for loading indicators
|
|
29
|
+
const checkLoading = async () => {
|
|
30
|
+
try {
|
|
31
|
+
const indicators = await page.evaluate(() => {
|
|
32
|
+
const found = [];
|
|
33
|
+
|
|
34
|
+
// Check aria-busy
|
|
35
|
+
const ariaBusy = document.querySelectorAll('[aria-busy="true"]');
|
|
36
|
+
if (ariaBusy.length > 0) {
|
|
37
|
+
found.push({ type: 'aria-busy', count: ariaBusy.length });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check progress bars
|
|
41
|
+
const progressBars = document.querySelectorAll('[role="progressbar"]');
|
|
42
|
+
if (progressBars.length > 0) {
|
|
43
|
+
found.push({ type: 'progressbar', count: progressBars.length });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check spinners/loaders by class
|
|
47
|
+
const spinners = document.querySelectorAll(
|
|
48
|
+
'[class*="spin"], [class*="load"], [class*="progress"], [class*="skeleton"]'
|
|
49
|
+
);
|
|
50
|
+
if (spinners.length > 0) {
|
|
51
|
+
found.push({ type: 'spinner-class', count: spinners.length });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check disabled submit buttons (often indicates pending submission)
|
|
55
|
+
const disabledSubmits = document.querySelectorAll('button[type="submit"]:disabled');
|
|
56
|
+
if (disabledSubmits.length > 0) {
|
|
57
|
+
found.push({ type: 'disabled-submit', count: disabledSubmits.length });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return found;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const hasLoading = indicators.length > 0;
|
|
64
|
+
|
|
65
|
+
if (hasLoading && !state.isCurrentlyLoading) {
|
|
66
|
+
// Loading started
|
|
67
|
+
state.isCurrentlyLoading = true;
|
|
68
|
+
state.loadingStartTime = Date.now();
|
|
69
|
+
state.loadingIndicators = indicators;
|
|
70
|
+
} else if (!hasLoading && state.isCurrentlyLoading) {
|
|
71
|
+
// Loading resolved
|
|
72
|
+
state.isCurrentlyLoading = false;
|
|
73
|
+
state.resolveTime = Date.now();
|
|
74
|
+
state.maxLoadingDuration = state.resolveTime - state.loadingStartTime;
|
|
75
|
+
} else if (hasLoading && state.isCurrentlyLoading) {
|
|
76
|
+
// Still loading, update indicators
|
|
77
|
+
state.loadingIndicators = indicators;
|
|
78
|
+
}
|
|
79
|
+
} catch (e) {
|
|
80
|
+
// Silently ignore evaluation errors
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// Set up interval to check loading state (every 100ms for deterministic detection)
|
|
85
|
+
const intervalId = setInterval(checkLoading, 100);
|
|
86
|
+
|
|
87
|
+
// Immediately check once
|
|
88
|
+
checkLoading();
|
|
89
|
+
|
|
90
|
+
// Store interval for cleanup
|
|
91
|
+
state._intervalId = intervalId;
|
|
92
|
+
state._checkLoading = checkLoading;
|
|
93
|
+
|
|
94
|
+
return { windowId, state };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Stop monitoring and get the loading state summary
|
|
99
|
+
*/
|
|
100
|
+
async stopWindow(windowId, state) {
|
|
101
|
+
if (!state || !state._intervalId) {
|
|
102
|
+
return {
|
|
103
|
+
id: windowId,
|
|
104
|
+
loadingIndicators: [],
|
|
105
|
+
isLoading: false,
|
|
106
|
+
unresolved: false,
|
|
107
|
+
duration: 0,
|
|
108
|
+
timeout: false,
|
|
109
|
+
hasLoadingIndicators: false
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
clearInterval(state._intervalId);
|
|
114
|
+
|
|
115
|
+
// Final check
|
|
116
|
+
if (state._checkLoading) {
|
|
117
|
+
await state._checkLoading();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Determine if loading is unresolved (exceeded timeout)
|
|
121
|
+
const isStillLoading = state.isCurrentlyLoading === true;
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
const loadingDuration = state.loadingStartTime ? (now - state.loadingStartTime) : 0;
|
|
124
|
+
const exceededTimeout = state.loadingStartTime && (now - state.loadingStartTime) > this.loadingTimeout;
|
|
125
|
+
const unresolved = isStillLoading && exceededTimeout;
|
|
126
|
+
const timeout = exceededTimeout;
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
id: state.id,
|
|
130
|
+
loadingIndicators: state.loadingIndicators || [],
|
|
131
|
+
isLoading: state.isCurrentlyLoading,
|
|
132
|
+
unresolved: unresolved,
|
|
133
|
+
duration: state.resolveTime ? state.maxLoadingDuration : loadingDuration,
|
|
134
|
+
timeout: timeout,
|
|
135
|
+
resolveTime: state.resolveTime,
|
|
136
|
+
hasLoadingIndicators: (state.loadingIndicators && state.loadingIndicators.length > 0) || isStillLoading
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NAVIGATION INTELLIGENCE v2 — Navigation Sensor
|
|
3
|
+
*
|
|
4
|
+
* Captures navigation state changes per interaction:
|
|
5
|
+
* - URL changes (beforeUrl → afterUrl)
|
|
6
|
+
* - History API state (length, pushState, replaceState)
|
|
7
|
+
* - SPA Router events (Next.js, React Router)
|
|
8
|
+
* - Blocked navigation signals (preventDefault, guards)
|
|
9
|
+
*
|
|
10
|
+
* Provides runtime evidence for navigation failure detection.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export class NavigationSensor {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.windows = new Map();
|
|
16
|
+
this.nextWindowId = 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Start a navigation observation window.
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} page - Playwright page
|
|
23
|
+
* @returns {number} - Window ID
|
|
24
|
+
*/
|
|
25
|
+
startWindow(page) {
|
|
26
|
+
const windowId = this.nextWindowId++;
|
|
27
|
+
|
|
28
|
+
const state = {
|
|
29
|
+
windowId,
|
|
30
|
+
beforeUrl: null,
|
|
31
|
+
afterUrl: null,
|
|
32
|
+
beforeHistoryLength: null,
|
|
33
|
+
afterHistoryLength: null,
|
|
34
|
+
historyChanges: [],
|
|
35
|
+
routerEvents: [],
|
|
36
|
+
blockedNavigations: [],
|
|
37
|
+
started: Date.now()
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
this.windows.set(windowId, state);
|
|
41
|
+
|
|
42
|
+
// Capture initial state immediately
|
|
43
|
+
this._captureBeforeState(page, state);
|
|
44
|
+
|
|
45
|
+
// Set up listeners for navigation events
|
|
46
|
+
this._attachListeners(page, state);
|
|
47
|
+
|
|
48
|
+
return windowId;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Capture before-state synchronously.
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} page - Playwright page
|
|
55
|
+
* @param {Object} state - Window state
|
|
56
|
+
*/
|
|
57
|
+
async _captureBeforeState(page, state) {
|
|
58
|
+
try {
|
|
59
|
+
state.beforeUrl = page.url();
|
|
60
|
+
state.beforeHistoryLength = await page.evaluate(() => window.history.length).catch(() => null);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
// Ignore errors during capture
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Attach navigation listeners.
|
|
68
|
+
*
|
|
69
|
+
* @param {Object} page - Playwright page
|
|
70
|
+
* @param {Object} state - Window state
|
|
71
|
+
*/
|
|
72
|
+
_attachListeners(page, state) {
|
|
73
|
+
// Listen for history API calls
|
|
74
|
+
page.on('console', (msg) => {
|
|
75
|
+
const text = msg.text();
|
|
76
|
+
|
|
77
|
+
// Custom markers from injected tracking script
|
|
78
|
+
if (text.startsWith('[NAV]')) {
|
|
79
|
+
try {
|
|
80
|
+
const data = JSON.parse(text.substring(5));
|
|
81
|
+
if (data.type === 'history') {
|
|
82
|
+
state.historyChanges.push({
|
|
83
|
+
method: data.method,
|
|
84
|
+
url: data.url,
|
|
85
|
+
timestamp: Date.now()
|
|
86
|
+
});
|
|
87
|
+
} else if (data.type === 'router') {
|
|
88
|
+
state.routerEvents.push({
|
|
89
|
+
event: data.event,
|
|
90
|
+
url: data.url,
|
|
91
|
+
timestamp: Date.now()
|
|
92
|
+
});
|
|
93
|
+
} else if (data.type === 'blocked') {
|
|
94
|
+
state.blockedNavigations.push({
|
|
95
|
+
reason: data.reason,
|
|
96
|
+
url: data.url,
|
|
97
|
+
timestamp: Date.now()
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
// Invalid JSON, ignore
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Stop a navigation observation window.
|
|
109
|
+
*
|
|
110
|
+
* @param {number} windowId - Window ID
|
|
111
|
+
* @param {Object} page - Playwright page
|
|
112
|
+
* @returns {Object} - Navigation summary
|
|
113
|
+
*/
|
|
114
|
+
async stopWindow(windowId, page) {
|
|
115
|
+
const state = this.windows.get(windowId);
|
|
116
|
+
|
|
117
|
+
if (!state) {
|
|
118
|
+
return this._emptyNavigationSummary(windowId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Capture after state
|
|
122
|
+
try {
|
|
123
|
+
state.afterUrl = page.url();
|
|
124
|
+
state.afterHistoryLength = await page.evaluate(() => window.history.length).catch(() => null);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
state.afterUrl = state.beforeUrl;
|
|
127
|
+
state.afterHistoryLength = state.beforeHistoryLength;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const duration = Date.now() - state.started;
|
|
131
|
+
|
|
132
|
+
// Compute deltas
|
|
133
|
+
const urlChanged = state.beforeUrl !== state.afterUrl;
|
|
134
|
+
const historyLengthDelta = (state.afterHistoryLength !== null && state.beforeHistoryLength !== null)
|
|
135
|
+
? state.afterHistoryLength - state.beforeHistoryLength
|
|
136
|
+
: null;
|
|
137
|
+
|
|
138
|
+
const summary = {
|
|
139
|
+
windowId,
|
|
140
|
+
beforeUrl: state.beforeUrl,
|
|
141
|
+
afterUrl: state.afterUrl,
|
|
142
|
+
urlChanged,
|
|
143
|
+
beforeHistoryLength: state.beforeHistoryLength,
|
|
144
|
+
afterHistoryLength: state.afterHistoryLength,
|
|
145
|
+
historyLengthDelta,
|
|
146
|
+
historyChanges: state.historyChanges,
|
|
147
|
+
routerEvents: state.routerEvents,
|
|
148
|
+
blockedNavigations: state.blockedNavigations,
|
|
149
|
+
hasNavigationActivity: urlChanged || historyLengthDelta !== 0 || state.historyChanges.length > 0,
|
|
150
|
+
duration
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
this.windows.delete(windowId);
|
|
154
|
+
return summary;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Return empty summary for invalid window ID.
|
|
159
|
+
*
|
|
160
|
+
* @param {number} windowId - Window ID
|
|
161
|
+
* @returns {Object} - Empty summary
|
|
162
|
+
*/
|
|
163
|
+
_emptyNavigationSummary(windowId) {
|
|
164
|
+
return {
|
|
165
|
+
windowId,
|
|
166
|
+
beforeUrl: null,
|
|
167
|
+
afterUrl: null,
|
|
168
|
+
urlChanged: false,
|
|
169
|
+
beforeHistoryLength: null,
|
|
170
|
+
afterHistoryLength: null,
|
|
171
|
+
historyLengthDelta: null,
|
|
172
|
+
historyChanges: [],
|
|
173
|
+
routerEvents: [],
|
|
174
|
+
blockedNavigations: [],
|
|
175
|
+
hasNavigationActivity: false,
|
|
176
|
+
duration: 0
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Inject navigation tracking script into page.
|
|
182
|
+
* Call this before interaction to capture history/router events.
|
|
183
|
+
*
|
|
184
|
+
* @param {Object} page - Playwright page
|
|
185
|
+
*/
|
|
186
|
+
async injectTrackingScript(page) {
|
|
187
|
+
try {
|
|
188
|
+
await page.evaluate(() => {
|
|
189
|
+
// Skip if already injected
|
|
190
|
+
if (window.__veraxNavTracking) return;
|
|
191
|
+
window.__veraxNavTracking = true;
|
|
192
|
+
|
|
193
|
+
// Intercept history API
|
|
194
|
+
const originalPushState = window.history.pushState;
|
|
195
|
+
const originalReplaceState = window.history.replaceState;
|
|
196
|
+
|
|
197
|
+
window.history.pushState = function(...args) {
|
|
198
|
+
console.log('[NAV]' + JSON.stringify({
|
|
199
|
+
type: 'history',
|
|
200
|
+
method: 'pushState',
|
|
201
|
+
url: args[2] || window.location.href
|
|
202
|
+
}));
|
|
203
|
+
return originalPushState.apply(this, args);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
window.history.replaceState = function(...args) {
|
|
207
|
+
console.log('[NAV]' + JSON.stringify({
|
|
208
|
+
type: 'history',
|
|
209
|
+
method: 'replaceState',
|
|
210
|
+
url: args[2] || window.location.href
|
|
211
|
+
}));
|
|
212
|
+
return originalReplaceState.apply(this, args);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
// Listen for popstate (back/forward)
|
|
216
|
+
window.addEventListener('popstate', () => {
|
|
217
|
+
console.log('[NAV]' + JSON.stringify({
|
|
218
|
+
type: 'history',
|
|
219
|
+
method: 'popstate',
|
|
220
|
+
url: window.location.href
|
|
221
|
+
}));
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Next.js Router events
|
|
225
|
+
if (window.next?.router) {
|
|
226
|
+
window.next.router.events.on('routeChangeStart', (url) => {
|
|
227
|
+
console.log('[NAV]' + JSON.stringify({
|
|
228
|
+
type: 'router',
|
|
229
|
+
event: 'routeChangeStart',
|
|
230
|
+
url
|
|
231
|
+
}));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
window.next.router.events.on('routeChangeComplete', (url) => {
|
|
235
|
+
console.log('[NAV]' + JSON.stringify({
|
|
236
|
+
type: 'router',
|
|
237
|
+
event: 'routeChangeComplete',
|
|
238
|
+
url
|
|
239
|
+
}));
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
window.next.router.events.on('routeChangeError', (err, url) => {
|
|
243
|
+
console.log('[NAV]' + JSON.stringify({
|
|
244
|
+
type: 'blocked',
|
|
245
|
+
reason: 'routeChangeError',
|
|
246
|
+
url
|
|
247
|
+
}));
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
} catch (e) {
|
|
252
|
+
// Ignore injection errors (page might not be ready)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
@@ -24,7 +24,8 @@ export class NetworkSensor {
|
|
|
24
24
|
failedRequests: [],
|
|
25
25
|
failedByStatus: {}, // status code -> count
|
|
26
26
|
unfinishedRequests: new Set(), // urls still pending
|
|
27
|
-
lastErrors: []
|
|
27
|
+
lastErrors: [],
|
|
28
|
+
requestOrder: []
|
|
28
29
|
};
|
|
29
30
|
|
|
30
31
|
// Track all requests
|
|
@@ -40,8 +41,10 @@ export class NetworkSensor {
|
|
|
40
41
|
status: null,
|
|
41
42
|
failed: false,
|
|
42
43
|
duration: 0,
|
|
43
|
-
count: 0
|
|
44
|
+
count: 0,
|
|
45
|
+
completed: false
|
|
44
46
|
});
|
|
47
|
+
state.requestOrder.push(url);
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
const reqData = state.requests.get(url);
|
|
@@ -58,11 +61,15 @@ export class NetworkSensor {
|
|
|
58
61
|
reqData.endTime = Date.now();
|
|
59
62
|
reqData.status = status;
|
|
60
63
|
reqData.duration = reqData.endTime - reqData.startTime;
|
|
64
|
+
reqData.completed = true;
|
|
61
65
|
|
|
62
66
|
if (status >= 400) {
|
|
63
67
|
reqData.failed = true;
|
|
64
68
|
state.failedRequests.push({ url, status, duration: reqData.duration });
|
|
65
69
|
state.failedByStatus[status] = (state.failedByStatus[status] || 0) + 1;
|
|
70
|
+
} else if (status >= 200 && status < 300) {
|
|
71
|
+
// Track successful 2xx responses explicitly
|
|
72
|
+
reqData.successful = true;
|
|
66
73
|
}
|
|
67
74
|
}
|
|
68
75
|
|
|
@@ -77,9 +84,20 @@ export class NetworkSensor {
|
|
|
77
84
|
reqData.endTime = Date.now();
|
|
78
85
|
reqData.duration = reqData.endTime - reqData.startTime;
|
|
79
86
|
reqData.failed = true;
|
|
87
|
+
} else {
|
|
88
|
+
state.requests.set(url, {
|
|
89
|
+
url: url,
|
|
90
|
+
startTime: Date.now(),
|
|
91
|
+
endTime: Date.now(),
|
|
92
|
+
status: null,
|
|
93
|
+
failed: true,
|
|
94
|
+
duration: 0,
|
|
95
|
+
count: 1
|
|
96
|
+
});
|
|
80
97
|
}
|
|
81
98
|
|
|
82
99
|
state.failedRequests.push({ url, status: 'FAILED', duration: 0 });
|
|
100
|
+
state.failedByStatus['FAILED'] = (state.failedByStatus['FAILED'] || 0) + 1;
|
|
83
101
|
state.unfinishedRequests.delete(url);
|
|
84
102
|
};
|
|
85
103
|
|
|
@@ -111,10 +129,31 @@ export class NetworkSensor {
|
|
|
111
129
|
const endTime = Date.now();
|
|
112
130
|
const duration = endTime - state.startTime;
|
|
113
131
|
|
|
114
|
-
//
|
|
132
|
+
// Count failed requests: requests that had 4xx/5xx status or failed completely
|
|
133
|
+
// Note: incomplete requests are NOT counted as failed - they might just be slow
|
|
134
|
+
const failedCount = Array.from(state.requests.values()).filter(
|
|
135
|
+
(r) => r.failed === true
|
|
136
|
+
).length;
|
|
137
|
+
|
|
138
|
+
// Find slow requests (completed requests that took longer than threshold)
|
|
139
|
+
// Also include incomplete requests that have been pending longer than threshold
|
|
140
|
+
const now = Date.now();
|
|
115
141
|
const slowRequests = Array.from(state.requests.values())
|
|
116
|
-
.filter((r) =>
|
|
117
|
-
|
|
142
|
+
.filter((r) => {
|
|
143
|
+
if (r.completed && r.duration && r.duration > this.slowThresholdMs) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
// Incomplete request that's been pending longer than threshold
|
|
147
|
+
if (!r.completed && r.startTime && (now - r.startTime) > this.slowThresholdMs) {
|
|
148
|
+
// Estimate duration as time since start
|
|
149
|
+
if (!r.duration) {
|
|
150
|
+
r.duration = now - r.startTime;
|
|
151
|
+
}
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
})
|
|
156
|
+
.sort((a, b) => (b.duration || 0) - (a.duration || 0))
|
|
118
157
|
.slice(0, 5);
|
|
119
158
|
|
|
120
159
|
// Get top failed URLs (limit to 5)
|
|
@@ -122,10 +161,16 @@ export class NetworkSensor {
|
|
|
122
161
|
.slice(0, 5)
|
|
123
162
|
.map((f) => ({ url: f.url, status: f.status, duration: f.duration }));
|
|
124
163
|
|
|
164
|
+
// Count successful 2xx requests
|
|
165
|
+
const successfulRequests = Array.from(state.requests.values()).filter(
|
|
166
|
+
(r) => r.successful === true && r.status >= 200 && r.status < 300
|
|
167
|
+
);
|
|
168
|
+
|
|
125
169
|
const summary = {
|
|
126
170
|
windowId,
|
|
127
171
|
totalRequests: state.requests.size,
|
|
128
|
-
failedRequests:
|
|
172
|
+
failedRequests: failedCount,
|
|
173
|
+
successfulRequests: successfulRequests.length,
|
|
129
174
|
failedByStatus: state.failedByStatus,
|
|
130
175
|
hasNetworkActivity: state.requests.size > 0,
|
|
131
176
|
slowRequestsCount: slowRequests.length,
|
|
@@ -135,7 +180,9 @@ export class NetworkSensor {
|
|
|
135
180
|
})),
|
|
136
181
|
topFailedUrls: topFailedUrls,
|
|
137
182
|
duration: duration,
|
|
138
|
-
unfinishedCount: state.unfinishedRequests.size
|
|
183
|
+
unfinishedCount: state.unfinishedRequests.size,
|
|
184
|
+
firstRequestUrl: state.requestOrder[0] || null,
|
|
185
|
+
observedRequestUrls: state.requestOrder.slice(0, 5)
|
|
139
186
|
};
|
|
140
187
|
|
|
141
188
|
this.windows.delete(windowId);
|
|
@@ -161,6 +208,7 @@ export class NetworkSensor {
|
|
|
161
208
|
windowId: -1,
|
|
162
209
|
totalRequests: 0,
|
|
163
210
|
failedRequests: 0,
|
|
211
|
+
successfulRequests: 0,
|
|
164
212
|
failedByStatus: {},
|
|
165
213
|
hasNetworkActivity: false,
|
|
166
214
|
slowRequestsCount: 0,
|