@odavl/guardian 0.1.0-rc1
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/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/bin/guardian.js +690 -0
- package/flows/example-login-flow.json +36 -0
- package/flows/example-signup-flow.json +44 -0
- package/guardian-contract-v1.md +149 -0
- package/guardian.config.json +54 -0
- package/guardian.policy.json +12 -0
- package/guardian.profile.docs.yaml +18 -0
- package/guardian.profile.ecommerce.yaml +17 -0
- package/guardian.profile.marketing.yaml +18 -0
- package/guardian.profile.saas.yaml +21 -0
- package/package.json +69 -0
- package/policies/enterprise.json +12 -0
- package/policies/saas.json +12 -0
- package/policies/startup.json +12 -0
- package/src/guardian/attempt-engine.js +454 -0
- package/src/guardian/attempt-registry.js +227 -0
- package/src/guardian/attempt-reporter.js +507 -0
- package/src/guardian/attempt.js +227 -0
- package/src/guardian/auto-attempt-builder.js +283 -0
- package/src/guardian/baseline-reporter.js +143 -0
- package/src/guardian/baseline-storage.js +285 -0
- package/src/guardian/baseline.js +492 -0
- package/src/guardian/behavioral-signals.js +261 -0
- package/src/guardian/breakage-intelligence.js +223 -0
- package/src/guardian/browser.js +92 -0
- package/src/guardian/cli-summary.js +141 -0
- package/src/guardian/crawler.js +142 -0
- package/src/guardian/discovery-engine.js +661 -0
- package/src/guardian/enhanced-html-reporter.js +305 -0
- package/src/guardian/failure-taxonomy.js +169 -0
- package/src/guardian/flow-executor.js +374 -0
- package/src/guardian/flow-registry.js +67 -0
- package/src/guardian/html-reporter.js +414 -0
- package/src/guardian/index.js +218 -0
- package/src/guardian/init-command.js +139 -0
- package/src/guardian/junit-reporter.js +264 -0
- package/src/guardian/market-criticality.js +335 -0
- package/src/guardian/market-reporter.js +305 -0
- package/src/guardian/network-trace.js +178 -0
- package/src/guardian/policy.js +357 -0
- package/src/guardian/preset-loader.js +148 -0
- package/src/guardian/reality.js +547 -0
- package/src/guardian/reporter.js +181 -0
- package/src/guardian/root-cause-analysis.js +171 -0
- package/src/guardian/safety.js +248 -0
- package/src/guardian/scan-presets.js +60 -0
- package/src/guardian/screenshot.js +152 -0
- package/src/guardian/sitemap.js +225 -0
- package/src/guardian/snapshot-schema.js +266 -0
- package/src/guardian/snapshot.js +327 -0
- package/src/guardian/validators.js +323 -0
- package/src/guardian/visual-diff.js +247 -0
- package/src/guardian/webhook.js +206 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 5 — Behavioral Signal Detector
|
|
3
|
+
* Detects non-visual but UI-affecting changes that break user trust
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detect behavioral changes (missing elements, layout shifts, disabled CTAs)
|
|
8
|
+
*/
|
|
9
|
+
class BehavioralSignalDetector {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if critical element is visible and accessible
|
|
16
|
+
*/
|
|
17
|
+
async checkElementVisibility(page, selector) {
|
|
18
|
+
try {
|
|
19
|
+
const element = await page.$(selector);
|
|
20
|
+
if (!element) {
|
|
21
|
+
return {
|
|
22
|
+
visible: false,
|
|
23
|
+
accessible: false,
|
|
24
|
+
signal: 'ELEMENT_MISSING',
|
|
25
|
+
severity: 'CRITICAL',
|
|
26
|
+
description: `Critical element not found: ${selector}`
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const boundingBox = await element.boundingBox();
|
|
31
|
+
if (!boundingBox) {
|
|
32
|
+
return {
|
|
33
|
+
visible: false,
|
|
34
|
+
accessible: false,
|
|
35
|
+
signal: 'OFFSCREEN_ELEMENT',
|
|
36
|
+
severity: 'CRITICAL',
|
|
37
|
+
description: `Element off-screen: ${selector}`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isHidden = await element.isHidden();
|
|
42
|
+
if (isHidden) {
|
|
43
|
+
return {
|
|
44
|
+
visible: false,
|
|
45
|
+
accessible: false,
|
|
46
|
+
signal: 'HIDDEN_ELEMENT',
|
|
47
|
+
severity: 'CRITICAL',
|
|
48
|
+
description: `Element hidden: ${selector}`
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const isDisabled = await element.isDisabled();
|
|
53
|
+
if (isDisabled) {
|
|
54
|
+
return {
|
|
55
|
+
visible: true,
|
|
56
|
+
accessible: false,
|
|
57
|
+
signal: 'DISABLED_ELEMENT',
|
|
58
|
+
severity: 'WARNING',
|
|
59
|
+
description: `Element disabled: ${selector}`
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
visible: true,
|
|
65
|
+
accessible: true,
|
|
66
|
+
signal: null,
|
|
67
|
+
severity: 'INFO'
|
|
68
|
+
};
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return {
|
|
71
|
+
visible: false,
|
|
72
|
+
accessible: false,
|
|
73
|
+
signal: 'CHECK_FAILED',
|
|
74
|
+
severity: 'INFO',
|
|
75
|
+
description: `Check failed: ${err.message}`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Detect layout shifts - elements that moved unexpectedly
|
|
82
|
+
*/
|
|
83
|
+
async detectLayoutShift(page, selectors) {
|
|
84
|
+
const shifts = [];
|
|
85
|
+
|
|
86
|
+
for (const selector of selectors) {
|
|
87
|
+
try {
|
|
88
|
+
const element = await page.$(selector);
|
|
89
|
+
if (!element) continue;
|
|
90
|
+
|
|
91
|
+
const boundingBox = await element.boundingBox();
|
|
92
|
+
if (!boundingBox) continue;
|
|
93
|
+
|
|
94
|
+
// Check if element is near viewport edges (shifted)
|
|
95
|
+
const viewportSize = page.viewportSize();
|
|
96
|
+
if (viewportSize) {
|
|
97
|
+
const { x, y, width, height } = boundingBox;
|
|
98
|
+
|
|
99
|
+
// Completely off-screen
|
|
100
|
+
if (x + width <= 0 || x >= viewportSize.width || y + height <= 0 || y >= viewportSize.height) {
|
|
101
|
+
shifts.push({
|
|
102
|
+
selector,
|
|
103
|
+
signal: 'OFFSCREEN_SHIFT',
|
|
104
|
+
severity: 'CRITICAL',
|
|
105
|
+
description: `Element shifted off-screen: ${selector}`,
|
|
106
|
+
position: { x, y, width, height }
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// Partially off-screen
|
|
110
|
+
else if (x < 0 || y < 0 || x + width > viewportSize.width) {
|
|
111
|
+
shifts.push({
|
|
112
|
+
selector,
|
|
113
|
+
signal: 'PARTIAL_SHIFT',
|
|
114
|
+
severity: 'WARNING',
|
|
115
|
+
description: `Element partially shifted: ${selector}`,
|
|
116
|
+
position: { x, y, width, height }
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (err) {
|
|
121
|
+
// Silently skip errors
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return shifts;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if critical CTA (button, link) is clickable
|
|
130
|
+
*/
|
|
131
|
+
async checkCTAAccessibility(page, selector) {
|
|
132
|
+
try {
|
|
133
|
+
const element = await page.$(selector);
|
|
134
|
+
if (!element) {
|
|
135
|
+
return {
|
|
136
|
+
clickable: false,
|
|
137
|
+
signal: 'CTA_MISSING',
|
|
138
|
+
severity: 'CRITICAL',
|
|
139
|
+
description: `Call-to-action not found: ${selector}`
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const isDisabled = await element.isDisabled();
|
|
144
|
+
if (isDisabled) {
|
|
145
|
+
return {
|
|
146
|
+
clickable: false,
|
|
147
|
+
signal: 'CTA_DISABLED',
|
|
148
|
+
severity: 'CRITICAL',
|
|
149
|
+
description: `CTA disabled unexpectedly: ${selector}`
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const isHidden = await element.isHidden();
|
|
154
|
+
if (isHidden) {
|
|
155
|
+
return {
|
|
156
|
+
clickable: false,
|
|
157
|
+
signal: 'CTA_HIDDEN',
|
|
158
|
+
severity: 'CRITICAL',
|
|
159
|
+
description: `CTA hidden unexpectedly: ${selector}`
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const isEnabled = await element.isEnabled();
|
|
164
|
+
if (!isEnabled) {
|
|
165
|
+
return {
|
|
166
|
+
clickable: false,
|
|
167
|
+
signal: 'CTA_UNAVAILABLE',
|
|
168
|
+
severity: 'CRITICAL',
|
|
169
|
+
description: `CTA unavailable: ${selector}`
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
clickable: true,
|
|
175
|
+
signal: null,
|
|
176
|
+
severity: 'INFO'
|
|
177
|
+
};
|
|
178
|
+
} catch (err) {
|
|
179
|
+
return {
|
|
180
|
+
clickable: false,
|
|
181
|
+
signal: 'CHECK_FAILED',
|
|
182
|
+
severity: 'INFO',
|
|
183
|
+
description: `CTA check failed: ${err.message}`
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Detect color/styling changes on critical elements
|
|
190
|
+
*/
|
|
191
|
+
async detectStyleChanges(page, selector, expectedStyles = {}) {
|
|
192
|
+
const changes = [];
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
const element = await page.$(selector);
|
|
196
|
+
if (!element) return changes;
|
|
197
|
+
|
|
198
|
+
for (const [property, expectedValue] of Object.entries(expectedStyles)) {
|
|
199
|
+
const actualValue = await element.evaluate((el, prop) => {
|
|
200
|
+
return window.getComputedStyle(el).getPropertyValue(prop);
|
|
201
|
+
}, property);
|
|
202
|
+
|
|
203
|
+
if (actualValue !== expectedValue) {
|
|
204
|
+
changes.push({
|
|
205
|
+
selector,
|
|
206
|
+
property,
|
|
207
|
+
expectedValue,
|
|
208
|
+
actualValue,
|
|
209
|
+
signal: 'STYLE_CHANGE',
|
|
210
|
+
severity: 'WARNING',
|
|
211
|
+
description: `Style changed: ${property} from ${expectedValue} to ${actualValue}`
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} catch (err) {
|
|
216
|
+
// Silently skip
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return changes;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Comprehensive behavioral audit
|
|
224
|
+
*/
|
|
225
|
+
async auditBehavior(page, config) {
|
|
226
|
+
const signals = [];
|
|
227
|
+
|
|
228
|
+
// Check critical elements
|
|
229
|
+
if (config.criticalElements) {
|
|
230
|
+
for (const selector of config.criticalElements) {
|
|
231
|
+
const check = await this.checkElementVisibility(page, selector);
|
|
232
|
+
if (check.signal) signals.push(check);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check CTAs
|
|
237
|
+
if (config.criticalCTAs) {
|
|
238
|
+
for (const selector of config.criticalCTAs) {
|
|
239
|
+
const check = await this.checkCTAAccessibility(page, selector);
|
|
240
|
+
if (check.signal) signals.push(check);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Check layout shifts
|
|
245
|
+
if (config.monitoredElements) {
|
|
246
|
+
const shifts = await this.detectLayoutShift(page, config.monitoredElements);
|
|
247
|
+
signals.push(...shifts);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
hasSignals: signals.length > 0,
|
|
252
|
+
signals,
|
|
253
|
+
criticalCount: signals.filter(s => s.severity === 'CRITICAL').length,
|
|
254
|
+
warningCount: signals.filter(s => s.severity === 'WARNING').length
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
BehavioralSignalDetector
|
|
261
|
+
};
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 4 — Breakage Intelligence
|
|
3
|
+
* Aggregate failure taxonomy and hints into actionable summaries
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
BREAK_TYPES,
|
|
8
|
+
IMPACT_DOMAINS,
|
|
9
|
+
SEVERITY_LEVELS,
|
|
10
|
+
getImpactDomain,
|
|
11
|
+
classifyBreakType,
|
|
12
|
+
determineSeverity
|
|
13
|
+
} = require('./failure-taxonomy');
|
|
14
|
+
const { deriveRootCauseHints } = require('./root-cause-analysis');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Analyze a single attempt/flow failure and produce intelligence
|
|
18
|
+
* @param {Object} item - Attempt or flow result with outcome, error, validators, visualDiff, behavioralSignals, etc.
|
|
19
|
+
* @param {boolean} isFlow - true if this is a flow
|
|
20
|
+
* @returns {Object} Intelligence object with taxonomy, hints, actions
|
|
21
|
+
*/
|
|
22
|
+
function analyzeFailure(item, isFlow = false) {
|
|
23
|
+
if (!item || item.outcome === 'SUCCESS') {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const domain = getImpactDomain(isFlow ? item.flowId : item.attemptId);
|
|
28
|
+
const breakType = classifyBreakType(item);
|
|
29
|
+
const severity = determineSeverity(domain, breakType, isFlow);
|
|
30
|
+
const { hints, primaryHint } = deriveRootCauseHints(item, breakType);
|
|
31
|
+
|
|
32
|
+
// Phase 5: Include visual regression metadata
|
|
33
|
+
const intelligence = {
|
|
34
|
+
id: isFlow ? item.flowId : item.attemptId,
|
|
35
|
+
name: isFlow ? item.flowName : item.attemptName,
|
|
36
|
+
outcome: item.outcome,
|
|
37
|
+
breakType,
|
|
38
|
+
domain,
|
|
39
|
+
severity,
|
|
40
|
+
primaryHint,
|
|
41
|
+
hints,
|
|
42
|
+
whyItMatters: generateWhyItMatters(domain, severity, breakType),
|
|
43
|
+
topActions: generateTopActions(breakType, domain)
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Phase 5: Add visual regression details if available
|
|
47
|
+
if (item.visualDiff) {
|
|
48
|
+
intelligence.visualDiff = {
|
|
49
|
+
hasDiff: item.visualDiff.hasDiff,
|
|
50
|
+
percentChange: item.visualDiff.percentChange,
|
|
51
|
+
reason: item.visualDiff.reason,
|
|
52
|
+
diffRegions: item.visualDiff.diffRegions
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Phase 5: Add behavioral signals if available
|
|
57
|
+
if (item.behavioralSignals) {
|
|
58
|
+
intelligence.behavioralSignals = item.behavioralSignals;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return intelligence;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generate 1–3 bullet "Why It Matters" summary
|
|
66
|
+
* @param {string} domain - IMPACT_DOMAIN
|
|
67
|
+
* @param {string} severity - SEVERITY_LEVEL
|
|
68
|
+
* @returns {string[]} Array of 1–3 bullets
|
|
69
|
+
*/
|
|
70
|
+
function generateWhyItMatters(domain, severity, breakType = null) {
|
|
71
|
+
const bullets = [];
|
|
72
|
+
|
|
73
|
+
// Domain-specific impact
|
|
74
|
+
if (domain === IMPACT_DOMAINS.REVENUE) {
|
|
75
|
+
bullets.push('🚨 Revenue impact: Checkout/payment flow is broken. Customers cannot complete purchases.');
|
|
76
|
+
} else if (domain === IMPACT_DOMAINS.LEAD) {
|
|
77
|
+
bullets.push('📉 Lead gen impact: Signup/contact flow is broken. Cannot capture customer interest.');
|
|
78
|
+
} else if (domain === IMPACT_DOMAINS.TRUST) {
|
|
79
|
+
bullets.push('⚠️ Trust impact: Auth/account flow is broken. Users cannot access their data.');
|
|
80
|
+
} else {
|
|
81
|
+
bullets.push('📊 UX impact: Core interaction is broken. User journey degraded.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Severity escalation
|
|
85
|
+
if (severity === SEVERITY_LEVELS.CRITICAL) {
|
|
86
|
+
bullets.push('🔴 CRITICAL: Escalate immediately. Page/API down or core feature broken.');
|
|
87
|
+
} else if (severity === SEVERITY_LEVELS.WARNING) {
|
|
88
|
+
bullets.push('🟡 WARNING: High priority. Fix before peak traffic to avoid customer impact.');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Phase 5: Visual regression context
|
|
92
|
+
if (breakType === BREAK_TYPES.VISUAL) {
|
|
93
|
+
bullets.push('👁️ Visual regression: UI elements changed from baseline (CSS, layout, or styling).');
|
|
94
|
+
if (severity === SEVERITY_LEVELS.CRITICAL) {
|
|
95
|
+
bullets.push('Critical visual change may completely obscure content or block user interaction.');
|
|
96
|
+
} else if (severity === SEVERITY_LEVELS.WARNING) {
|
|
97
|
+
bullets.push('Visual change may degrade readability, accessibility, or user experience.');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return bullets;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generate top 3 actionable next steps
|
|
106
|
+
* @param {string} breakType - BREAK_TYPE
|
|
107
|
+
* @param {string} domain - IMPACT_DOMAIN
|
|
108
|
+
* @returns {string[]} Array of 3 action strings
|
|
109
|
+
*/
|
|
110
|
+
function generateTopActions(breakType, domain) {
|
|
111
|
+
const actions = [];
|
|
112
|
+
|
|
113
|
+
// Break-type-specific actions
|
|
114
|
+
if (breakType === BREAK_TYPES.NAVIGATION || breakType === BREAK_TYPES.NETWORK) {
|
|
115
|
+
actions.push('1. Check server status and network logs for errors');
|
|
116
|
+
actions.push('2. Verify DNS and SSL certificate validity');
|
|
117
|
+
actions.push('3. Check CDN/load balancer for 5xx errors');
|
|
118
|
+
} else if (breakType === BREAK_TYPES.TIMEOUT) {
|
|
119
|
+
actions.push('1. Check server response times and database queries');
|
|
120
|
+
actions.push('2. Review recent deployments for performance regressions');
|
|
121
|
+
actions.push('3. Check if rate limiting is too strict');
|
|
122
|
+
} else if (breakType === BREAK_TYPES.VISUAL) {
|
|
123
|
+
// Phase 5: Visual-specific diagnostic actions
|
|
124
|
+
actions.push('1. Compare baseline screenshot to current; identify CSS/layout changes');
|
|
125
|
+
actions.push('2. Check recent CSS commits, theme changes, or Tailwind/Bootstrap updates');
|
|
126
|
+
actions.push('3. Validate element positioning using browser DevTools layout analysis');
|
|
127
|
+
} else if (breakType === BREAK_TYPES.VALIDATION) {
|
|
128
|
+
actions.push('1. Review frontend code for recent CSS/JS changes');
|
|
129
|
+
actions.push('2. Check browser console for JavaScript errors');
|
|
130
|
+
actions.push('3. Verify DOM selectors match current HTML structure');
|
|
131
|
+
} else if (breakType === BREAK_TYPES.SUBMISSION) {
|
|
132
|
+
actions.push('1. Check form validation rules and error messages');
|
|
133
|
+
actions.push('2. Verify backend API endpoint is reachable');
|
|
134
|
+
actions.push('3. Check for CORS or authentication failures');
|
|
135
|
+
} else if (breakType === BREAK_TYPES.CONSOLE) {
|
|
136
|
+
actions.push('1. Review browser console error logs');
|
|
137
|
+
actions.push('2. Check for third-party script failures');
|
|
138
|
+
actions.push('3. Verify API endpoints and authentication tokens');
|
|
139
|
+
} else {
|
|
140
|
+
actions.push('1. Check application logs for errors');
|
|
141
|
+
actions.push('2. Verify all dependencies are deployed');
|
|
142
|
+
actions.push('3. Check recent changes that might affect this flow');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return actions;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Aggregate all failures into intelligence summary
|
|
150
|
+
* @param {Array} attempts - Attempt results
|
|
151
|
+
* @param {Array} flows - Flow results
|
|
152
|
+
* @returns {Object} Summary with failures, by-domain counts, escalation signals
|
|
153
|
+
*/
|
|
154
|
+
function aggregateIntelligence(attempts = [], flows = []) {
|
|
155
|
+
const allFailures = [];
|
|
156
|
+
const byDomain = {
|
|
157
|
+
[IMPACT_DOMAINS.REVENUE]: [],
|
|
158
|
+
[IMPACT_DOMAINS.LEAD]: [],
|
|
159
|
+
[IMPACT_DOMAINS.TRUST]: [],
|
|
160
|
+
[IMPACT_DOMAINS.UX]: []
|
|
161
|
+
};
|
|
162
|
+
const bySeverity = {
|
|
163
|
+
[SEVERITY_LEVELS.CRITICAL]: [],
|
|
164
|
+
[SEVERITY_LEVELS.WARNING]: [],
|
|
165
|
+
[SEVERITY_LEVELS.INFO]: []
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Analyze attempts
|
|
169
|
+
for (const attempt of attempts) {
|
|
170
|
+
const intel = analyzeFailure(attempt, false);
|
|
171
|
+
if (intel) {
|
|
172
|
+
allFailures.push(intel);
|
|
173
|
+
byDomain[intel.domain].push(intel);
|
|
174
|
+
bySeverity[intel.severity].push(intel);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Analyze flows (higher weight)
|
|
179
|
+
for (const flow of flows) {
|
|
180
|
+
const intel = analyzeFailure(flow, true);
|
|
181
|
+
if (intel) {
|
|
182
|
+
allFailures.push(intel);
|
|
183
|
+
byDomain[intel.domain].push(intel);
|
|
184
|
+
bySeverity[intel.severity].push(intel);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Escalation signals
|
|
189
|
+
const escalationSignals = [];
|
|
190
|
+
if (bySeverity[SEVERITY_LEVELS.CRITICAL].length > 0) {
|
|
191
|
+
escalationSignals.push('CRITICAL failures detected - immediate action required');
|
|
192
|
+
}
|
|
193
|
+
if (byDomain[IMPACT_DOMAINS.REVENUE].length > 0) {
|
|
194
|
+
escalationSignals.push('REVENUE domain affected - financial impact likely');
|
|
195
|
+
}
|
|
196
|
+
if (
|
|
197
|
+
flows.filter(f => f.outcome === 'FAILURE').length > 0 ||
|
|
198
|
+
allFailures.filter(f => f.breakType === BREAK_TYPES.NETWORK || f.breakType === BREAK_TYPES.TIMEOUT).length > 0
|
|
199
|
+
) {
|
|
200
|
+
escalationSignals.push('Infrastructure/availability issue indicated');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
totalFailures: allFailures.length,
|
|
205
|
+
failures: allFailures,
|
|
206
|
+
byDomain,
|
|
207
|
+
bySeverity,
|
|
208
|
+
escalationSignals,
|
|
209
|
+
criticalCount: bySeverity[SEVERITY_LEVELS.CRITICAL].length,
|
|
210
|
+
warningCount: bySeverity[SEVERITY_LEVELS.WARNING].length,
|
|
211
|
+
infoCount: bySeverity[SEVERITY_LEVELS.INFO].length
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = {
|
|
216
|
+
analyzeFailure,
|
|
217
|
+
aggregateIntelligence,
|
|
218
|
+
generateWhyItMatters,
|
|
219
|
+
generateTopActions,
|
|
220
|
+
BREAK_TYPES,
|
|
221
|
+
IMPACT_DOMAINS,
|
|
222
|
+
SEVERITY_LEVELS
|
|
223
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const { chromium } = require('playwright');
|
|
2
|
+
|
|
3
|
+
class GuardianBrowser {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.browser = null;
|
|
6
|
+
this.context = null;
|
|
7
|
+
this.page = null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async launch(timeout = 20000, options = {}) {
|
|
11
|
+
try {
|
|
12
|
+
const launchOptions = {
|
|
13
|
+
headless: options.headless !== undefined ? options.headless : true,
|
|
14
|
+
args: options.args || []
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
this.browser = await chromium.launch(launchOptions);
|
|
18
|
+
|
|
19
|
+
const contextOptions = {
|
|
20
|
+
...options,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Enable HAR recording if requested
|
|
24
|
+
if (options.recordHar) {
|
|
25
|
+
// Note: HAR path must be provided in the actual implementation
|
|
26
|
+
// For now, we'll prepare the context for HAR recording
|
|
27
|
+
contextOptions.recordHar = options.harPath ? { path: options.harPath } : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.context = await this.browser.newContext(contextOptions);
|
|
31
|
+
this.page = await this.context.newPage();
|
|
32
|
+
this.page.setDefaultTimeout(timeout);
|
|
33
|
+
return true;
|
|
34
|
+
} catch (err) {
|
|
35
|
+
throw new Error(`Failed to launch browser: ${err.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async navigate(url, timeout = 20000) {
|
|
40
|
+
try {
|
|
41
|
+
const response = await this.page.goto(url, {
|
|
42
|
+
waitUntil: 'networkidle',
|
|
43
|
+
timeout
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
status: response?.status() || 200,
|
|
49
|
+
url: this.page.url()
|
|
50
|
+
};
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return {
|
|
53
|
+
success: false,
|
|
54
|
+
status: null,
|
|
55
|
+
error: err.message
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getLinks() {
|
|
61
|
+
try {
|
|
62
|
+
const links = await this.page.locator('a[href]').evaluateAll(elements =>
|
|
63
|
+
elements.map(el => ({
|
|
64
|
+
href: el.href,
|
|
65
|
+
text: el.innerText?.trim() || ''
|
|
66
|
+
}))
|
|
67
|
+
);
|
|
68
|
+
return links;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async takeScreenshot(filePath) {
|
|
75
|
+
try {
|
|
76
|
+
await this.page.screenshot({ path: filePath });
|
|
77
|
+
return true;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async close() {
|
|
84
|
+
try {
|
|
85
|
+
if (this.browser) await this.browser.close();
|
|
86
|
+
} catch (err) {
|
|
87
|
+
// Ignore close errors
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { GuardianBrowser };
|