har-o-scope 0.1.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/LICENSE +661 -0
- package/README.md +179 -0
- package/completions/har-o-scope.bash +64 -0
- package/completions/har-o-scope.fish +43 -0
- package/completions/har-o-scope.zsh +63 -0
- package/dist/cli/colors.d.ts +17 -0
- package/dist/cli/colors.d.ts.map +1 -0
- package/dist/cli/colors.js +54 -0
- package/dist/cli/demo.d.ts +7 -0
- package/dist/cli/demo.d.ts.map +1 -0
- package/dist/cli/demo.js +62 -0
- package/dist/cli/formatters.d.ts +12 -0
- package/dist/cli/formatters.d.ts.map +1 -0
- package/dist/cli/formatters.js +249 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +260 -0
- package/dist/cli/rules.d.ts +3 -0
- package/dist/cli/rules.d.ts.map +1 -0
- package/dist/cli/rules.js +36 -0
- package/dist/cli/sarif.d.ts +9 -0
- package/dist/cli/sarif.d.ts.map +1 -0
- package/dist/cli/sarif.js +104 -0
- package/dist/lib/analyze.d.ts +10 -0
- package/dist/lib/analyze.d.ts.map +1 -0
- package/dist/lib/analyze.js +83 -0
- package/dist/lib/classifier.d.ts +8 -0
- package/dist/lib/classifier.d.ts.map +1 -0
- package/dist/lib/classifier.js +74 -0
- package/dist/lib/diff.d.ts +15 -0
- package/dist/lib/diff.d.ts.map +1 -0
- package/dist/lib/diff.js +130 -0
- package/dist/lib/errors.d.ts +56 -0
- package/dist/lib/errors.d.ts.map +1 -0
- package/dist/lib/errors.js +65 -0
- package/dist/lib/evaluate.d.ts +19 -0
- package/dist/lib/evaluate.d.ts.map +1 -0
- package/dist/lib/evaluate.js +189 -0
- package/dist/lib/health-score.d.ts +18 -0
- package/dist/lib/health-score.d.ts.map +1 -0
- package/dist/lib/health-score.js +74 -0
- package/dist/lib/html-report.d.ts +15 -0
- package/dist/lib/html-report.d.ts.map +1 -0
- package/dist/lib/html-report.js +299 -0
- package/dist/lib/index.d.ts +26 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/index.js +24 -0
- package/dist/lib/normalizer.d.ts +18 -0
- package/dist/lib/normalizer.d.ts.map +1 -0
- package/dist/lib/normalizer.js +201 -0
- package/dist/lib/rule-engine.d.ts +12 -0
- package/dist/lib/rule-engine.d.ts.map +1 -0
- package/dist/lib/rule-engine.js +122 -0
- package/dist/lib/sanitizer.d.ts +10 -0
- package/dist/lib/sanitizer.d.ts.map +1 -0
- package/dist/lib/sanitizer.js +129 -0
- package/dist/lib/schema.d.ts +85 -0
- package/dist/lib/schema.d.ts.map +1 -0
- package/dist/lib/schema.js +1 -0
- package/dist/lib/trace-sanitizer.d.ts +30 -0
- package/dist/lib/trace-sanitizer.d.ts.map +1 -0
- package/dist/lib/trace-sanitizer.js +85 -0
- package/dist/lib/types.d.ts +161 -0
- package/dist/lib/types.d.ts.map +1 -0
- package/dist/lib/types.js +1 -0
- package/dist/lib/unbatched-detect.d.ts +7 -0
- package/dist/lib/unbatched-detect.d.ts.map +1 -0
- package/dist/lib/unbatched-detect.js +59 -0
- package/dist/lib/validator.d.ts +4 -0
- package/dist/lib/validator.d.ts.map +1 -0
- package/dist/lib/validator.js +409 -0
- package/package.json +98 -0
- package/rules/generic/issue-rules.yaml +292 -0
- package/rules/generic/shared/base-conditions.yaml +28 -0
- package/rules/generic/shared/filters.yaml +12 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level analyze pipeline: parse -> normalize -> rules -> classify -> score.
|
|
3
|
+
* This is the main entry point for library consumers.
|
|
4
|
+
*/
|
|
5
|
+
import type { Har } from 'har-format';
|
|
6
|
+
import type { AnalysisResult, AnalysisOptions } from './types.js';
|
|
7
|
+
import type { IssueRulesFile, SharedConditionsFile, FiltersFile } from './schema.js';
|
|
8
|
+
export declare function setBuiltinRules(rules: IssueRulesFile, conditions?: SharedConditionsFile, filters?: FiltersFile): void;
|
|
9
|
+
export declare function analyze(input: string | Har, options?: AnalysisOptions): AnalysisResult;
|
|
10
|
+
//# sourceMappingURL=analyze.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../src/lib/analyze.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA;AACrC,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EAIhB,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAapF,wBAAgB,eAAe,CAC7B,KAAK,EAAE,cAAc,EACrB,UAAU,CAAC,EAAE,oBAAoB,EACjC,OAAO,CAAC,EAAE,WAAW,GACpB,IAAI,CAIN;AAmBD,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,cAAc,CA+EtF"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { parseHar, normalizeHar } from './normalizer.js';
|
|
2
|
+
import { evaluateRules } from './rule-engine.js';
|
|
3
|
+
import { classifyRootCause } from './classifier.js';
|
|
4
|
+
import { detectUnbatchedApis } from './unbatched-detect.js';
|
|
5
|
+
import { createWarning } from './errors.js';
|
|
6
|
+
// ── Built-in rules (loaded lazily) ──────────────────────────────
|
|
7
|
+
let builtinRules = null;
|
|
8
|
+
let builtinConditions = null;
|
|
9
|
+
let builtinFilters = null;
|
|
10
|
+
export function setBuiltinRules(rules, conditions, filters) {
|
|
11
|
+
builtinRules = rules;
|
|
12
|
+
builtinConditions = conditions ?? null;
|
|
13
|
+
builtinFilters = filters ?? null;
|
|
14
|
+
}
|
|
15
|
+
// ── Severity filter ─────────────────────────────────────────────
|
|
16
|
+
const SEVERITY_ORDER = { info: 0, warning: 1, critical: 2 };
|
|
17
|
+
function filterBySeverity(findings, minSeverity) {
|
|
18
|
+
if (!minSeverity || minSeverity === 'info')
|
|
19
|
+
return findings;
|
|
20
|
+
const minOrder = SEVERITY_ORDER[minSeverity] ?? 0;
|
|
21
|
+
return findings.filter((f) => (SEVERITY_ORDER[f.severity] ?? 0) >= minOrder);
|
|
22
|
+
}
|
|
23
|
+
// ── Main analyze function ───────────────────────────────────────
|
|
24
|
+
export function analyze(input, options) {
|
|
25
|
+
const startTime = performance.now();
|
|
26
|
+
const warnings = [];
|
|
27
|
+
// Parse if string input
|
|
28
|
+
const har = typeof input === 'string' ? parseHar(input) : input;
|
|
29
|
+
// Normalize
|
|
30
|
+
const { entries, warnings: normalizeWarnings } = normalizeHar(har);
|
|
31
|
+
warnings.push(...normalizeWarnings);
|
|
32
|
+
// Evaluate rules
|
|
33
|
+
let findings = [];
|
|
34
|
+
if (!options?.noBuiltin && builtinRules) {
|
|
35
|
+
const builtinFindings = evaluateRules(builtinRules, entries, builtinConditions ?? undefined, builtinFilters ?? undefined);
|
|
36
|
+
findings.push(...builtinFindings);
|
|
37
|
+
}
|
|
38
|
+
// Custom rules
|
|
39
|
+
if (options?.customRulesData) {
|
|
40
|
+
for (const data of options.customRulesData) {
|
|
41
|
+
try {
|
|
42
|
+
const customFile = data;
|
|
43
|
+
if (customFile?.rules) {
|
|
44
|
+
const customFindings = evaluateRules(customFile, entries);
|
|
45
|
+
findings.push(...customFindings);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
warnings.push(createWarning('RULE001', `Failed to evaluate custom rules: ${e instanceof Error ? e.message : 'unknown error'}`, 'Check that custom rules follow the YAML rule schema.'));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// TypeScript-only rules
|
|
54
|
+
const unbatchedFinding = detectUnbatchedApis(entries);
|
|
55
|
+
if (unbatchedFinding)
|
|
56
|
+
findings.push(unbatchedFinding);
|
|
57
|
+
// Severity filter
|
|
58
|
+
findings = filterBySeverity(findings, options?.minSeverity);
|
|
59
|
+
// Classify root cause
|
|
60
|
+
const rootCause = classifyRootCause(findings, entries, builtinRules ?? undefined);
|
|
61
|
+
const analysisTimeMs = Math.round(performance.now() - startTime);
|
|
62
|
+
// Compute total wall-clock time
|
|
63
|
+
let totalTimeMs = 0;
|
|
64
|
+
if (entries.length > 0) {
|
|
65
|
+
const maxEnd = Math.max(...entries.map((e) => e.startTimeMs + e.totalDuration));
|
|
66
|
+
const minStart = Math.min(...entries.map((e) => e.startTimeMs));
|
|
67
|
+
totalTimeMs = Math.round(maxEnd - minStart);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
entries,
|
|
71
|
+
findings,
|
|
72
|
+
rootCause,
|
|
73
|
+
warnings,
|
|
74
|
+
metadata: {
|
|
75
|
+
rulesEvaluated: Object.keys(builtinRules?.rules ?? {}).length +
|
|
76
|
+
(options?.customRulesData?.length ?? 0) + 1, // +1 for unbatched detect
|
|
77
|
+
customRulesLoaded: options?.customRulesData?.length ?? 0,
|
|
78
|
+
analysisTimeMs,
|
|
79
|
+
totalRequests: entries.length,
|
|
80
|
+
totalTimeMs,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root cause classifier: weighted scoring across findings.
|
|
3
|
+
* Produces client/network/server scores from rule-defined weights.
|
|
4
|
+
*/
|
|
5
|
+
import type { Finding, RootCauseResult, NormalizedEntry } from './types.js';
|
|
6
|
+
import type { IssueRulesFile } from './schema.js';
|
|
7
|
+
export declare function classifyRootCause(findings: Finding[], entries: NormalizedEntry[], rulesFile?: IssueRulesFile, customWeights?: Map<string, Record<string, number>>): RootCauseResult;
|
|
8
|
+
//# sourceMappingURL=classifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"classifier.d.ts","sourceRoot":"","sources":["../../src/lib/classifier.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC3E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AASjD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,eAAe,EAAE,EAC1B,SAAS,CAAC,EAAE,cAAc,EAC1B,aAAa,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAClD,eAAe,CAqDjB"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { getRootCauseWeights } from './rule-engine.js';
|
|
2
|
+
const SEVERITY_MULTIPLIER = {
|
|
3
|
+
critical: 3,
|
|
4
|
+
warning: 2,
|
|
5
|
+
info: 1,
|
|
6
|
+
};
|
|
7
|
+
export function classifyRootCause(findings, entries, rulesFile, customWeights) {
|
|
8
|
+
const scores = { client: 0, network: 0, server: 0 };
|
|
9
|
+
// Merge weights from rules file + custom weights
|
|
10
|
+
const weights = new Map();
|
|
11
|
+
if (rulesFile) {
|
|
12
|
+
for (const [id, w] of getRootCauseWeights(rulesFile)) {
|
|
13
|
+
weights.set(id, w);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (customWeights) {
|
|
17
|
+
for (const [id, w] of customWeights) {
|
|
18
|
+
weights.set(id, w);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
for (const finding of findings) {
|
|
22
|
+
// Skip WebSocket-only findings from root cause scoring
|
|
23
|
+
if (finding.affectedEntries.length > 0) {
|
|
24
|
+
const allWebSocket = finding.affectedEntries.every((idx) => entries[idx]?.isWebSocket);
|
|
25
|
+
if (allWebSocket)
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
const ruleWeights = weights.get(finding.ruleId);
|
|
29
|
+
const severityMult = SEVERITY_MULTIPLIER[finding.severity] ?? 1;
|
|
30
|
+
const countFactor = Math.min(finding.affectedEntries.length, 20) / 20 + 0.5;
|
|
31
|
+
if (ruleWeights) {
|
|
32
|
+
scores.client += (ruleWeights.client ?? 0) * severityMult * countFactor;
|
|
33
|
+
scores.network += (ruleWeights.network ?? 0) * severityMult * countFactor;
|
|
34
|
+
scores.server += (ruleWeights.server ?? 0) * severityMult * countFactor;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// Default weights based on category
|
|
38
|
+
const defaultWeights = getDefaultWeights(finding.category);
|
|
39
|
+
scores.client += defaultWeights.client * severityMult * countFactor;
|
|
40
|
+
scores.network += defaultWeights.network * severityMult * countFactor;
|
|
41
|
+
scores.server += defaultWeights.server * severityMult * countFactor;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Normalize to 0-1 range
|
|
45
|
+
const total = scores.client + scores.network + scores.server;
|
|
46
|
+
if (total === 0) {
|
|
47
|
+
return { client: 0, network: 0, server: 0 };
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
client: Math.round((scores.client / total) * 100) / 100,
|
|
51
|
+
network: Math.round((scores.network / total) * 100) / 100,
|
|
52
|
+
server: Math.round((scores.server / total) * 100) / 100,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function getDefaultWeights(category) {
|
|
56
|
+
switch (category) {
|
|
57
|
+
case 'server':
|
|
58
|
+
return { client: 0, network: 0, server: 2 };
|
|
59
|
+
case 'network':
|
|
60
|
+
return { client: 0, network: 2, server: 0 };
|
|
61
|
+
case 'client':
|
|
62
|
+
return { client: 2, network: 0, server: 0 };
|
|
63
|
+
case 'errors':
|
|
64
|
+
return { client: 0, network: 1, server: 2 };
|
|
65
|
+
case 'optimization':
|
|
66
|
+
return { client: 1, network: 0, server: 1 };
|
|
67
|
+
case 'security':
|
|
68
|
+
return { client: 0, network: 1, server: 1 };
|
|
69
|
+
case 'performance':
|
|
70
|
+
return { client: 1, network: 1, server: 1 };
|
|
71
|
+
default:
|
|
72
|
+
return { client: 1, network: 1, server: 1 };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff engine: compare two HAR analyses.
|
|
3
|
+
*
|
|
4
|
+
* URL-based grouping: strip query params, normalize numeric/UUID path segments.
|
|
5
|
+
* Statistical comparison of timing groups + finding diffing.
|
|
6
|
+
*/
|
|
7
|
+
import type { AnalysisResult, DiffResult } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a URL for grouping. Strip query params and fragments.
|
|
10
|
+
* Replace purely numeric path segments and UUIDs with '*'.
|
|
11
|
+
* Mixed alphanumeric segments (e.g., /v2/, /page3/) are preserved.
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeUrlForGrouping(url: string): string;
|
|
14
|
+
export declare function diff(before: AnalysisResult, after: AnalysisResult): DiffResult;
|
|
15
|
+
//# sourceMappingURL=diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../../src/lib/diff.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EAKX,MAAM,YAAY,CAAA;AAQnB;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAwB3D;AAgED,wBAAgB,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,KAAK,EAAE,cAAc,GAAG,UAAU,CA4D9E"}
|
package/dist/lib/diff.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { computeHealthScore } from './health-score.js';
|
|
2
|
+
// ── URL normalization ───────────────────────────────────────────
|
|
3
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4
|
+
const PURE_NUMERIC = /^\d+$/;
|
|
5
|
+
/**
|
|
6
|
+
* Normalize a URL for grouping. Strip query params and fragments.
|
|
7
|
+
* Replace purely numeric path segments and UUIDs with '*'.
|
|
8
|
+
* Mixed alphanumeric segments (e.g., /v2/, /page3/) are preserved.
|
|
9
|
+
*/
|
|
10
|
+
export function normalizeUrlForGrouping(url) {
|
|
11
|
+
let pathname;
|
|
12
|
+
let origin;
|
|
13
|
+
try {
|
|
14
|
+
const parsed = new URL(url);
|
|
15
|
+
pathname = parsed.pathname;
|
|
16
|
+
origin = parsed.origin;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// Fallback for malformed URLs
|
|
20
|
+
const protoEnd = url.indexOf('//') + 2;
|
|
21
|
+
const pathStart = url.indexOf('/', protoEnd);
|
|
22
|
+
const queryStart = url.indexOf('?');
|
|
23
|
+
if (pathStart === -1)
|
|
24
|
+
return url;
|
|
25
|
+
origin = url.slice(0, pathStart);
|
|
26
|
+
pathname = queryStart === -1 ? url.slice(pathStart) : url.slice(pathStart, queryStart);
|
|
27
|
+
}
|
|
28
|
+
const segments = pathname.split('/').map((seg) => {
|
|
29
|
+
if (PURE_NUMERIC.test(seg))
|
|
30
|
+
return '*';
|
|
31
|
+
if (UUID_PATTERN.test(seg))
|
|
32
|
+
return '*';
|
|
33
|
+
return seg;
|
|
34
|
+
});
|
|
35
|
+
return origin + segments.join('/');
|
|
36
|
+
}
|
|
37
|
+
function groupByUrl(entries) {
|
|
38
|
+
const groups = new Map();
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
const pattern = normalizeUrlForGrouping(entry.entry.request?.url ?? '');
|
|
41
|
+
const existing = groups.get(pattern);
|
|
42
|
+
if (existing) {
|
|
43
|
+
existing.entries.push(entry);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
groups.set(pattern, { pattern, entries: [entry] });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return groups;
|
|
50
|
+
}
|
|
51
|
+
function avgDuration(entries) {
|
|
52
|
+
if (entries.length === 0)
|
|
53
|
+
return 0;
|
|
54
|
+
const sum = entries.reduce((acc, e) => acc + e.totalDuration, 0);
|
|
55
|
+
return Math.round(sum / entries.length);
|
|
56
|
+
}
|
|
57
|
+
// ── Finding diff ────────────────────────────────────────────────
|
|
58
|
+
function diffFindings(before, after) {
|
|
59
|
+
const beforeIds = new Set(before.map((f) => f.ruleId));
|
|
60
|
+
const afterIds = new Set(after.map((f) => f.ruleId));
|
|
61
|
+
const beforeMap = new Map(before.map((f) => [f.ruleId, f]));
|
|
62
|
+
const afterMap = new Map(after.map((f) => [f.ruleId, f]));
|
|
63
|
+
const newFindings = after.filter((f) => !beforeIds.has(f.ruleId));
|
|
64
|
+
const resolvedFindings = before.filter((f) => !afterIds.has(f.ruleId));
|
|
65
|
+
const persistedFindings = [];
|
|
66
|
+
for (const ruleId of beforeIds) {
|
|
67
|
+
if (!afterIds.has(ruleId))
|
|
68
|
+
continue;
|
|
69
|
+
const b = beforeMap.get(ruleId);
|
|
70
|
+
const a = afterMap.get(ruleId);
|
|
71
|
+
persistedFindings.push({
|
|
72
|
+
ruleId,
|
|
73
|
+
beforeSeverity: b.severity,
|
|
74
|
+
afterSeverity: a.severity,
|
|
75
|
+
beforeCount: b.affectedEntries.length,
|
|
76
|
+
afterCount: a.affectedEntries.length,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return { newFindings, resolvedFindings, persistedFindings };
|
|
80
|
+
}
|
|
81
|
+
// ── Main diff ───────────────────────────────────────────────────
|
|
82
|
+
export function diff(before, after) {
|
|
83
|
+
const beforeScore = computeHealthScore(before);
|
|
84
|
+
const afterScore = computeHealthScore(after);
|
|
85
|
+
const beforeGroups = groupByUrl(before.entries);
|
|
86
|
+
const afterGroups = groupByUrl(after.entries);
|
|
87
|
+
// Compute timing deltas for URL patterns that appear in both
|
|
88
|
+
const allPatterns = new Set([...beforeGroups.keys(), ...afterGroups.keys()]);
|
|
89
|
+
const timingDeltas = [];
|
|
90
|
+
for (const pattern of allPatterns) {
|
|
91
|
+
const bg = beforeGroups.get(pattern);
|
|
92
|
+
const ag = afterGroups.get(pattern);
|
|
93
|
+
const beforeAvg = bg ? avgDuration(bg.entries) : 0;
|
|
94
|
+
const afterAvg = ag ? avgDuration(ag.entries) : 0;
|
|
95
|
+
const beforeCount = bg?.entries.length ?? 0;
|
|
96
|
+
const afterCount = ag?.entries.length ?? 0;
|
|
97
|
+
// Only include if there's something meaningful to compare
|
|
98
|
+
if (beforeCount === 0 && afterCount === 0)
|
|
99
|
+
continue;
|
|
100
|
+
const deltaMs = afterAvg - beforeAvg;
|
|
101
|
+
const deltaPercent = beforeAvg > 0 ? Math.round((deltaMs / beforeAvg) * 100) : 0;
|
|
102
|
+
timingDeltas.push({
|
|
103
|
+
urlPattern: pattern,
|
|
104
|
+
beforeCount,
|
|
105
|
+
afterCount,
|
|
106
|
+
beforeAvgMs: beforeAvg,
|
|
107
|
+
afterAvgMs: afterAvg,
|
|
108
|
+
deltaMs,
|
|
109
|
+
deltaPercent,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// Sort by absolute delta descending (biggest changes first)
|
|
113
|
+
timingDeltas.sort((a, b) => Math.abs(b.deltaMs) - Math.abs(a.deltaMs));
|
|
114
|
+
const { newFindings, resolvedFindings, persistedFindings } = diffFindings(before.findings, after.findings);
|
|
115
|
+
const beforeTime = before.entries.length > 0
|
|
116
|
+
? Math.max(...before.entries.map((e) => e.startTimeMs + e.totalDuration)) - Math.min(...before.entries.map((e) => e.startTimeMs))
|
|
117
|
+
: 0;
|
|
118
|
+
const afterTime = after.entries.length > 0
|
|
119
|
+
? Math.max(...after.entries.map((e) => e.startTimeMs + e.totalDuration)) - Math.min(...after.entries.map((e) => e.startTimeMs))
|
|
120
|
+
: 0;
|
|
121
|
+
return {
|
|
122
|
+
scoreDelta: afterScore.score - beforeScore.score,
|
|
123
|
+
newFindings,
|
|
124
|
+
resolvedFindings,
|
|
125
|
+
persistedFindings,
|
|
126
|
+
timingDeltas,
|
|
127
|
+
requestCountDelta: after.entries.length - before.entries.length,
|
|
128
|
+
totalTimeDelta: Math.round(afterTime - beforeTime),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error system for har-o-scope.
|
|
3
|
+
* Two channels: thrown HarError for fatal, result.warnings[] for non-fatal.
|
|
4
|
+
*/
|
|
5
|
+
export interface HarErrorOptions {
|
|
6
|
+
code: string;
|
|
7
|
+
message: string;
|
|
8
|
+
help: string;
|
|
9
|
+
docsUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class HarError extends Error {
|
|
12
|
+
readonly code: string;
|
|
13
|
+
readonly help: string;
|
|
14
|
+
readonly docsUrl: string;
|
|
15
|
+
constructor(opts: HarErrorOptions);
|
|
16
|
+
}
|
|
17
|
+
/** HAR parse/validation errors */
|
|
18
|
+
export declare const HAR_ERRORS: {
|
|
19
|
+
/** Input is not valid JSON */
|
|
20
|
+
readonly HAR001: "HAR001";
|
|
21
|
+
/** Valid JSON but not a valid HAR file (missing log property) */
|
|
22
|
+
readonly HAR002: "HAR002";
|
|
23
|
+
/** HAR file has no entries */
|
|
24
|
+
readonly HAR003: "HAR003";
|
|
25
|
+
/** HAR file exceeds size limit */
|
|
26
|
+
readonly HAR004: "HAR004";
|
|
27
|
+
};
|
|
28
|
+
/** Rule engine errors */
|
|
29
|
+
export declare const RULE_ERRORS: {
|
|
30
|
+
/** Invalid YAML syntax in rule file */
|
|
31
|
+
readonly RULE001: "RULE001";
|
|
32
|
+
/** Unknown field path in rule condition */
|
|
33
|
+
readonly RULE002: "RULE002";
|
|
34
|
+
/** Circular inheritance in rule composition */
|
|
35
|
+
readonly RULE003: "RULE003";
|
|
36
|
+
/** Invalid rule schema (missing required fields) */
|
|
37
|
+
readonly RULE004: "RULE004";
|
|
38
|
+
/** Contradictory conditions detected */
|
|
39
|
+
readonly RULE005: "RULE005";
|
|
40
|
+
/** Condition nesting too deep (>5 levels) */
|
|
41
|
+
readonly RULE006: "RULE006";
|
|
42
|
+
/** Unknown operator in condition */
|
|
43
|
+
readonly RULE007: "RULE007";
|
|
44
|
+
/** Invalid severity value */
|
|
45
|
+
readonly RULE008: "RULE008";
|
|
46
|
+
};
|
|
47
|
+
/** CLI-specific errors */
|
|
48
|
+
export declare const CLI_ERRORS: {
|
|
49
|
+
/** File not found */
|
|
50
|
+
readonly CLI001: "CLI001";
|
|
51
|
+
/** Invalid CLI arguments */
|
|
52
|
+
readonly CLI002: "CLI002";
|
|
53
|
+
};
|
|
54
|
+
import type { AnalysisWarning } from './types.js';
|
|
55
|
+
export declare function createWarning(code: string, message: string, help: string): AnalysisWarning;
|
|
56
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAID,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;gBAEZ,IAAI,EAAE,eAAe;CAUlC;AAID,kCAAkC;AAClC,eAAO,MAAM,UAAU;IACrB,8BAA8B;;IAE9B,iEAAiE;;IAEjE,8BAA8B;;IAE9B,kCAAkC;;CAE1B,CAAA;AAEV,yBAAyB;AACzB,eAAO,MAAM,WAAW;IACtB,uCAAuC;;IAEvC,2CAA2C;;IAE3C,+CAA+C;;IAE/C,oDAAoD;;IAEpD,wCAAwC;;IAExC,6CAA6C;;IAE7C,oCAAoC;;IAEpC,6BAA6B;;CAErB,CAAA;AAEV,0BAA0B;AAC1B,eAAO,MAAM,UAAU;IACrB,qBAAqB;;IAErB,4BAA4B;;CAEpB,CAAA;AAIV,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,eAAe,CAOjB"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error system for har-o-scope.
|
|
3
|
+
* Two channels: thrown HarError for fatal, result.warnings[] for non-fatal.
|
|
4
|
+
*/
|
|
5
|
+
const DOCS_BASE = 'https://github.com/vegaPDX/har-o-scope/blob/main/docs/errors';
|
|
6
|
+
export class HarError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
help;
|
|
9
|
+
docsUrl;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
super(opts.message);
|
|
12
|
+
this.name = 'HarError';
|
|
13
|
+
this.code = opts.code;
|
|
14
|
+
this.help = opts.help;
|
|
15
|
+
this.docsUrl = opts.docsUrl ?? `${DOCS_BASE}/${opts.code}.md`;
|
|
16
|
+
// Maintain proper prototype chain for instanceof checks
|
|
17
|
+
Object.setPrototypeOf(this, HarError.prototype);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// ── Error code constants ───────────────────────────────────────
|
|
21
|
+
/** HAR parse/validation errors */
|
|
22
|
+
export const HAR_ERRORS = {
|
|
23
|
+
/** Input is not valid JSON */
|
|
24
|
+
HAR001: 'HAR001',
|
|
25
|
+
/** Valid JSON but not a valid HAR file (missing log property) */
|
|
26
|
+
HAR002: 'HAR002',
|
|
27
|
+
/** HAR file has no entries */
|
|
28
|
+
HAR003: 'HAR003',
|
|
29
|
+
/** HAR file exceeds size limit */
|
|
30
|
+
HAR004: 'HAR004',
|
|
31
|
+
};
|
|
32
|
+
/** Rule engine errors */
|
|
33
|
+
export const RULE_ERRORS = {
|
|
34
|
+
/** Invalid YAML syntax in rule file */
|
|
35
|
+
RULE001: 'RULE001',
|
|
36
|
+
/** Unknown field path in rule condition */
|
|
37
|
+
RULE002: 'RULE002',
|
|
38
|
+
/** Circular inheritance in rule composition */
|
|
39
|
+
RULE003: 'RULE003',
|
|
40
|
+
/** Invalid rule schema (missing required fields) */
|
|
41
|
+
RULE004: 'RULE004',
|
|
42
|
+
/** Contradictory conditions detected */
|
|
43
|
+
RULE005: 'RULE005',
|
|
44
|
+
/** Condition nesting too deep (>5 levels) */
|
|
45
|
+
RULE006: 'RULE006',
|
|
46
|
+
/** Unknown operator in condition */
|
|
47
|
+
RULE007: 'RULE007',
|
|
48
|
+
/** Invalid severity value */
|
|
49
|
+
RULE008: 'RULE008',
|
|
50
|
+
};
|
|
51
|
+
/** CLI-specific errors */
|
|
52
|
+
export const CLI_ERRORS = {
|
|
53
|
+
/** File not found */
|
|
54
|
+
CLI001: 'CLI001',
|
|
55
|
+
/** Invalid CLI arguments */
|
|
56
|
+
CLI002: 'CLI002',
|
|
57
|
+
};
|
|
58
|
+
export function createWarning(code, message, help) {
|
|
59
|
+
return {
|
|
60
|
+
code,
|
|
61
|
+
message,
|
|
62
|
+
help,
|
|
63
|
+
docsUrl: `${DOCS_BASE}/${code}.md`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure rule evaluation functions. No DOM dependencies.
|
|
3
|
+
* Works in Web Worker, Node.js CLI, and library consumers.
|
|
4
|
+
*
|
|
5
|
+
* Adapted from the reference engine (yaml_base evaluate.ts).
|
|
6
|
+
* Generalized: DisplayEntry -> NormalizedEntry, stripped SF-specific fields.
|
|
7
|
+
*/
|
|
8
|
+
import type { FieldCondition, ResponseHeaderCondition, ConditionGroup, ConditionNode, SeverityEscalation, ImpactSpec } from './schema.js';
|
|
9
|
+
import type { IssueSeverity } from './types.js';
|
|
10
|
+
export declare function getField(obj: unknown, path: string): unknown;
|
|
11
|
+
export declare function isFieldCondition(node: ConditionNode): node is FieldCondition;
|
|
12
|
+
export declare function isHeaderCondition(node: ConditionNode): node is ResponseHeaderCondition;
|
|
13
|
+
export declare function isConditionGroup(node: ConditionNode): node is ConditionGroup;
|
|
14
|
+
export declare function evaluateFieldCondition(entry: unknown, cond: FieldCondition): boolean;
|
|
15
|
+
export declare function evaluateCondition(entry: unknown, node: ConditionNode): boolean;
|
|
16
|
+
export declare function computeSeverity(baseSeverity: IssueSeverity, escalation: SeverityEscalation | undefined, affectedCount: number, totalCount: number): IssueSeverity;
|
|
17
|
+
export declare function computeImpact(entries: unknown[], affectedIndices: number[], impactSpec: ImpactSpec | undefined): number;
|
|
18
|
+
export declare function interpolate(template: string, vars: Record<string, string | number>): string;
|
|
19
|
+
//# sourceMappingURL=evaluate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluate.d.ts","sourceRoot":"","sources":["../../src/lib/evaluate.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EACV,cAAc,EACd,uBAAuB,EACvB,cAAc,EACd,aAAa,EACb,kBAAkB,EAClB,UAAU,EACX,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAM/C,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAS5D;AAoCD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,IAAI,cAAc,CAE5E;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,IAAI,uBAAuB,CAEtF;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,IAAI,cAAc,CAE5E;AAWD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAsBpF;AAmCD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,GAAG,OAAO,CAQ9E;AAUD,wBAAgB,eAAe,CAC7B,YAAY,EAAE,aAAa,EAC3B,UAAU,EAAE,kBAAkB,GAAG,SAAS,EAC1C,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,GACjB,aAAa,CAmBf;AAID,wBAAgB,aAAa,CAC3B,OAAO,EAAE,OAAO,EAAE,EAClB,eAAe,EAAE,MAAM,EAAE,EACzB,UAAU,EAAE,UAAU,GAAG,SAAS,GACjC,MAAM,CAwBR;AAID,wBAAgB,WAAW,CACzB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,GACpC,MAAM,CAKR"}
|