@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,507 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Guardian Attempt Reporter
|
|
3
|
+
* Generates JSON and HTML reports for single user attempts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
class AttemptReporter {
|
|
10
|
+
/**
|
|
11
|
+
* Create attempt report from execution result
|
|
12
|
+
*/
|
|
13
|
+
createReport(attemptResult, baseUrl, attemptId) {
|
|
14
|
+
const { outcome, steps, startedAt, endedAt, totalDurationMs, friction, error, successReason } = attemptResult;
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
version: '1.0.0',
|
|
18
|
+
runId: this._generateRunId(),
|
|
19
|
+
timestamp: new Date().toISOString(),
|
|
20
|
+
attemptId,
|
|
21
|
+
baseUrl,
|
|
22
|
+
outcome,
|
|
23
|
+
meta: {
|
|
24
|
+
goal: this._getGoalForAttempt(attemptId),
|
|
25
|
+
startedAt,
|
|
26
|
+
endedAt,
|
|
27
|
+
durationMs: totalDurationMs
|
|
28
|
+
},
|
|
29
|
+
steps: steps.map((step, index) => ({
|
|
30
|
+
index: index + 1,
|
|
31
|
+
...step
|
|
32
|
+
})),
|
|
33
|
+
friction: {
|
|
34
|
+
isFriction: friction.isFriction,
|
|
35
|
+
signals: friction.signals || [],
|
|
36
|
+
summary: friction.summary || null,
|
|
37
|
+
reasons: friction.reasons,
|
|
38
|
+
thresholds: friction.thresholds,
|
|
39
|
+
metrics: friction.metrics
|
|
40
|
+
},
|
|
41
|
+
error,
|
|
42
|
+
successReason,
|
|
43
|
+
evidence: {
|
|
44
|
+
screenshotDir: 'attempt-screenshots',
|
|
45
|
+
tracePath: 'trace.zip'
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Save report to JSON
|
|
52
|
+
*/
|
|
53
|
+
saveJsonReport(report, artifactsDir) {
|
|
54
|
+
const reportPath = path.join(artifactsDir, 'attempt-report.json');
|
|
55
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
56
|
+
return reportPath;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generate HTML report
|
|
61
|
+
*/
|
|
62
|
+
generateHtmlReport(report) {
|
|
63
|
+
const { outcome, attemptId, meta, steps, friction, error, successReason } = report;
|
|
64
|
+
|
|
65
|
+
const outcomeColor = outcome === 'SUCCESS' ? '#10b981' : outcome === 'FRICTION' ? '#f59e0b' : '#ef4444';
|
|
66
|
+
const outcomeText = outcome === 'SUCCESS' ? '✅ SUCCESS' : outcome === 'FRICTION' ? '⚠️ FRICTION' : '❌ FAILURE';
|
|
67
|
+
const outcomeEmoji = outcome === 'SUCCESS' ? '🟢' : outcome === 'FRICTION' ? '🟡' : '🔴';
|
|
68
|
+
|
|
69
|
+
const stepsHtml = steps.map(step => `
|
|
70
|
+
<tr>
|
|
71
|
+
<td class="step-index">${step.index}</td>
|
|
72
|
+
<td class="step-id">${step.id}</td>
|
|
73
|
+
<td class="step-desc">${step.description || step.type}</td>
|
|
74
|
+
<td class="step-duration">${step.durationMs || 0}ms</td>
|
|
75
|
+
<td class="step-status ${step.status}">
|
|
76
|
+
${step.status === 'success' ? '✅' : '❌'}
|
|
77
|
+
${step.status}
|
|
78
|
+
${step.retries > 0 ? `<br/>(${step.retries} retries)` : ''}
|
|
79
|
+
</td>
|
|
80
|
+
${step.error ? `<td class="step-error">${step.error}</td>` : '<td></td>'}
|
|
81
|
+
</tr>
|
|
82
|
+
`).join('');
|
|
83
|
+
|
|
84
|
+
const frictionHtml = friction.isFriction ? `
|
|
85
|
+
<div class="friction-block">
|
|
86
|
+
<h3>⚠️ Friction Detected</h3>
|
|
87
|
+
${friction.summary ? `<p class="friction-summary">${friction.summary}</p>` : ''}
|
|
88
|
+
|
|
89
|
+
${friction.signals && friction.signals.length > 0 ? `
|
|
90
|
+
<div class="friction-signals">
|
|
91
|
+
<h4>Friction Signals:</h4>
|
|
92
|
+
${friction.signals.map(signal => {
|
|
93
|
+
const severityClass = signal.severity || 'medium';
|
|
94
|
+
const severityLabel = signal.severity ? signal.severity.toUpperCase() : 'MEDIUM';
|
|
95
|
+
return `
|
|
96
|
+
<div class="signal-card severity-${severityClass}">
|
|
97
|
+
<div class="signal-header">
|
|
98
|
+
<span class="signal-id">${signal.id}</span>
|
|
99
|
+
<span class="signal-severity severity-${severityClass}">${severityLabel}</span>
|
|
100
|
+
</div>
|
|
101
|
+
<p class="signal-description">${signal.description}</p>
|
|
102
|
+
<div class="signal-metrics">
|
|
103
|
+
<div class="signal-metric">
|
|
104
|
+
<strong>Metric:</strong> ${signal.metric}
|
|
105
|
+
</div>
|
|
106
|
+
<div class="signal-metric">
|
|
107
|
+
<strong>Threshold:</strong> ${signal.threshold}
|
|
108
|
+
</div>
|
|
109
|
+
<div class="signal-metric">
|
|
110
|
+
<strong>Observed:</strong> ${signal.observedValue}
|
|
111
|
+
</div>
|
|
112
|
+
${signal.affectedStepId ? `
|
|
113
|
+
<div class="signal-metric">
|
|
114
|
+
<strong>Affected Step:</strong> ${signal.affectedStepId}
|
|
115
|
+
</div>
|
|
116
|
+
` : ''}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
`;
|
|
120
|
+
}).join('')}
|
|
121
|
+
</div>
|
|
122
|
+
` : ''}
|
|
123
|
+
|
|
124
|
+
${friction.reasons && friction.reasons.length > 0 ? `
|
|
125
|
+
<details class="legacy-friction">
|
|
126
|
+
<summary>Legacy Friction Reasons</summary>
|
|
127
|
+
<ul>
|
|
128
|
+
${friction.reasons.map(reason => `<li>${reason}</li>`).join('')}
|
|
129
|
+
</ul>
|
|
130
|
+
</details>
|
|
131
|
+
` : ''}
|
|
132
|
+
|
|
133
|
+
<p><strong>Metrics:</strong></p>
|
|
134
|
+
<ul>
|
|
135
|
+
<li>Total duration: ${friction.metrics.totalDurationMs || 0}ms</li>
|
|
136
|
+
<li>Steps executed: ${friction.metrics.stepCount || 0}</li>
|
|
137
|
+
<li>Total retries: ${friction.metrics.totalRetries || 0}</li>
|
|
138
|
+
<li>Max step duration: ${friction.metrics.maxStepDurationMs || 0}ms</li>
|
|
139
|
+
</ul>
|
|
140
|
+
</div>
|
|
141
|
+
` : '';
|
|
142
|
+
|
|
143
|
+
const errorHtml = error ? `
|
|
144
|
+
<div class="error-block">
|
|
145
|
+
<h3>Error Details</h3>
|
|
146
|
+
<p class="error-message">${error}</p>
|
|
147
|
+
</div>
|
|
148
|
+
` : '';
|
|
149
|
+
|
|
150
|
+
const successHtml = successReason ? `
|
|
151
|
+
<div class="success-block">
|
|
152
|
+
<h3>✅ Success Criteria Met</h3>
|
|
153
|
+
<p>${successReason}</p>
|
|
154
|
+
</div>
|
|
155
|
+
` : '';
|
|
156
|
+
|
|
157
|
+
const html = `<!DOCTYPE html>
|
|
158
|
+
<html lang="en">
|
|
159
|
+
<head>
|
|
160
|
+
<meta charset="UTF-8">
|
|
161
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
162
|
+
<title>Guardian Attempt Report</title>
|
|
163
|
+
<style>
|
|
164
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
165
|
+
body {
|
|
166
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
167
|
+
background: #f5f5f5;
|
|
168
|
+
color: #333;
|
|
169
|
+
line-height: 1.6;
|
|
170
|
+
}
|
|
171
|
+
.container {
|
|
172
|
+
max-width: 1200px;
|
|
173
|
+
margin: 0 auto;
|
|
174
|
+
padding: 20px;
|
|
175
|
+
}
|
|
176
|
+
.header {
|
|
177
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
178
|
+
color: white;
|
|
179
|
+
padding: 30px 20px;
|
|
180
|
+
border-radius: 10px;
|
|
181
|
+
margin-bottom: 30px;
|
|
182
|
+
}
|
|
183
|
+
.header h1 {
|
|
184
|
+
font-size: 2em;
|
|
185
|
+
margin-bottom: 10px;
|
|
186
|
+
}
|
|
187
|
+
.outcome-badge {
|
|
188
|
+
display: inline-block;
|
|
189
|
+
padding: 12px 20px;
|
|
190
|
+
border-radius: 25px;
|
|
191
|
+
font-weight: bold;
|
|
192
|
+
font-size: 1.1em;
|
|
193
|
+
background: ${outcomeColor};
|
|
194
|
+
color: white;
|
|
195
|
+
margin-top: 15px;
|
|
196
|
+
}
|
|
197
|
+
.meta-info {
|
|
198
|
+
background: white;
|
|
199
|
+
padding: 20px;
|
|
200
|
+
border-radius: 8px;
|
|
201
|
+
margin-bottom: 20px;
|
|
202
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
203
|
+
}
|
|
204
|
+
.meta-item {
|
|
205
|
+
display: flex;
|
|
206
|
+
justify-content: space-between;
|
|
207
|
+
padding: 10px 0;
|
|
208
|
+
border-bottom: 1px solid #eee;
|
|
209
|
+
}
|
|
210
|
+
.meta-item:last-child {
|
|
211
|
+
border-bottom: none;
|
|
212
|
+
}
|
|
213
|
+
.meta-label {
|
|
214
|
+
font-weight: bold;
|
|
215
|
+
color: #666;
|
|
216
|
+
min-width: 150px;
|
|
217
|
+
}
|
|
218
|
+
.meta-value {
|
|
219
|
+
color: #333;
|
|
220
|
+
}
|
|
221
|
+
.steps-table {
|
|
222
|
+
width: 100%;
|
|
223
|
+
border-collapse: collapse;
|
|
224
|
+
background: white;
|
|
225
|
+
border-radius: 8px;
|
|
226
|
+
overflow: hidden;
|
|
227
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
228
|
+
margin-bottom: 20px;
|
|
229
|
+
}
|
|
230
|
+
.steps-table thead {
|
|
231
|
+
background: #f0f0f0;
|
|
232
|
+
font-weight: bold;
|
|
233
|
+
}
|
|
234
|
+
.steps-table th, .steps-table td {
|
|
235
|
+
padding: 12px;
|
|
236
|
+
text-align: left;
|
|
237
|
+
border-bottom: 1px solid #ddd;
|
|
238
|
+
}
|
|
239
|
+
.steps-table tbody tr:hover {
|
|
240
|
+
background: #f9f9f9;
|
|
241
|
+
}
|
|
242
|
+
.step-index {
|
|
243
|
+
font-weight: bold;
|
|
244
|
+
color: #667eea;
|
|
245
|
+
width: 60px;
|
|
246
|
+
}
|
|
247
|
+
.step-id {
|
|
248
|
+
font-family: monospace;
|
|
249
|
+
font-size: 0.9em;
|
|
250
|
+
color: #666;
|
|
251
|
+
}
|
|
252
|
+
.step-duration {
|
|
253
|
+
font-family: monospace;
|
|
254
|
+
width: 80px;
|
|
255
|
+
text-align: right;
|
|
256
|
+
}
|
|
257
|
+
.step-status {
|
|
258
|
+
text-align: center;
|
|
259
|
+
font-weight: bold;
|
|
260
|
+
}
|
|
261
|
+
.step-status.success {
|
|
262
|
+
color: #10b981;
|
|
263
|
+
}
|
|
264
|
+
.step-status.failed {
|
|
265
|
+
color: #ef4444;
|
|
266
|
+
}
|
|
267
|
+
.step-error {
|
|
268
|
+
color: #ef4444;
|
|
269
|
+
font-size: 0.9em;
|
|
270
|
+
}
|
|
271
|
+
.friction-block, .error-block, .success-block {
|
|
272
|
+
background: white;
|
|
273
|
+
padding: 20px;
|
|
274
|
+
border-radius: 8px;
|
|
275
|
+
margin-bottom: 20px;
|
|
276
|
+
border-left: 4px solid #f59e0b;
|
|
277
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
278
|
+
}
|
|
279
|
+
.error-block {
|
|
280
|
+
border-left-color: #ef4444;
|
|
281
|
+
}
|
|
282
|
+
.success-block {
|
|
283
|
+
border-left-color: #10b981;
|
|
284
|
+
}
|
|
285
|
+
.friction-block h3, .error-block h3, .success-block h3 {
|
|
286
|
+
margin-bottom: 15px;
|
|
287
|
+
font-size: 1.1em;
|
|
288
|
+
}
|
|
289
|
+
.friction-block ul, .error-block ul, .success-block ul {
|
|
290
|
+
margin-left: 20px;
|
|
291
|
+
}
|
|
292
|
+
.friction-block li, .error-block li, .success-block li {
|
|
293
|
+
margin-bottom: 8px;
|
|
294
|
+
}
|
|
295
|
+
.error-message {
|
|
296
|
+
background: #fee2e2;
|
|
297
|
+
padding: 12px;
|
|
298
|
+
border-radius: 5px;
|
|
299
|
+
border-left: 3px solid #ef4444;
|
|
300
|
+
font-family: monospace;
|
|
301
|
+
font-size: 0.9em;
|
|
302
|
+
}
|
|
303
|
+
.friction-summary {
|
|
304
|
+
font-weight: bold;
|
|
305
|
+
font-size: 1.05em;
|
|
306
|
+
margin-bottom: 15px;
|
|
307
|
+
color: #f59e0b;
|
|
308
|
+
}
|
|
309
|
+
.friction-signals {
|
|
310
|
+
margin: 20px 0;
|
|
311
|
+
}
|
|
312
|
+
.friction-signals h4 {
|
|
313
|
+
margin-bottom: 12px;
|
|
314
|
+
color: #333;
|
|
315
|
+
}
|
|
316
|
+
.signal-card {
|
|
317
|
+
background: #fefce8;
|
|
318
|
+
border: 1px solid #fde047;
|
|
319
|
+
border-left: 4px solid #f59e0b;
|
|
320
|
+
border-radius: 6px;
|
|
321
|
+
padding: 15px;
|
|
322
|
+
margin-bottom: 12px;
|
|
323
|
+
}
|
|
324
|
+
.signal-card.severity-low {
|
|
325
|
+
background: #f0f9ff;
|
|
326
|
+
border-color: #7dd3fc;
|
|
327
|
+
border-left-color: #0ea5e9;
|
|
328
|
+
}
|
|
329
|
+
.signal-card.severity-medium {
|
|
330
|
+
background: #fefce8;
|
|
331
|
+
border-color: #fde047;
|
|
332
|
+
border-left-color: #f59e0b;
|
|
333
|
+
}
|
|
334
|
+
.signal-card.severity-high {
|
|
335
|
+
background: #fef2f2;
|
|
336
|
+
border-color: #fca5a5;
|
|
337
|
+
border-left-color: #ef4444;
|
|
338
|
+
}
|
|
339
|
+
.signal-header {
|
|
340
|
+
display: flex;
|
|
341
|
+
justify-content: space-between;
|
|
342
|
+
align-items: center;
|
|
343
|
+
margin-bottom: 10px;
|
|
344
|
+
}
|
|
345
|
+
.signal-id {
|
|
346
|
+
font-family: monospace;
|
|
347
|
+
font-weight: bold;
|
|
348
|
+
font-size: 0.95em;
|
|
349
|
+
color: #333;
|
|
350
|
+
}
|
|
351
|
+
.signal-severity {
|
|
352
|
+
padding: 3px 8px;
|
|
353
|
+
border-radius: 4px;
|
|
354
|
+
font-size: 0.75em;
|
|
355
|
+
font-weight: bold;
|
|
356
|
+
}
|
|
357
|
+
.signal-severity.severity-low {
|
|
358
|
+
background: #0ea5e9;
|
|
359
|
+
color: white;
|
|
360
|
+
}
|
|
361
|
+
.signal-severity.severity-medium {
|
|
362
|
+
background: #f59e0b;
|
|
363
|
+
color: white;
|
|
364
|
+
}
|
|
365
|
+
.signal-severity.severity-high {
|
|
366
|
+
background: #ef4444;
|
|
367
|
+
color: white;
|
|
368
|
+
}
|
|
369
|
+
.signal-description {
|
|
370
|
+
color: #666;
|
|
371
|
+
margin-bottom: 12px;
|
|
372
|
+
line-height: 1.5;
|
|
373
|
+
}
|
|
374
|
+
.signal-metrics {
|
|
375
|
+
display: grid;
|
|
376
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
377
|
+
gap: 8px;
|
|
378
|
+
font-size: 0.9em;
|
|
379
|
+
}
|
|
380
|
+
.signal-metric {
|
|
381
|
+
background: white;
|
|
382
|
+
padding: 6px 10px;
|
|
383
|
+
border-radius: 4px;
|
|
384
|
+
border: 1px solid #e5e7eb;
|
|
385
|
+
}
|
|
386
|
+
.signal-metric strong {
|
|
387
|
+
color: #666;
|
|
388
|
+
font-size: 0.85em;
|
|
389
|
+
display: block;
|
|
390
|
+
margin-bottom: 2px;
|
|
391
|
+
}
|
|
392
|
+
.legacy-friction {
|
|
393
|
+
margin-top: 15px;
|
|
394
|
+
font-size: 0.9em;
|
|
395
|
+
}
|
|
396
|
+
.legacy-friction summary {
|
|
397
|
+
cursor: pointer;
|
|
398
|
+
color: #666;
|
|
399
|
+
font-weight: bold;
|
|
400
|
+
margin-bottom: 8px;
|
|
401
|
+
}
|
|
402
|
+
.legacy-friction summary:hover {
|
|
403
|
+
color: #333;
|
|
404
|
+
}
|
|
405
|
+
.footer {
|
|
406
|
+
text-align: center;
|
|
407
|
+
color: #999;
|
|
408
|
+
font-size: 0.9em;
|
|
409
|
+
margin-top: 30px;
|
|
410
|
+
padding-top: 20px;
|
|
411
|
+
border-top: 1px solid #ddd;
|
|
412
|
+
}
|
|
413
|
+
</style>
|
|
414
|
+
</head>
|
|
415
|
+
<body>
|
|
416
|
+
<div class="container">
|
|
417
|
+
<div class="header">
|
|
418
|
+
<h1>🛡️ Guardian Attempt Report</h1>
|
|
419
|
+
<div class="outcome-badge">${outcomeEmoji} ${outcomeText}</div>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
<div class="meta-info">
|
|
423
|
+
<div class="meta-item">
|
|
424
|
+
<span class="meta-label">Attempt</span>
|
|
425
|
+
<span class="meta-value">${attemptId}</span>
|
|
426
|
+
</div>
|
|
427
|
+
<div class="meta-item">
|
|
428
|
+
<span class="meta-label">URL</span>
|
|
429
|
+
<span class="meta-value">${report.baseUrl}</span>
|
|
430
|
+
</div>
|
|
431
|
+
<div class="meta-item">
|
|
432
|
+
<span class="meta-label">Started</span>
|
|
433
|
+
<span class="meta-value">${meta.startedAt}</span>
|
|
434
|
+
</div>
|
|
435
|
+
<div class="meta-item">
|
|
436
|
+
<span class="meta-label">Duration</span>
|
|
437
|
+
<span class="meta-value">${meta.durationMs}ms</span>
|
|
438
|
+
</div>
|
|
439
|
+
<div class="meta-item">
|
|
440
|
+
<span class="meta-label">Steps</span>
|
|
441
|
+
<span class="meta-value">${steps.length}</span>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
|
|
445
|
+
<h2>Steps Timeline</h2>
|
|
446
|
+
<table class="steps-table">
|
|
447
|
+
<thead>
|
|
448
|
+
<tr>
|
|
449
|
+
<th>#</th>
|
|
450
|
+
<th>Step ID</th>
|
|
451
|
+
<th>Description</th>
|
|
452
|
+
<th>Duration</th>
|
|
453
|
+
<th>Status</th>
|
|
454
|
+
<th>Error (if any)</th>
|
|
455
|
+
</tr>
|
|
456
|
+
</thead>
|
|
457
|
+
<tbody>
|
|
458
|
+
${stepsHtml}
|
|
459
|
+
</tbody>
|
|
460
|
+
</table>
|
|
461
|
+
|
|
462
|
+
${successHtml}
|
|
463
|
+
${frictionHtml}
|
|
464
|
+
${errorHtml}
|
|
465
|
+
|
|
466
|
+
<div class="footer">
|
|
467
|
+
<p>Generated by Guardian Phase 1 — Single User Attempt MVP</p>
|
|
468
|
+
<p>Report ID: ${report.runId}</p>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
</body>
|
|
472
|
+
</html>`;
|
|
473
|
+
|
|
474
|
+
return html;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Save HTML report
|
|
479
|
+
*/
|
|
480
|
+
saveHtmlReport(html, artifactsDir) {
|
|
481
|
+
const reportPath = path.join(artifactsDir, 'attempt-report.html');
|
|
482
|
+
fs.writeFileSync(reportPath, html);
|
|
483
|
+
return reportPath;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Generate run ID
|
|
488
|
+
*/
|
|
489
|
+
_generateRunId() {
|
|
490
|
+
const now = new Date();
|
|
491
|
+
return now.toISOString().replace(/[:\-]/g, '').substring(0, 15).replace('T', '-');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Get goal description for attempt
|
|
496
|
+
*/
|
|
497
|
+
_getGoalForAttempt(attemptId) {
|
|
498
|
+
const goals = {
|
|
499
|
+
contact_form: 'User submits a contact form successfully',
|
|
500
|
+
language_switch: 'User switches site language successfully',
|
|
501
|
+
newsletter_signup: 'User signs up for newsletter'
|
|
502
|
+
};
|
|
503
|
+
return goals[attemptId] || 'Complete user attempt';
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
module.exports = { AttemptReporter };
|