perfgraph 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 +21 -0
- package/README.md +280 -0
- package/dist/causal/builder.d.ts +34 -0
- package/dist/causal/builder.d.ts.map +1 -0
- package/dist/causal/builder.js +132 -0
- package/dist/causal/builder.js.map +1 -0
- package/dist/causal/index.d.ts +11 -0
- package/dist/causal/index.d.ts.map +1 -0
- package/dist/causal/index.js +10 -0
- package/dist/causal/index.js.map +1 -0
- package/dist/causal/rules/js.d.ts +49 -0
- package/dist/causal/rules/js.d.ts.map +1 -0
- package/dist/causal/rules/js.js +209 -0
- package/dist/causal/rules/js.js.map +1 -0
- package/dist/causal/rules/layout.d.ts +39 -0
- package/dist/causal/rules/layout.d.ts.map +1 -0
- package/dist/causal/rules/layout.js +169 -0
- package/dist/causal/rules/layout.js.map +1 -0
- package/dist/causal/rules/lcp.d.ts +49 -0
- package/dist/causal/rules/lcp.d.ts.map +1 -0
- package/dist/causal/rules/lcp.js +250 -0
- package/dist/causal/rules/lcp.js.map +1 -0
- package/dist/causal/rules/network.d.ts +39 -0
- package/dist/causal/rules/network.d.ts.map +1 -0
- package/dist/causal/rules/network.js +140 -0
- package/dist/causal/rules/network.js.map +1 -0
- package/dist/causal/types.d.ts +142 -0
- package/dist/causal/types.d.ts.map +1 -0
- package/dist/causal/types.js +88 -0
- package/dist/causal/types.js.map +1 -0
- package/dist/cli/analyze.d.ts +33 -0
- package/dist/cli/analyze.d.ts.map +1 -0
- package/dist/cli/analyze.js +137 -0
- package/dist/cli/analyze.js.map +1 -0
- package/dist/cli/collect.d.ts +28 -0
- package/dist/cli/collect.d.ts.map +1 -0
- package/dist/cli/collect.js +125 -0
- package/dist/cli/collect.js.map +1 -0
- package/dist/cli/extract.d.ts +31 -0
- package/dist/cli/extract.d.ts.map +1 -0
- package/dist/cli/extract.js +135 -0
- package/dist/cli/extract.js.map +1 -0
- package/dist/cli/mcp.d.ts +14 -0
- package/dist/cli/mcp.d.ts.map +1 -0
- package/dist/cli/mcp.js +53 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/cli/normalize.d.ts +58 -0
- package/dist/cli/normalize.d.ts.map +1 -0
- package/dist/cli/normalize.js +179 -0
- package/dist/cli/normalize.js.map +1 -0
- package/dist/cli/report.d.ts +33 -0
- package/dist/cli/report.d.ts.map +1 -0
- package/dist/cli/report.js +137 -0
- package/dist/cli/report.js.map +1 -0
- package/dist/cli/run.d.ts +49 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +243 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/collect/buffer-monitor.d.ts +25 -0
- package/dist/collect/buffer-monitor.d.ts.map +1 -0
- package/dist/collect/buffer-monitor.js +69 -0
- package/dist/collect/buffer-monitor.js.map +1 -0
- package/dist/collect/console.d.ts +25 -0
- package/dist/collect/console.d.ts.map +1 -0
- package/dist/collect/console.js +82 -0
- package/dist/collect/console.js.map +1 -0
- package/dist/collect/coverage.d.ts +26 -0
- package/dist/collect/coverage.d.ts.map +1 -0
- package/dist/collect/coverage.js +99 -0
- package/dist/collect/coverage.js.map +1 -0
- package/dist/collect/dom.d.ts +37 -0
- package/dist/collect/dom.d.ts.map +1 -0
- package/dist/collect/dom.js +118 -0
- package/dist/collect/dom.js.map +1 -0
- package/dist/collect/lighthouse.d.ts +41 -0
- package/dist/collect/lighthouse.d.ts.map +1 -0
- package/dist/collect/lighthouse.js +111 -0
- package/dist/collect/lighthouse.js.map +1 -0
- package/dist/collect/network.d.ts +32 -0
- package/dist/collect/network.d.ts.map +1 -0
- package/dist/collect/network.js +236 -0
- package/dist/collect/network.js.map +1 -0
- package/dist/collect/orchestrator.d.ts +23 -0
- package/dist/collect/orchestrator.d.ts.map +1 -0
- package/dist/collect/orchestrator.js +350 -0
- package/dist/collect/orchestrator.js.map +1 -0
- package/dist/collect/performance.d.ts +28 -0
- package/dist/collect/performance.d.ts.map +1 -0
- package/dist/collect/performance.js +108 -0
- package/dist/collect/performance.js.map +1 -0
- package/dist/collect/runtime.d.ts +26 -0
- package/dist/collect/runtime.d.ts.map +1 -0
- package/dist/collect/runtime.js +81 -0
- package/dist/collect/runtime.js.map +1 -0
- package/dist/collect/session.d.ts +58 -0
- package/dist/collect/session.d.ts.map +1 -0
- package/dist/collect/session.js +162 -0
- package/dist/collect/session.js.map +1 -0
- package/dist/collect/trace.d.ts +40 -0
- package/dist/collect/trace.d.ts.map +1 -0
- package/dist/collect/trace.js +127 -0
- package/dist/collect/trace.js.map +1 -0
- package/dist/collect/types.d.ts +317 -0
- package/dist/collect/types.d.ts.map +1 -0
- package/dist/collect/types.js +12 -0
- package/dist/collect/types.js.map +1 -0
- package/dist/distill/insights.d.ts +67 -0
- package/dist/distill/insights.d.ts.map +1 -0
- package/dist/distill/insights.js +317 -0
- package/dist/distill/insights.js.map +1 -0
- package/dist/distill/summaries.d.ts +71 -0
- package/dist/distill/summaries.d.ts.map +1 -0
- package/dist/distill/summaries.js +165 -0
- package/dist/distill/summaries.js.map +1 -0
- package/dist/extract/critical-path.d.ts +17 -0
- package/dist/extract/critical-path.d.ts.map +1 -0
- package/dist/extract/critical-path.js +95 -0
- package/dist/extract/critical-path.js.map +1 -0
- package/dist/extract/index.d.ts +16 -0
- package/dist/extract/index.d.ts.map +1 -0
- package/dist/extract/index.js +32 -0
- package/dist/extract/index.js.map +1 -0
- package/dist/extract/js-hotspots.d.ts +15 -0
- package/dist/extract/js-hotspots.d.ts.map +1 -0
- package/dist/extract/js-hotspots.js +22 -0
- package/dist/extract/js-hotspots.js.map +1 -0
- package/dist/extract/layout-shifts.d.ts +19 -0
- package/dist/extract/layout-shifts.d.ts.map +1 -0
- package/dist/extract/layout-shifts.js +47 -0
- package/dist/extract/layout-shifts.js.map +1 -0
- package/dist/extract/lcp-breakdown.d.ts +19 -0
- package/dist/extract/lcp-breakdown.d.ts.map +1 -0
- package/dist/extract/lcp-breakdown.js +110 -0
- package/dist/extract/lcp-breakdown.js.map +1 -0
- package/dist/extract/main-thread.d.ts +16 -0
- package/dist/extract/main-thread.d.ts.map +1 -0
- package/dist/extract/main-thread.js +41 -0
- package/dist/extract/main-thread.js.map +1 -0
- package/dist/extract/render-blocking.d.ts +16 -0
- package/dist/extract/render-blocking.d.ts.map +1 -0
- package/dist/extract/render-blocking.js +79 -0
- package/dist/extract/render-blocking.js.map +1 -0
- package/dist/extract/third-party.d.ts +15 -0
- package/dist/extract/third-party.d.ts.map +1 -0
- package/dist/extract/third-party.js +127 -0
- package/dist/extract/third-party.js.map +1 -0
- package/dist/extract/types.d.ts +183 -0
- package/dist/extract/types.d.ts.map +1 -0
- package/dist/extract/types.js +118 -0
- package/dist/extract/types.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +188 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/handlers.d.ts +72 -0
- package/dist/mcp/handlers.d.ts.map +1 -0
- package/dist/mcp/handlers.js +285 -0
- package/dist/mcp/handlers.js.map +1 -0
- package/dist/mcp/progress.d.ts +37 -0
- package/dist/mcp/progress.d.ts.map +1 -0
- package/dist/mcp/progress.js +51 -0
- package/dist/mcp/progress.js.map +1 -0
- package/dist/mcp/prompts.d.ts +57 -0
- package/dist/mcp/prompts.d.ts.map +1 -0
- package/dist/mcp/prompts.js +225 -0
- package/dist/mcp/prompts.js.map +1 -0
- package/dist/mcp/resources.d.ts +48 -0
- package/dist/mcp/resources.d.ts.map +1 -0
- package/dist/mcp/resources.js +149 -0
- package/dist/mcp/resources.js.map +1 -0
- package/dist/mcp/server.d.ts +34 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +452 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/types.d.ts +47 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +67 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/normalize/clock.d.ts +40 -0
- package/dist/normalize/clock.d.ts.map +1 -0
- package/dist/normalize/clock.js +96 -0
- package/dist/normalize/clock.js.map +1 -0
- package/dist/normalize/dom-ir.d.ts +16 -0
- package/dist/normalize/dom-ir.d.ts.map +1 -0
- package/dist/normalize/dom-ir.js +48 -0
- package/dist/normalize/dom-ir.js.map +1 -0
- package/dist/normalize/index.d.ts +23 -0
- package/dist/normalize/index.d.ts.map +1 -0
- package/dist/normalize/index.js +118 -0
- package/dist/normalize/index.js.map +1 -0
- package/dist/normalize/lighthouse-insights.d.ts +27 -0
- package/dist/normalize/lighthouse-insights.d.ts.map +1 -0
- package/dist/normalize/lighthouse-insights.js +318 -0
- package/dist/normalize/lighthouse-insights.js.map +1 -0
- package/dist/normalize/lighthouse-ir.d.ts +25 -0
- package/dist/normalize/lighthouse-ir.d.ts.map +1 -0
- package/dist/normalize/lighthouse-ir.js +151 -0
- package/dist/normalize/lighthouse-ir.js.map +1 -0
- package/dist/normalize/network-ir.d.ts +23 -0
- package/dist/normalize/network-ir.d.ts.map +1 -0
- package/dist/normalize/network-ir.js +180 -0
- package/dist/normalize/network-ir.js.map +1 -0
- package/dist/normalize/runtime-ir.d.ts +18 -0
- package/dist/normalize/runtime-ir.d.ts.map +1 -0
- package/dist/normalize/runtime-ir.js +114 -0
- package/dist/normalize/runtime-ir.js.map +1 -0
- package/dist/normalize/trace-ir.d.ts +20 -0
- package/dist/normalize/trace-ir.d.ts.map +1 -0
- package/dist/normalize/trace-ir.js +224 -0
- package/dist/normalize/trace-ir.js.map +1 -0
- package/dist/normalize/types.d.ts +536 -0
- package/dist/normalize/types.d.ts.map +1 -0
- package/dist/normalize/types.js +242 -0
- package/dist/normalize/types.js.map +1 -0
- package/dist/output/manifest.d.ts +44 -0
- package/dist/output/manifest.d.ts.map +1 -0
- package/dist/output/manifest.js +232 -0
- package/dist/output/manifest.js.map +1 -0
- package/dist/output/toon.d.ts +19 -0
- package/dist/output/toon.d.ts.map +1 -0
- package/dist/output/toon.js +22 -0
- package/dist/output/toon.js.map +1 -0
- package/dist/output/writer.d.ts +31 -0
- package/dist/output/writer.d.ts.map +1 -0
- package/dist/output/writer.js +51 -0
- package/dist/output/writer.js.map +1 -0
- package/dist/report/analyzer.d.ts +29 -0
- package/dist/report/analyzer.d.ts.map +1 -0
- package/dist/report/analyzer.js +330 -0
- package/dist/report/analyzer.js.map +1 -0
- package/dist/report/index.d.ts +13 -0
- package/dist/report/index.d.ts.map +1 -0
- package/dist/report/index.js +12 -0
- package/dist/report/index.js.map +1 -0
- package/dist/report/markdown.d.ts +21 -0
- package/dist/report/markdown.d.ts.map +1 -0
- package/dist/report/markdown.js +182 -0
- package/dist/report/markdown.js.map +1 -0
- package/dist/report/remediations.d.ts +42 -0
- package/dist/report/remediations.d.ts.map +1 -0
- package/dist/report/remediations.js +179 -0
- package/dist/report/remediations.js.map +1 -0
- package/dist/report/scorer.d.ts +35 -0
- package/dist/report/scorer.d.ts.map +1 -0
- package/dist/report/scorer.js +72 -0
- package/dist/report/scorer.js.map +1 -0
- package/dist/report/types.d.ts +201 -0
- package/dist/report/types.d.ts.map +1 -0
- package/dist/report/types.js +149 -0
- package/dist/report/types.js.map +1 -0
- package/dist/shared/fs-utils.d.ts +12 -0
- package/dist/shared/fs-utils.d.ts.map +1 -0
- package/dist/shared/fs-utils.js +44 -0
- package/dist/shared/fs-utils.js.map +1 -0
- package/dist/shared/types.d.ts +69 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +18 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/shared/utils.d.ts +6 -0
- package/dist/shared/utils.d.ts.map +1 -0
- package/dist/shared/utils.js +9 -0
- package/dist/shared/utils.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clock domain normalization utilities.
|
|
3
|
+
*
|
|
4
|
+
* Raw data from different CDP domains arrives on different clock domains.
|
|
5
|
+
* This module converts all timestamps to a common epoch: milliseconds
|
|
6
|
+
* relative to the navigation start (navigationStart).
|
|
7
|
+
*
|
|
8
|
+
* Supported clock domains:
|
|
9
|
+
* - cdp-monotonic-us : Chromium Tracing microsecond monotonic clock
|
|
10
|
+
* - cdp-monotonic-ms : CDP millisecond monotonic clock
|
|
11
|
+
* - epoch-ms : Standard Unix epoch in milliseconds
|
|
12
|
+
* - epoch-s : Standard Unix epoch in seconds
|
|
13
|
+
* - network-monotonic-s : Network domain monotonic seconds (with wall-time offset)
|
|
14
|
+
* - lighthouse-ms : Lighthouse-reported relative milliseconds
|
|
15
|
+
*/
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Converters
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
/**
|
|
20
|
+
* Convert a raw timestamp to milliseconds relative to navigationStart.
|
|
21
|
+
*
|
|
22
|
+
* NaN / Infinity guard: if the result is not a finite number, return 0.
|
|
23
|
+
*/
|
|
24
|
+
export function toRelativeMs(raw, domain, anchor) {
|
|
25
|
+
let result;
|
|
26
|
+
switch (domain) {
|
|
27
|
+
case 'cdp-monotonic-us':
|
|
28
|
+
// Chrome tracing timestamps are in microseconds from boot
|
|
29
|
+
result = raw / 1000 - anchor.navigationStart;
|
|
30
|
+
break;
|
|
31
|
+
case 'cdp-monotonic-ms':
|
|
32
|
+
// Some CDP APIs return ms-monotonic
|
|
33
|
+
result = raw - anchor.navigationStart;
|
|
34
|
+
break;
|
|
35
|
+
case 'epoch-ms':
|
|
36
|
+
// Standard epoch milliseconds
|
|
37
|
+
result = raw - anchor.navigationStart;
|
|
38
|
+
break;
|
|
39
|
+
case 'epoch-s':
|
|
40
|
+
// Epoch seconds → multiply to ms, then shift
|
|
41
|
+
result = raw * 1000 - anchor.navigationStart;
|
|
42
|
+
break;
|
|
43
|
+
case 'network-monotonic-s': {
|
|
44
|
+
// Network events use a monotonic seconds clock anchored to a wall time.
|
|
45
|
+
// The wall-time offset is derived from the first request's wallTime.
|
|
46
|
+
const wallOffset = anchor.firstRequestWallTime ?? 0;
|
|
47
|
+
result =
|
|
48
|
+
raw * 1000 + (anchor.navigationStart - wallOffset * 1000);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case 'lighthouse-ms':
|
|
52
|
+
// Lighthouse already reports relative ms
|
|
53
|
+
result = raw;
|
|
54
|
+
break;
|
|
55
|
+
default: {
|
|
56
|
+
// Exhaustiveness check (should never reach here if domain union is complete)
|
|
57
|
+
const _exhaustive = domain;
|
|
58
|
+
result = 0;
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// NaN / Infinity guard
|
|
63
|
+
if (!Number.isFinite(result)) {
|
|
64
|
+
return 0;
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolve a ClockAnchor from a RawDataBundle.
|
|
70
|
+
*
|
|
71
|
+
* navigationStart is extracted from the performance metrics (Timestamp metric)
|
|
72
|
+
* or falls back to 0. firstRequestWallTime is taken from the first network
|
|
73
|
+
* request's wallTime field if available.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveClockAnchor(raw) {
|
|
76
|
+
// Extract navigation start from performance metrics
|
|
77
|
+
let navigationStart = 0;
|
|
78
|
+
const perfData = raw.performance;
|
|
79
|
+
if (perfData?.metrics) {
|
|
80
|
+
const navStartMetric = perfData.metrics.find((m) => m.name === 'NavigationStart');
|
|
81
|
+
if (navStartMetric) {
|
|
82
|
+
navigationStart = navStartMetric.value;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Extract first request wall time from network data
|
|
86
|
+
let firstRequestWallTime;
|
|
87
|
+
const netData = raw.network;
|
|
88
|
+
if (netData?.requests && netData.requests.length > 0) {
|
|
89
|
+
const firstReq = netData.requests[0];
|
|
90
|
+
if (firstReq && typeof firstReq.wallTime === 'number') {
|
|
91
|
+
firstRequestWallTime = firstReq.wallTime;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { navigationStart, firstRequestWallTime };
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=clock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.js","sourceRoot":"","sources":["../../src/normalize/clock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAyBH,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAC1B,GAAW,EACX,MAAmB,EACnB,MAAmB;IAEnB,IAAI,MAAc,CAAC;IAEnB,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,kBAAkB;YACrB,0DAA0D;YAC1D,MAAM,GAAG,GAAG,GAAG,IAAI,GAAG,MAAM,CAAC,eAAe,CAAC;YAC7C,MAAM;QAER,KAAK,kBAAkB;YACrB,oCAAoC;YACpC,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,eAAe,CAAC;YACtC,MAAM;QAER,KAAK,UAAU;YACb,8BAA8B;YAC9B,MAAM,GAAG,GAAG,GAAG,MAAM,CAAC,eAAe,CAAC;YACtC,MAAM;QAER,KAAK,SAAS;YACZ,6CAA6C;YAC7C,MAAM,GAAG,GAAG,GAAG,IAAI,GAAG,MAAM,CAAC,eAAe,CAAC;YAC7C,MAAM;QAER,KAAK,qBAAqB,CAAC,CAAC,CAAC;YAC3B,wEAAwE;YACxE,qEAAqE;YACrE,MAAM,UAAU,GAAG,MAAM,CAAC,oBAAoB,IAAI,CAAC,CAAC;YACpD,MAAM;gBACJ,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,eAAe,GAAG,UAAU,GAAG,IAAI,CAAC,CAAC;YAC5D,MAAM;QACR,CAAC;QAED,KAAK,eAAe;YAClB,yCAAyC;YACzC,MAAM,GAAG,GAAG,CAAC;YACb,MAAM;QAER,OAAO,CAAC,CAAC,CAAC;YACR,6EAA6E;YAC7E,MAAM,WAAW,GAAU,MAAM,CAAC;YAClC,MAAM,GAAG,CAAC,CAAC;YACX,MAAM;QACR,CAAC;IACH,CAAC;IAED,uBAAuB;IACvB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,CAAC;IACX,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAkB;IACnD,oDAAoD;IACpD,IAAI,eAAe,GAAG,CAAC,CAAC;IAExB,MAAM,QAAQ,GAAG,GAAG,CAAC,WAER,CAAC;IACd,IAAI,QAAQ,EAAE,OAAO,EAAE,CAAC;QACtB,MAAM,cAAc,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAC1C,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,iBAAiB,CACpC,CAAC;QACF,IAAI,cAAc,EAAE,CAAC;YACnB,eAAe,GAAG,cAAc,CAAC,KAAK,CAAC;QACzC,CAAC;IACH,CAAC;IAED,oDAAoD;IACpD,IAAI,oBAAwC,CAAC;IAE7C,MAAM,OAAO,GAAG,GAAG,CAAC,OAEP,CAAC;IACd,IAAI,OAAO,EAAE,QAAQ,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrD,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QACrC,IAAI,QAAQ,IAAI,OAAO,QAAQ,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACtD,oBAAoB,GAAG,QAAQ,CAAC,QAAQ,CAAC;QAC3C,CAAC;IACH,CAAC;IAED,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,CAAC;AACnD,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOMIR builder from DOM raw data.
|
|
3
|
+
*
|
|
4
|
+
* Converts CDP DOM snapshot data into a normalized DOMIR structure with
|
|
5
|
+
* stats, tag distribution, and layout shift candidate analysis.
|
|
6
|
+
*/
|
|
7
|
+
import type { DOMIR } from './types.js';
|
|
8
|
+
import type { DomRawData } from '../collect/types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Build a complete DOMIR from DOM raw data.
|
|
11
|
+
*
|
|
12
|
+
* @param dom - Raw DOM data from the DOMCollector
|
|
13
|
+
* @returns A fully-constructed DOMIR
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildDOMIR(dom: DomRawData): DOMIR;
|
|
16
|
+
//# sourceMappingURL=dom-ir.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom-ir.d.ts","sourceRoot":"","sources":["../../src/normalize/dom-ir.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,KAAK,EAA6C,MAAM,YAAY,CAAC;AACnF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAMtD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,UAAU,GAAG,KAAK,CAsCjD"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOMIR builder from DOM raw data.
|
|
3
|
+
*
|
|
4
|
+
* Converts CDP DOM snapshot data into a normalized DOMIR structure with
|
|
5
|
+
* stats, tag distribution, and layout shift candidate analysis.
|
|
6
|
+
*/
|
|
7
|
+
import { DOMIRSchema } from './types.js';
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Main builder
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/**
|
|
12
|
+
* Build a complete DOMIR from DOM raw data.
|
|
13
|
+
*
|
|
14
|
+
* @param dom - Raw DOM data from the DOMCollector
|
|
15
|
+
* @returns A fully-constructed DOMIR
|
|
16
|
+
*/
|
|
17
|
+
export function buildDOMIR(dom) {
|
|
18
|
+
// 1. Stats: pass through totalNodes, elementCount, maxDepth
|
|
19
|
+
// maxChildren is derived from elementDistribution values (max count)
|
|
20
|
+
const elementCounts = Object.values(dom.elementDistribution);
|
|
21
|
+
const maxChildren = elementCounts.length > 0
|
|
22
|
+
? Math.max(...elementCounts)
|
|
23
|
+
: 0;
|
|
24
|
+
const stats = {
|
|
25
|
+
totalNodes: dom.stats.totalNodes,
|
|
26
|
+
elementCount: dom.stats.elementCount,
|
|
27
|
+
maxDepth: dom.stats.maxDepth,
|
|
28
|
+
maxChildren,
|
|
29
|
+
};
|
|
30
|
+
// 2. Tag distribution: convert Record<string, number> → TagCount[]
|
|
31
|
+
const tagDistribution = Object.entries(dom.elementDistribution).map(([tag, count]) => ({ tag, count }));
|
|
32
|
+
// 3. Layout shift candidates
|
|
33
|
+
const highComplexitySubtrees = maxChildren >= 50
|
|
34
|
+
? elementCounts.filter((count) => count > maxChildren / 2).length
|
|
35
|
+
: 0;
|
|
36
|
+
const deepNesting = dom.stats.maxDepth >= 15 ? 1 : 0;
|
|
37
|
+
const layoutShiftCandidates = {
|
|
38
|
+
highComplexitySubtrees,
|
|
39
|
+
deepNesting,
|
|
40
|
+
};
|
|
41
|
+
const result = {
|
|
42
|
+
stats,
|
|
43
|
+
tagDistribution,
|
|
44
|
+
layoutShiftCandidates,
|
|
45
|
+
};
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=dom-ir.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom-ir.js","sourceRoot":"","sources":["../../src/normalize/dom-ir.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAIzC,8EAA8E;AAC9E,eAAe;AACf,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,GAAe;IACxC,4DAA4D;IAC5D,wEAAwE;IACxE,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAC7D,MAAM,WAAW,GAAG,aAAa,CAAC,MAAM,GAAG,CAAC;QAC1C,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC;QAC5B,CAAC,CAAC,CAAC,CAAC;IAEN,MAAM,KAAK,GAAa;QACtB,UAAU,EAAE,GAAG,CAAC,KAAK,CAAC,UAAU;QAChC,YAAY,EAAE,GAAG,CAAC,KAAK,CAAC,YAAY;QACpC,QAAQ,EAAE,GAAG,CAAC,KAAK,CAAC,QAAQ;QAC5B,WAAW;KACZ,CAAC;IAEF,mEAAmE;IACnE,MAAM,eAAe,GAAe,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC,GAAG,CAC7E,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CACnC,CAAC;IAEF,6BAA6B;IAC7B,MAAM,sBAAsB,GAAG,WAAW,IAAI,EAAE;QAC9C,CAAC,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,GAAG,WAAW,GAAG,CAAC,CAAC,CAAC,MAAM;QACjE,CAAC,CAAC,CAAC,CAAC;IACN,MAAM,WAAW,GAAG,GAAG,CAAC,KAAK,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAErD,MAAM,qBAAqB,GAA0B;QACnD,sBAAsB;QACtB,WAAW;KACZ,CAAC;IAEF,MAAM,MAAM,GAAU;QACpB,KAAK;QACL,eAAe;QACf,qBAAqB;KACtB,CAAC;IAEF,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Entry point for the normalization pipeline. Takes a RawDataBundle from
|
|
5
|
+
* collection and returns a validated IRBundle with all timestamps in ms
|
|
6
|
+
* relative to navigationStart.
|
|
7
|
+
*/
|
|
8
|
+
import type { RawDataBundle } from '../collect/types.js';
|
|
9
|
+
import type { IRBundle } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Normalize raw collected data into a validated Intermediate Representation
|
|
12
|
+
* bundle.
|
|
13
|
+
*
|
|
14
|
+
* Builds all domain IRs (performance, network, runtime, DOM, lighthouse)
|
|
15
|
+
* from their respective raw data sources and assembles them into a
|
|
16
|
+
* validated IRBundle.
|
|
17
|
+
*
|
|
18
|
+
* @param raw - Raw data bundle from a single collection run
|
|
19
|
+
* @returns Validated IRBundle
|
|
20
|
+
* @throws {Error} If the assembled bundle fails Zod validation
|
|
21
|
+
*/
|
|
22
|
+
export declare function normalize(raw: RawDataBundle): IRBundle;
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/normalize/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,aAAa,EAA8C,MAAM,qBAAqB,CAAC;AACrG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAU3C;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,aAAa,GAAG,QAAQ,CA8GtD"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Entry point for the normalization pipeline. Takes a RawDataBundle from
|
|
5
|
+
* collection and returns a validated IRBundle with all timestamps in ms
|
|
6
|
+
* relative to navigationStart.
|
|
7
|
+
*/
|
|
8
|
+
import { IRBundleSchema } from './types.js';
|
|
9
|
+
import { resolveClockAnchor } from './clock.js';
|
|
10
|
+
import { buildPerformanceIR } from './trace-ir.js';
|
|
11
|
+
import { buildNetworkIR } from './network-ir.js';
|
|
12
|
+
import { buildRuntimeIR } from './runtime-ir.js';
|
|
13
|
+
import { buildDOMIR } from './dom-ir.js';
|
|
14
|
+
import { buildLighthouseIR } from './lighthouse-ir.js';
|
|
15
|
+
/**
|
|
16
|
+
* Normalize raw collected data into a validated Intermediate Representation
|
|
17
|
+
* bundle.
|
|
18
|
+
*
|
|
19
|
+
* Builds all domain IRs (performance, network, runtime, DOM, lighthouse)
|
|
20
|
+
* from their respective raw data sources and assembles them into a
|
|
21
|
+
* validated IRBundle.
|
|
22
|
+
*
|
|
23
|
+
* @param raw - Raw data bundle from a single collection run
|
|
24
|
+
* @returns Validated IRBundle
|
|
25
|
+
* @throws {Error} If the assembled bundle fails Zod validation
|
|
26
|
+
*/
|
|
27
|
+
export function normalize(raw) {
|
|
28
|
+
// 1. Resolve the clock anchor (navigationStart time)
|
|
29
|
+
const anchor = resolveClockAnchor(raw);
|
|
30
|
+
// 2. Build PerformanceIR from trace + performance data
|
|
31
|
+
const traceData = raw.trace;
|
|
32
|
+
const perfData = raw.performance;
|
|
33
|
+
const performance = (traceData && perfData)
|
|
34
|
+
? buildPerformanceIR(raw.trace, raw.performance, anchor)
|
|
35
|
+
: {
|
|
36
|
+
navigation: {
|
|
37
|
+
url: '',
|
|
38
|
+
navigationStart: 0,
|
|
39
|
+
domContentLoaded: 0,
|
|
40
|
+
domContentLoadedEventEnd: 0,
|
|
41
|
+
loadEventStart: 0,
|
|
42
|
+
loadEventEnd: 0,
|
|
43
|
+
domInteractive: 0,
|
|
44
|
+
},
|
|
45
|
+
coreWebVitals: {},
|
|
46
|
+
traceSummary: {
|
|
47
|
+
totalDuration: 0,
|
|
48
|
+
eventCount: 0,
|
|
49
|
+
categories: {},
|
|
50
|
+
threadActivity: { totalMs: 0, byCategory: {} },
|
|
51
|
+
},
|
|
52
|
+
mainThreadBusyness: 0,
|
|
53
|
+
};
|
|
54
|
+
// 3. Build domain IRs
|
|
55
|
+
const network = raw.network
|
|
56
|
+
? buildNetworkIR(raw.network, anchor)
|
|
57
|
+
: {
|
|
58
|
+
requests: [],
|
|
59
|
+
summary: {
|
|
60
|
+
totalRequests: 0,
|
|
61
|
+
totalBytes: 0,
|
|
62
|
+
byType: {},
|
|
63
|
+
byPriority: {},
|
|
64
|
+
criticalPath: { tree: { url: '' }, depth: 0, urlsOnLongestPath: [] },
|
|
65
|
+
longestChain: { url: '', length: 0 },
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
const runtimeRaw = raw.runtime;
|
|
69
|
+
const consoleRaw = raw.consoleEntries;
|
|
70
|
+
const runtime = runtimeRaw
|
|
71
|
+
? buildRuntimeIR(runtimeRaw, consoleRaw ?? { entries: [], counts: { log: 0, warn: 0, error: 0, info: 0, debug: 0, other: 0 }, warnings: [] })
|
|
72
|
+
: { executionContexts: [] };
|
|
73
|
+
const domRaw = raw.dom;
|
|
74
|
+
const dom = domRaw
|
|
75
|
+
? buildDOMIR(domRaw)
|
|
76
|
+
: {
|
|
77
|
+
stats: { totalNodes: 0, elementCount: 0, maxDepth: 0, maxChildren: 0 },
|
|
78
|
+
tagDistribution: [],
|
|
79
|
+
layoutShiftCandidates: { highComplexitySubtrees: 0, deepNesting: 0 },
|
|
80
|
+
};
|
|
81
|
+
const lighthouse = raw.lighthouse
|
|
82
|
+
? buildLighthouseIR(raw.lighthouse)
|
|
83
|
+
: {
|
|
84
|
+
categories: {},
|
|
85
|
+
failedAudits: [],
|
|
86
|
+
scores: {},
|
|
87
|
+
};
|
|
88
|
+
// 4. Extract URL from the first network request (most reliable source)
|
|
89
|
+
const networkRaw = raw.network;
|
|
90
|
+
const firstRequestUrl = networkRaw?.requests?.[0]?.url
|
|
91
|
+
?? network.requests[0]?.url
|
|
92
|
+
?? performance.navigation.url
|
|
93
|
+
?? '';
|
|
94
|
+
// 5. Assemble IRMeta
|
|
95
|
+
const perfMetrics = perfData?.metrics;
|
|
96
|
+
const navStartMetric = perfMetrics?.find((m) => m.name === 'NavigationStart');
|
|
97
|
+
const meta = {
|
|
98
|
+
url: firstRequestUrl,
|
|
99
|
+
fetchedAt: new Date().toISOString(),
|
|
100
|
+
navigationStart: navStartMetric?.value ?? 0,
|
|
101
|
+
irVersion: '1.0.0',
|
|
102
|
+
};
|
|
103
|
+
// 6. Build and validate IRBundle
|
|
104
|
+
const bundle = {
|
|
105
|
+
meta,
|
|
106
|
+
performance,
|
|
107
|
+
network,
|
|
108
|
+
runtime,
|
|
109
|
+
dom,
|
|
110
|
+
lighthouse,
|
|
111
|
+
};
|
|
112
|
+
const parseResult = IRBundleSchema.safeParse(bundle);
|
|
113
|
+
if (!parseResult.success) {
|
|
114
|
+
throw new Error(`Error: IR validation failed: ${parseResult.error.message}`);
|
|
115
|
+
}
|
|
116
|
+
return parseResult.data;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/normalize/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lighthouse Insights builder from Lighthouse v13+ insight audits.
|
|
3
|
+
*
|
|
4
|
+
* Extracts structured data from 3 specific Lighthouse insight audits:
|
|
5
|
+
* - lcp-breakdown-insight
|
|
6
|
+
* - render-blocking-insight
|
|
7
|
+
* - network-dependency-tree-insight
|
|
8
|
+
*
|
|
9
|
+
* Each insight audit is parsed independently so partial data does not
|
|
10
|
+
* prevent the rest from being extracted.
|
|
11
|
+
*/
|
|
12
|
+
import type { LighthouseInsights } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Build a structured LighthouseInsights object from raw Lighthouse data.
|
|
15
|
+
*
|
|
16
|
+
* Accepts raw Lighthouse data in either form:
|
|
17
|
+
* - `{ lhr: { audits: {...} } }` — from LighthouseRawData
|
|
18
|
+
* - `{ audits: {...} }` — from a plain Lighthouse Report
|
|
19
|
+
*
|
|
20
|
+
* Each insight audit is parsed independently. Missing or invalid audits
|
|
21
|
+
* simply result in that field being omitted from the result.
|
|
22
|
+
*
|
|
23
|
+
* @param rawLighthouse - Raw Lighthouse data (unknown shape, validated at parse time)
|
|
24
|
+
* @returns A partially-populated LighthouseInsights object
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildLighthouseInsights(rawLighthouse: unknown): LighthouseInsights;
|
|
27
|
+
//# sourceMappingURL=lighthouse-insights.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lighthouse-insights.d.ts","sourceRoot":"","sources":["../../src/normalize/lighthouse-insights.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAMnB,MAAM,YAAY,CAAC;AA+RpB;;;;;;;;;;;;GAYG;AACH,wBAAgB,uBAAuB,CACrC,aAAa,EAAE,OAAO,GACrB,kBAAkB,CA2CpB"}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lighthouse Insights builder from Lighthouse v13+ insight audits.
|
|
3
|
+
*
|
|
4
|
+
* Extracts structured data from 3 specific Lighthouse insight audits:
|
|
5
|
+
* - lcp-breakdown-insight
|
|
6
|
+
* - render-blocking-insight
|
|
7
|
+
* - network-dependency-tree-insight
|
|
8
|
+
*
|
|
9
|
+
* Each insight audit is parsed independently so partial data does not
|
|
10
|
+
* prevent the rest from being extracted.
|
|
11
|
+
*/
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Internal helpers for safe access
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
/** Assert value is a plain object (record-like). */
|
|
16
|
+
function isRecord(value) {
|
|
17
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
18
|
+
}
|
|
19
|
+
/** Safely coerce a value to a string. */
|
|
20
|
+
function safeString(value) {
|
|
21
|
+
return typeof value === 'string' ? value : undefined;
|
|
22
|
+
}
|
|
23
|
+
/** Safely coerce a value to a number. */
|
|
24
|
+
function safeNumber(value) {
|
|
25
|
+
return typeof value === 'number' ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
/** Safely coerce a value to an array, filtering out non-record entries. */
|
|
28
|
+
function safeRecordArray(value) {
|
|
29
|
+
if (!Array.isArray(value))
|
|
30
|
+
return [];
|
|
31
|
+
return value.filter(isRecord);
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Insight audit parsers
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
/**
|
|
37
|
+
* Parse the lcp-breakdown-insight audit.
|
|
38
|
+
*
|
|
39
|
+
* Lighthouse v13+ structures this as a list containing:
|
|
40
|
+
* - a table item with subpart rows (timeToFirstByte, elementRenderDelay,
|
|
41
|
+
* resourceLoadDelay, resourceLoadTime)
|
|
42
|
+
* - a node item with LCP element metadata (selector, snippet, nodeLabel)
|
|
43
|
+
*/
|
|
44
|
+
function parseLCPBreakdown(audit) {
|
|
45
|
+
const details = audit.details;
|
|
46
|
+
if (!isRecord(details))
|
|
47
|
+
return undefined;
|
|
48
|
+
const items = safeRecordArray(details.items);
|
|
49
|
+
if (items.length === 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
let timeToFirstByte = 0;
|
|
52
|
+
let resourceLoadDelay;
|
|
53
|
+
let resourceLoadTime;
|
|
54
|
+
let elementRenderDelay = 0;
|
|
55
|
+
let lcpElementSelector;
|
|
56
|
+
let lcpElementSnippet;
|
|
57
|
+
let lcpElementNodeLabel;
|
|
58
|
+
for (const item of items) {
|
|
59
|
+
const itemType = safeString(item.type);
|
|
60
|
+
if (itemType === 'table') {
|
|
61
|
+
// Extract subpart durations from the table items
|
|
62
|
+
const subItems = safeRecordArray(item.items);
|
|
63
|
+
for (const sub of subItems) {
|
|
64
|
+
const subpart = safeString(sub.subpart);
|
|
65
|
+
const duration = safeNumber(sub.duration) ?? 0;
|
|
66
|
+
switch (subpart) {
|
|
67
|
+
case 'timeToFirstByte':
|
|
68
|
+
timeToFirstByte = duration;
|
|
69
|
+
break;
|
|
70
|
+
case 'elementRenderDelay':
|
|
71
|
+
elementRenderDelay = duration;
|
|
72
|
+
break;
|
|
73
|
+
case 'resourceLoadDelay':
|
|
74
|
+
resourceLoadDelay = duration;
|
|
75
|
+
break;
|
|
76
|
+
case 'resourceLoadTime':
|
|
77
|
+
resourceLoadTime = duration;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (itemType === 'node') {
|
|
83
|
+
lcpElementSelector = safeString(item.selector);
|
|
84
|
+
lcpElementSnippet = safeString(item.snippet);
|
|
85
|
+
lcpElementNodeLabel = safeString(item.nodeLabel);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Determine source
|
|
89
|
+
const scoreDisplayMode = safeString(audit.scoreDisplayMode);
|
|
90
|
+
let source = 'lighthouse';
|
|
91
|
+
if (scoreDisplayMode === 'informative') {
|
|
92
|
+
// Insight audits with trace data combined are 'mixed'
|
|
93
|
+
source = 'mixed';
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
timeToFirstByte,
|
|
97
|
+
elementRenderDelay,
|
|
98
|
+
resourceLoadDelay: resourceLoadDelay !== undefined ? resourceLoadDelay : undefined,
|
|
99
|
+
resourceLoadTime: resourceLoadTime !== undefined ? resourceLoadTime : undefined,
|
|
100
|
+
lcpElementSelector,
|
|
101
|
+
lcpElementSnippet,
|
|
102
|
+
lcpElementNodeLabel,
|
|
103
|
+
source,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Parse the render-blocking-insight audit.
|
|
108
|
+
*
|
|
109
|
+
* Lighthouse v13+ structures this as:
|
|
110
|
+
* - metricSavings: { FCP: number, LCP: number }
|
|
111
|
+
* - details.items: array of render-blocking resources
|
|
112
|
+
*/
|
|
113
|
+
function parseRenderBlocking(audit) {
|
|
114
|
+
const details = audit.details;
|
|
115
|
+
if (!isRecord(details))
|
|
116
|
+
return undefined;
|
|
117
|
+
// Extract metric savings from the audit level
|
|
118
|
+
const metricSavings = isRecord(audit.metricSavings) ? audit.metricSavings : undefined;
|
|
119
|
+
const fcpSavingsMs = safeNumber(metricSavings?.['FCP']);
|
|
120
|
+
const lcpSavingsMs = safeNumber(metricSavings?.['LCP']);
|
|
121
|
+
// Extract resource list from details.items
|
|
122
|
+
const items = safeRecordArray(details.items);
|
|
123
|
+
const resources = [];
|
|
124
|
+
for (const item of items) {
|
|
125
|
+
const url = safeString(item.url);
|
|
126
|
+
if (!url)
|
|
127
|
+
continue;
|
|
128
|
+
resources.push({
|
|
129
|
+
url,
|
|
130
|
+
totalBytes: safeNumber(item.totalBytes),
|
|
131
|
+
wastedMs: safeNumber(item.wastedMs),
|
|
132
|
+
resourceType: safeString(item.resourceType),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
fcpSavingsMs: fcpSavingsMs,
|
|
137
|
+
lcpSavingsMs: lcpSavingsMs,
|
|
138
|
+
resources,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Recursively walk a network chain subtree to collect all leaf URLs
|
|
143
|
+
* along the longest path (by navStartToEndTime).
|
|
144
|
+
*/
|
|
145
|
+
function walkLongestChain(node, path) {
|
|
146
|
+
let bestDuration = node.navStartToEndTime;
|
|
147
|
+
let bestUrls = [...path, node.url];
|
|
148
|
+
if (node.children) {
|
|
149
|
+
for (const childKey of Object.keys(node.children)) {
|
|
150
|
+
const child = node.children[childKey];
|
|
151
|
+
const result = walkLongestChain(child, [...path, node.url]);
|
|
152
|
+
const totalDuration = node.navStartToEndTime + result.duration;
|
|
153
|
+
if (totalDuration > bestDuration) {
|
|
154
|
+
bestDuration = totalDuration;
|
|
155
|
+
bestUrls = result.urls;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { duration: bestDuration, urls: bestUrls };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Convert a raw chain record (unknown-shape) into a proper NetworkChainNode
|
|
163
|
+
* by recursively walking and casting each entry.
|
|
164
|
+
*/
|
|
165
|
+
function parseChainNode(raw) {
|
|
166
|
+
if (!isRecord(raw))
|
|
167
|
+
return undefined;
|
|
168
|
+
const url = safeString(raw.url);
|
|
169
|
+
if (!url)
|
|
170
|
+
return undefined;
|
|
171
|
+
const node = {
|
|
172
|
+
url,
|
|
173
|
+
navStartToEndTime: safeNumber(raw.navStartToEndTime) ?? 0,
|
|
174
|
+
transferSize: safeNumber(raw.transferSize),
|
|
175
|
+
isLongest: raw.isLongest === true,
|
|
176
|
+
};
|
|
177
|
+
if (raw.children && isRecord(raw.children)) {
|
|
178
|
+
const children = {};
|
|
179
|
+
for (const [key, childRaw] of Object.entries(raw.children)) {
|
|
180
|
+
const child = parseChainNode(childRaw);
|
|
181
|
+
if (child) {
|
|
182
|
+
children[key] = child;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (Object.keys(children).length > 0) {
|
|
186
|
+
node.children = children;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return node;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Parse the network-dependency-tree-insight audit.
|
|
193
|
+
*
|
|
194
|
+
* Lighthouse v13+ structures this as a list containing:
|
|
195
|
+
* - a network-tree item with chains and longestChain metadata
|
|
196
|
+
* - a list-section item with preconnect candidates
|
|
197
|
+
*/
|
|
198
|
+
function parseNetworkDependencyTree(audit) {
|
|
199
|
+
const details = audit.details;
|
|
200
|
+
if (!isRecord(details))
|
|
201
|
+
return undefined;
|
|
202
|
+
const items = safeRecordArray(details.items);
|
|
203
|
+
if (items.length === 0)
|
|
204
|
+
return undefined;
|
|
205
|
+
let chains;
|
|
206
|
+
let longestChainDuration;
|
|
207
|
+
const longestChainUrls = [];
|
|
208
|
+
const preconnectCandidates = [];
|
|
209
|
+
for (const item of items) {
|
|
210
|
+
const itemType = safeString(item.type);
|
|
211
|
+
if (itemType === 'network-tree') {
|
|
212
|
+
// Parse chains
|
|
213
|
+
const rawChains = isRecord(item.chains) ? item.chains : undefined;
|
|
214
|
+
if (rawChains) {
|
|
215
|
+
const parsed = {};
|
|
216
|
+
for (const [key, rawNode] of Object.entries(rawChains)) {
|
|
217
|
+
const node = parseChainNode(rawNode);
|
|
218
|
+
if (node) {
|
|
219
|
+
parsed[key] = node;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (Object.keys(parsed).length > 0) {
|
|
223
|
+
chains = parsed;
|
|
224
|
+
// Walk the longest chain to get URLs
|
|
225
|
+
for (const rootNode of Object.values(parsed)) {
|
|
226
|
+
const result = walkLongestChain(rootNode, []);
|
|
227
|
+
if (result.duration > (longestChainDuration ?? 0)) {
|
|
228
|
+
longestChainDuration = result.duration;
|
|
229
|
+
}
|
|
230
|
+
longestChainUrls.push(...result.urls);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Extract longestChain duration from the item-level metadata
|
|
235
|
+
const longestChain = isRecord(item.longestChain) ? item.longestChain : undefined;
|
|
236
|
+
if (longestChain) {
|
|
237
|
+
const duration = safeNumber(longestChain.duration);
|
|
238
|
+
if (duration !== undefined) {
|
|
239
|
+
longestChainDuration = duration;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (itemType === 'list-section') {
|
|
244
|
+
const title = safeString(item.title);
|
|
245
|
+
if (title === 'Preconnect candidates' || title?.toLowerCase().includes('preconnect')) {
|
|
246
|
+
const preconnectItems = safeRecordArray(item.items);
|
|
247
|
+
for (const pi of preconnectItems) {
|
|
248
|
+
const url = safeString(pi.url);
|
|
249
|
+
if (url) {
|
|
250
|
+
preconnectCandidates.push(url);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
chains,
|
|
258
|
+
longestChainDuration: longestChainDuration,
|
|
259
|
+
longestChainUrls: longestChainUrls.length > 0 ? longestChainUrls : undefined,
|
|
260
|
+
preconnectCandidates: preconnectCandidates.length > 0 ? preconnectCandidates : undefined,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// Public API
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
/**
|
|
267
|
+
* Build a structured LighthouseInsights object from raw Lighthouse data.
|
|
268
|
+
*
|
|
269
|
+
* Accepts raw Lighthouse data in either form:
|
|
270
|
+
* - `{ lhr: { audits: {...} } }` — from LighthouseRawData
|
|
271
|
+
* - `{ audits: {...} }` — from a plain Lighthouse Report
|
|
272
|
+
*
|
|
273
|
+
* Each insight audit is parsed independently. Missing or invalid audits
|
|
274
|
+
* simply result in that field being omitted from the result.
|
|
275
|
+
*
|
|
276
|
+
* @param rawLighthouse - Raw Lighthouse data (unknown shape, validated at parse time)
|
|
277
|
+
* @returns A partially-populated LighthouseInsights object
|
|
278
|
+
*/
|
|
279
|
+
export function buildLighthouseInsights(rawLighthouse) {
|
|
280
|
+
if (!isRecord(rawLighthouse))
|
|
281
|
+
return {};
|
|
282
|
+
// Handle both { lhr: { audits } } and { audits } formats
|
|
283
|
+
const root = isRecord(rawLighthouse.lhr) ? rawLighthouse.lhr : rawLighthouse;
|
|
284
|
+
const audits = isRecord(root.audits) ? root.audits : {};
|
|
285
|
+
const insights = {};
|
|
286
|
+
// 1. Parse lcp-breakdown-insight
|
|
287
|
+
const lcpAudit = isRecord(audits['lcp-breakdown-insight'])
|
|
288
|
+
? audits['lcp-breakdown-insight']
|
|
289
|
+
: undefined;
|
|
290
|
+
if (lcpAudit) {
|
|
291
|
+
const parsed = parseLCPBreakdown(lcpAudit);
|
|
292
|
+
if (parsed) {
|
|
293
|
+
insights.lcpBreakdown = parsed;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// 2. Parse render-blocking-insight
|
|
297
|
+
const rbAudit = isRecord(audits['render-blocking-insight'])
|
|
298
|
+
? audits['render-blocking-insight']
|
|
299
|
+
: undefined;
|
|
300
|
+
if (rbAudit) {
|
|
301
|
+
const parsed = parseRenderBlocking(rbAudit);
|
|
302
|
+
if (parsed) {
|
|
303
|
+
insights.renderBlocking = parsed;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// 3. Parse network-dependency-tree-insight
|
|
307
|
+
const netAudit = isRecord(audits['network-dependency-tree-insight'])
|
|
308
|
+
? audits['network-dependency-tree-insight']
|
|
309
|
+
: undefined;
|
|
310
|
+
if (netAudit) {
|
|
311
|
+
const parsed = parseNetworkDependencyTree(netAudit);
|
|
312
|
+
if (parsed) {
|
|
313
|
+
insights.networkDependencyTree = parsed;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return insights;
|
|
317
|
+
}
|
|
318
|
+
//# sourceMappingURL=lighthouse-insights.js.map
|