trace-engine-cli 0.0.0-stage → 0.0.1
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 +202 -0
- package/README.md +360 -2
- package/dist/bin/trace-cli.d.ts +2 -0
- package/dist/bin/trace-cli.js +19 -0
- package/dist/src/cli.d.ts +1 -0
- package/dist/src/cli.js +477 -0
- package/dist/src/engine/adapter.d.ts +13 -0
- package/dist/src/engine/adapter.js +51 -0
- package/dist/src/engine/extractors/diff.d.ts +12 -0
- package/dist/src/engine/extractors/diff.js +100 -0
- package/dist/src/engine/extractors/events.d.ts +27 -0
- package/dist/src/engine/extractors/events.js +84 -0
- package/dist/src/engine/extractors/find.d.ts +12 -0
- package/dist/src/engine/extractors/find.js +100 -0
- package/dist/src/engine/extractors/insights.d.ts +15 -0
- package/dist/src/engine/extractors/insights.js +153 -0
- package/dist/src/engine/extractors/interactions.d.ts +7 -0
- package/dist/src/engine/extractors/interactions.js +55 -0
- package/dist/src/engine/extractors/navigations.d.ts +10 -0
- package/dist/src/engine/extractors/navigations.js +82 -0
- package/dist/src/engine/extractors/network.d.ts +10 -0
- package/dist/src/engine/extractors/network.js +105 -0
- package/dist/src/engine/extractors/overview.d.ts +11 -0
- package/dist/src/engine/extractors/overview.js +232 -0
- package/dist/src/engine/extractors/profile.d.ts +12 -0
- package/dist/src/engine/extractors/profile.js +83 -0
- package/dist/src/engine/extractors/scan.d.ts +37 -0
- package/dist/src/engine/extractors/scan.js +218 -0
- package/dist/src/engine/extractors/scheduling.d.ts +10 -0
- package/dist/src/engine/extractors/scheduling.js +89 -0
- package/dist/src/engine/extractors/shifts.d.ts +11 -0
- package/dist/src/engine/extractors/shifts.js +118 -0
- package/dist/src/engine/extractors/tasks.d.ts +13 -0
- package/dist/src/engine/extractors/tasks.js +156 -0
- package/dist/src/engine/helpers.d.ts +157 -0
- package/dist/src/engine/helpers.js +95 -0
- package/dist/src/engine/loader.d.ts +34 -0
- package/dist/src/engine/loader.js +142 -0
- package/dist/src/engine/types.d.ts +452 -0
- package/dist/src/engine/types.js +14 -0
- package/dist/src/formatters/index.d.ts +6 -0
- package/dist/src/formatters/index.js +66 -0
- package/dist/src/formatters/markdown.d.ts +15 -0
- package/dist/src/formatters/markdown.js +474 -0
- package/dist/src/formatters/toon.d.ts +1 -0
- package/dist/src/formatters/toon.js +17 -0
- package/dist/src/utils/filter.d.ts +47 -0
- package/dist/src/utils/filter.js +136 -0
- package/dist/src/utils/units.d.ts +24 -0
- package/dist/src/utils/units.js +57 -0
- package/package.json +45 -4
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// Copyright 2026 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
import { isEventInBounds } from '../../utils/filter.js';
|
|
15
|
+
import { microToMilli, round } from '../../utils/units.js';
|
|
16
|
+
import { getPropertyByPath } from '../helpers.js';
|
|
17
|
+
/**
|
|
18
|
+
* Filters and extracts raw trace events matching query criteria.
|
|
19
|
+
*
|
|
20
|
+
* Normalizes event timestamps and durations to milliseconds.
|
|
21
|
+
* Returns the total number of matched events and a capped array of event items.
|
|
22
|
+
*/
|
|
23
|
+
export function extractEvents(loadedTrace, options) {
|
|
24
|
+
const { rawEvents } = loadedTrace;
|
|
25
|
+
const names = options?.names && options.names.length > 0 ? new Set(options.names) : null;
|
|
26
|
+
const category = options?.category;
|
|
27
|
+
const phase = options?.phase;
|
|
28
|
+
const timeRange = options?.timeRange;
|
|
29
|
+
const fields = options?.fields && options.fields.length > 0 ? options.fields : null;
|
|
30
|
+
const limit = options?.limit ?? 50;
|
|
31
|
+
const matchedEvents = [];
|
|
32
|
+
let totalMatched = 0;
|
|
33
|
+
for (const raw of rawEvents) {
|
|
34
|
+
if (!raw || typeof raw !== 'object') {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const event = raw;
|
|
38
|
+
const eventName = event.name ?? '';
|
|
39
|
+
if (names && !names.has(eventName)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const eventCat = event.cat;
|
|
43
|
+
if (category && eventCat !== category) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const eventPh = event.ph;
|
|
47
|
+
if (phase && eventPh !== phase) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const rawTs = event.ts ?? 0;
|
|
51
|
+
const rawDur = event.dur ?? 0;
|
|
52
|
+
const tsMs = microToMilli(rawTs);
|
|
53
|
+
const durMs = microToMilli(rawDur);
|
|
54
|
+
if (timeRange && !isEventInBounds(tsMs, durMs, timeRange)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
totalMatched++;
|
|
58
|
+
if (matchedEvents.length < limit) {
|
|
59
|
+
const item = {
|
|
60
|
+
name: eventName,
|
|
61
|
+
tsMs: round(tsMs),
|
|
62
|
+
durMs: event.dur !== undefined ? round(durMs) : undefined,
|
|
63
|
+
cat: eventCat,
|
|
64
|
+
ph: eventPh,
|
|
65
|
+
pid: event.pid,
|
|
66
|
+
tid: event.tid,
|
|
67
|
+
args: event.args,
|
|
68
|
+
};
|
|
69
|
+
if (fields) {
|
|
70
|
+
const selectedFields = {};
|
|
71
|
+
for (const field of fields) {
|
|
72
|
+
selectedFields[field] = getPropertyByPath(event, field);
|
|
73
|
+
}
|
|
74
|
+
item.selectedFields = selectedFields;
|
|
75
|
+
}
|
|
76
|
+
matchedEvents.push(item);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
totalMatched,
|
|
81
|
+
returned: matchedEvents.length,
|
|
82
|
+
events: matchedEvents,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type TimeRange } from '../../utils/filter.js';
|
|
2
|
+
import type { LoadedTrace } from '../loader.js';
|
|
3
|
+
import type { FindResult } from '../types.js';
|
|
4
|
+
export interface ExtractFindOptions {
|
|
5
|
+
functionName: string;
|
|
6
|
+
timeRange?: TimeRange;
|
|
7
|
+
limit?: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Searches for all executions of a specific function name or pattern.
|
|
11
|
+
*/
|
|
12
|
+
export declare function extractFind(loadedTrace: LoadedTrace, options: ExtractFindOptions): FindResult;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Copyright 2026 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
import { isEventInBounds } from '../../utils/filter.js';
|
|
15
|
+
import { microToMilli, round } from '../../utils/units.js';
|
|
16
|
+
import { getMainRendererThread } from '../helpers.js';
|
|
17
|
+
/**
|
|
18
|
+
* Searches for all executions of a specific function name or pattern.
|
|
19
|
+
*/
|
|
20
|
+
export function extractFind(loadedTrace, options) {
|
|
21
|
+
const { engine, parsedTrace } = loadedTrace;
|
|
22
|
+
const functionQuery = options.functionName.trim();
|
|
23
|
+
const timeRange = options.timeRange;
|
|
24
|
+
const limit = options.limit ?? 50;
|
|
25
|
+
const result = {
|
|
26
|
+
functionQuery,
|
|
27
|
+
timeRange,
|
|
28
|
+
totalCalls: 0,
|
|
29
|
+
totalDurationMs: 0,
|
|
30
|
+
avgDurationMs: 0,
|
|
31
|
+
calls: [],
|
|
32
|
+
};
|
|
33
|
+
const mainThread = getMainRendererThread(parsedTrace.data);
|
|
34
|
+
if (!mainThread || !functionQuery) {
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
const queryLower = functionQuery.toLowerCase();
|
|
38
|
+
const matchedCalls = [];
|
|
39
|
+
for (const entry of mainThread.entries) {
|
|
40
|
+
const startMs = microToMilli(entry.ts);
|
|
41
|
+
const durMs = microToMilli(entry.dur ?? 0);
|
|
42
|
+
if (!isEventInBounds(startMs, durMs, timeRange)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
// biome-ignore lint/suspicious/noExplicitAny: stack frame extraction accepts generic event
|
|
46
|
+
const frame = engine.Extras.TraceTree.eventStackFrame(entry);
|
|
47
|
+
const fnName = frame?.functionName || entry.name || '';
|
|
48
|
+
if (fnName.toLowerCase().includes(queryLower)) {
|
|
49
|
+
matchedCalls.push({
|
|
50
|
+
timestampMs: round(startMs),
|
|
51
|
+
durationMs: round(durMs),
|
|
52
|
+
url: frame?.url,
|
|
53
|
+
lineNumber: frame?.lineNumber,
|
|
54
|
+
columnNumber: frame?.columnNumber,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
result.totalCalls = matchedCalls.length;
|
|
59
|
+
if (matchedCalls.length > 0) {
|
|
60
|
+
let totalDur = 0;
|
|
61
|
+
for (const c of matchedCalls) {
|
|
62
|
+
totalDur += c.durationMs;
|
|
63
|
+
}
|
|
64
|
+
result.totalDurationMs = round(totalDur);
|
|
65
|
+
result.avgDurationMs = round(totalDur / matchedCalls.length);
|
|
66
|
+
result.firstCallMs = matchedCalls[0].timestampMs;
|
|
67
|
+
result.lastCallMs = matchedCalls[matchedCalls.length - 1].timestampMs;
|
|
68
|
+
result.calls = matchedCalls.slice(0, limit);
|
|
69
|
+
// Build interval distribution if timeRange or trace bounds exist
|
|
70
|
+
const traceBounds = parsedTrace.data.Meta.traceBounds;
|
|
71
|
+
const windowStart = timeRange ? timeRange.startMs : microToMilli(traceBounds.min);
|
|
72
|
+
const windowEnd = timeRange ? timeRange.endMs : microToMilli(traceBounds.max);
|
|
73
|
+
const totalSpan = windowEnd - windowStart;
|
|
74
|
+
if (totalSpan > 0) {
|
|
75
|
+
const numIntervals = Math.min(5, Math.max(2, Math.floor(totalSpan / 100)));
|
|
76
|
+
const intervalSize = totalSpan / numIntervals;
|
|
77
|
+
const intervals = [];
|
|
78
|
+
for (let i = 0; i < numIntervals; i++) {
|
|
79
|
+
const iStart = round(windowStart + i * intervalSize);
|
|
80
|
+
const iEnd = round(i === numIntervals - 1 ? windowEnd : windowStart + (i + 1) * intervalSize);
|
|
81
|
+
let callsInInterval = 0;
|
|
82
|
+
let durInInterval = 0;
|
|
83
|
+
for (const c of matchedCalls) {
|
|
84
|
+
if (c.timestampMs >= iStart && c.timestampMs < iEnd) {
|
|
85
|
+
callsInInterval++;
|
|
86
|
+
durInInterval += c.durationMs;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
intervals.push({
|
|
90
|
+
startMs: iStart,
|
|
91
|
+
endMs: iEnd,
|
|
92
|
+
calls: callsInInterval,
|
|
93
|
+
totalDurationMs: round(durInInterval),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
result.intervals = intervals;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { LoadedTrace } from '../loader.js';
|
|
2
|
+
import type { InsightsResult } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Configuration options for diagnostic insights extraction.
|
|
5
|
+
*/
|
|
6
|
+
export interface ExtractInsightsOptions {
|
|
7
|
+
/** Filter insights by diagnostic category. Network requests are handled by extractNetwork. */
|
|
8
|
+
category?: 'lcp' | 'inp' | 'cls' | 'render-blocking' | 'third-parties';
|
|
9
|
+
/** Target navigation ID or InsightSet ID. Defaults to the first InsightSet in the trace. */
|
|
10
|
+
navigationId?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Extracts diagnostic insight cards for Web Vitals (LCP, INP, CLS, render blocking, 3rd party).
|
|
14
|
+
*/
|
|
15
|
+
export declare function extractInsights(loadedTrace: LoadedTrace, options?: ExtractInsightsOptions): InsightsResult;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Copyright 2026 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
import { microToMilli, round } from '../../utils/units.js';
|
|
15
|
+
import { getPrimaryInsightSet } from '../helpers.js';
|
|
16
|
+
/**
|
|
17
|
+
* Extracts diagnostic insight cards for Web Vitals (LCP, INP, CLS, render blocking, 3rd party).
|
|
18
|
+
*/
|
|
19
|
+
export function extractInsights(loadedTrace, options) {
|
|
20
|
+
const { parsedTrace } = loadedTrace;
|
|
21
|
+
const result = {
|
|
22
|
+
navigationId: options?.navigationId,
|
|
23
|
+
};
|
|
24
|
+
const warnings = [];
|
|
25
|
+
const insightSet = getPrimaryInsightSet(parsedTrace, options?.navigationId);
|
|
26
|
+
if (options?.navigationId && !insightSet) {
|
|
27
|
+
warnings.push(`No insights found for navigation ID: ${options.navigationId}`);
|
|
28
|
+
}
|
|
29
|
+
const category = options?.category;
|
|
30
|
+
const shouldInclude = (cat) => !category || category === cat;
|
|
31
|
+
const allowGlobalFallback = !options?.navigationId;
|
|
32
|
+
// 1. LCP Breakdown
|
|
33
|
+
if (shouldInclude('lcp') && insightSet?.LCPBreakdown) {
|
|
34
|
+
const l = insightSet.LCPBreakdown;
|
|
35
|
+
const subparts = l.subparts;
|
|
36
|
+
const lcpInsight = {
|
|
37
|
+
totalLcpMs: round(l.lcpMs ?? (l.lcpTs ? microToMilli(l.lcpTs) : 0)),
|
|
38
|
+
ttfbMs: subparts?.ttfb?.range ? round(microToMilli(subparts.ttfb.range)) : 0,
|
|
39
|
+
loadDelayMs: subparts?.loadDelay?.range ? round(microToMilli(subparts.loadDelay.range)) : 0,
|
|
40
|
+
loadDurationMs: subparts?.loadDuration?.range
|
|
41
|
+
? round(microToMilli(subparts.loadDuration.range))
|
|
42
|
+
: 0,
|
|
43
|
+
renderDelayMs: subparts?.renderDelay?.range
|
|
44
|
+
? round(microToMilli(subparts.renderDelay.range))
|
|
45
|
+
: 0,
|
|
46
|
+
lcpElement: l.lcpEvent?.args?.data?.nodeName,
|
|
47
|
+
lcpUrl: l.lcpRequest?.args?.data?.url,
|
|
48
|
+
};
|
|
49
|
+
result.lcpBreakdown = lcpInsight;
|
|
50
|
+
if (lcpInsight.renderDelayMs > 1000) {
|
|
51
|
+
warnings.push(`High LCP element render delay: ${lcpInsight.renderDelayMs}ms.`);
|
|
52
|
+
}
|
|
53
|
+
if (lcpInsight.loadDelayMs > 1000) {
|
|
54
|
+
warnings.push(`High LCP resource load delay: ${lcpInsight.loadDelayMs}ms.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// 2. INP Breakdown
|
|
58
|
+
if (shouldInclude('inp') &&
|
|
59
|
+
(insightSet?.INPBreakdown || (allowGlobalFallback && parsedTrace.data.UserInteractions))) {
|
|
60
|
+
const inpModel = insightSet?.INPBreakdown;
|
|
61
|
+
const longestEvent = inpModel?.longestInteractionEvent ||
|
|
62
|
+
(allowGlobalFallback
|
|
63
|
+
? parsedTrace.data.UserInteractions?.longestInteractionEvent
|
|
64
|
+
: undefined);
|
|
65
|
+
if (longestEvent) {
|
|
66
|
+
const inpInsight = {
|
|
67
|
+
totalDurationMs: round(microToMilli(longestEvent.dur)),
|
|
68
|
+
inputDelayMs: round(microToMilli(longestEvent.inputDelay ?? 0)),
|
|
69
|
+
processingDurationMs: round(microToMilli(longestEvent.mainThreadHandling ?? longestEvent.processingDuration ?? 0)),
|
|
70
|
+
presentationDelayMs: round(microToMilli(longestEvent.presentationDelay ?? 0)),
|
|
71
|
+
eventType: longestEvent.type || longestEvent.name || 'unknown',
|
|
72
|
+
target: longestEvent.args?.data?.nodeName,
|
|
73
|
+
};
|
|
74
|
+
result.inpBreakdown = inpInsight;
|
|
75
|
+
if (inpInsight.totalDurationMs > 200) {
|
|
76
|
+
warnings.push(`Longest interaction (${inpInsight.eventType}) exceeded 200ms threshold: ${inpInsight.totalDurationMs}ms.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// 3. CLS Culprits
|
|
81
|
+
if (shouldInclude('cls')) {
|
|
82
|
+
const clsList = [];
|
|
83
|
+
const clsModel = insightSet?.CLSCulprits;
|
|
84
|
+
if (clsModel?.clusters && clsModel.clusters.length > 0) {
|
|
85
|
+
for (const cluster of clsModel.clusters) {
|
|
86
|
+
const score = cluster.clusterCumulativeScore ?? 0;
|
|
87
|
+
const tsMs = round(microToMilli(cluster.clusterWindow?.min ?? cluster.ts ?? 0));
|
|
88
|
+
const culpritNodes = [];
|
|
89
|
+
if (cluster.events) {
|
|
90
|
+
for (const ev of cluster.events) {
|
|
91
|
+
const nodes = ev.args?.data?.impacted_nodes || [];
|
|
92
|
+
for (const n of nodes) {
|
|
93
|
+
if (n.node_id !== undefined)
|
|
94
|
+
culpritNodes.push(`Node #${n.node_id}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
clsList.push({
|
|
99
|
+
score: round(score, 4),
|
|
100
|
+
timestampMs: tsMs,
|
|
101
|
+
culpritNodes,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else if (allowGlobalFallback && parsedTrace.data.LayoutShifts?.clusters) {
|
|
106
|
+
for (const cluster of parsedTrace.data.LayoutShifts.clusters) {
|
|
107
|
+
clsList.push({
|
|
108
|
+
score: round(cluster.clusterCumulativeScore ?? 0, 4),
|
|
109
|
+
timestampMs: round(microToMilli(cluster.clusterWindow?.min ?? 0)),
|
|
110
|
+
culpritNodes: [],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (clsList.length > 0) {
|
|
115
|
+
result.clsCulprits = clsList;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// 4. Render Blocking Resources
|
|
119
|
+
if (shouldInclude('render-blocking') && insightSet?.RenderBlocking) {
|
|
120
|
+
const blocking = [];
|
|
121
|
+
const reqs = insightSet.RenderBlocking.renderBlockingRequests || [];
|
|
122
|
+
for (const req of reqs) {
|
|
123
|
+
blocking.push({
|
|
124
|
+
url: req.args?.data?.url || '',
|
|
125
|
+
durationMs: round(microToMilli(req.dur ?? 0)),
|
|
126
|
+
transferSizeBytes: req.args?.data?.encodedDataLength ?? 0,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (blocking.length > 0) {
|
|
130
|
+
result.renderBlocking = blocking;
|
|
131
|
+
warnings.push(`${blocking.length} render-blocking requests found.`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// 5. Third Parties
|
|
135
|
+
if (shouldInclude('third-parties') && insightSet?.ThirdParties) {
|
|
136
|
+
const thirdParties = [];
|
|
137
|
+
const summaries = insightSet.ThirdParties.entitySummaries || [];
|
|
138
|
+
for (const s of summaries) {
|
|
139
|
+
thirdParties.push({
|
|
140
|
+
entityName: s.entity?.name || 'Unknown',
|
|
141
|
+
transferSizeBytes: s.transferSize ?? 0,
|
|
142
|
+
mainThreadTimeMs: round(s.mainThreadTime ?? 0),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (thirdParties.length > 0) {
|
|
146
|
+
result.thirdParties = thirdParties;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (warnings.length > 0) {
|
|
150
|
+
result.warnings = warnings;
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type TimeRange } from '../../utils/filter.js';
|
|
2
|
+
import type { LoadedTrace } from '../loader.js';
|
|
3
|
+
import type { InteractionsResult } from '../types.js';
|
|
4
|
+
export interface ExtractInteractionsOptions {
|
|
5
|
+
timeRange?: TimeRange;
|
|
6
|
+
}
|
|
7
|
+
export declare function extractInteractions(loadedTrace: LoadedTrace, options?: ExtractInteractionsOptions): InteractionsResult;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Copyright 2026 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
import { isEventInBounds } from '../../utils/filter.js';
|
|
15
|
+
import { microToMilli, round } from '../../utils/units.js';
|
|
16
|
+
export function extractInteractions(loadedTrace, options) {
|
|
17
|
+
const { parsedTrace } = loadedTrace;
|
|
18
|
+
const timeRange = options?.timeRange;
|
|
19
|
+
const rawEvents = (parsedTrace.data.UserInteractions?.interactionEventsWithNoNesting ||
|
|
20
|
+
parsedTrace.data.UserInteractions?.interactionEvents ||
|
|
21
|
+
[]);
|
|
22
|
+
const interactions = [];
|
|
23
|
+
for (const ev of rawEvents) {
|
|
24
|
+
const startMs = microToMilli(ev.ts);
|
|
25
|
+
const durMs = microToMilli(ev.dur ?? 0);
|
|
26
|
+
if (!isEventInBounds(startMs, durMs, timeRange)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const inputDelayMs = microToMilli(ev.inputDelay ?? 0);
|
|
30
|
+
const processingMs = microToMilli(ev.mainThreadHandling ?? ev.processingDuration ?? 0);
|
|
31
|
+
const presentationDelayMs = microToMilli(ev.presentationDelay ?? 0);
|
|
32
|
+
let targetElement;
|
|
33
|
+
if (ev.args?.data?.nodeName) {
|
|
34
|
+
targetElement = ev.args.data.nodeName;
|
|
35
|
+
}
|
|
36
|
+
else if (ev.args?.data?.beginEvent?.args?.data?.nodeName) {
|
|
37
|
+
targetElement = ev.args.data.beginEvent.args.data.nodeName;
|
|
38
|
+
}
|
|
39
|
+
interactions.push({
|
|
40
|
+
eventType: ev.type || ev.name || 'interaction',
|
|
41
|
+
startTimeMs: round(startMs),
|
|
42
|
+
durationMs: round(durMs),
|
|
43
|
+
inputDelayMs: round(inputDelayMs),
|
|
44
|
+
processingDurationMs: round(processingMs),
|
|
45
|
+
presentationDelayMs: round(presentationDelayMs),
|
|
46
|
+
targetElement,
|
|
47
|
+
interactionId: ev.interactionId,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// Sort by duration descending
|
|
51
|
+
interactions.sort((a, b) => b.durationMs - a.durationMs);
|
|
52
|
+
return {
|
|
53
|
+
interactions,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LoadedTrace } from '../loader.js';
|
|
2
|
+
import type { NavigationsResult } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Extracts navigation events and associated insight mappings from trace metadata.
|
|
5
|
+
*
|
|
6
|
+
* Discovers navigations from Meta.navigationsByNavigationId and Meta.mainFrameNavigations,
|
|
7
|
+
* maps each navigation to its corresponding InsightSet ID (e.g. 'NAVIGATION_0'),
|
|
8
|
+
* and determines whether the navigation belongs to the primary main frame.
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractNavigations(loadedTrace: LoadedTrace): NavigationsResult;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Copyright 2026 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
import { microToMilli, round } from '../../utils/units.js';
|
|
15
|
+
import { findInsightEntry } from '../helpers.js';
|
|
16
|
+
function buildNavigationItem(navId, navEvent, finalUrls, isMainFrame, parsedTrace) {
|
|
17
|
+
const url = finalUrls?.get(navId) ||
|
|
18
|
+
navEvent?.args?.data?.documentLoaderURL ||
|
|
19
|
+
navEvent?.args?.data?.url ||
|
|
20
|
+
'';
|
|
21
|
+
const frameId = navEvent?.args?.frame || navEvent?.args?.data?.frame;
|
|
22
|
+
const tsMs = round(microToMilli(navEvent?.ts ?? 0));
|
|
23
|
+
const insightEntry = findInsightEntry(parsedTrace, navId);
|
|
24
|
+
return {
|
|
25
|
+
navigationId: navId,
|
|
26
|
+
insightSetId: insightEntry?.insightSetId,
|
|
27
|
+
frameId,
|
|
28
|
+
url,
|
|
29
|
+
timestampMs: tsMs,
|
|
30
|
+
isMainFrame,
|
|
31
|
+
hasInsights: Boolean(insightEntry),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extracts navigation events and associated insight mappings from trace metadata.
|
|
36
|
+
*
|
|
37
|
+
* Discovers navigations from Meta.navigationsByNavigationId and Meta.mainFrameNavigations,
|
|
38
|
+
* maps each navigation to its corresponding InsightSet ID (e.g. 'NAVIGATION_0'),
|
|
39
|
+
* and determines whether the navigation belongs to the primary main frame.
|
|
40
|
+
*/
|
|
41
|
+
export function extractNavigations(loadedTrace) {
|
|
42
|
+
const { parsedTrace } = loadedTrace;
|
|
43
|
+
const meta = parsedTrace.data?.Meta;
|
|
44
|
+
const navigationsMap = meta?.navigationsByNavigationId;
|
|
45
|
+
const finalUrls = meta?.finalDisplayUrlByNavigationId;
|
|
46
|
+
const mainFrameId = meta?.mainFrameId;
|
|
47
|
+
const mainFrameNavs = meta?.mainFrameNavigations;
|
|
48
|
+
const insightsMap = parsedTrace.insights;
|
|
49
|
+
const insightSetIds = insightsMap ? Array.from(insightsMap.keys()) : [];
|
|
50
|
+
const navigations = [];
|
|
51
|
+
const seenIds = new Set();
|
|
52
|
+
const mainFrameNavIdSet = new Set((mainFrameNavs || [])
|
|
53
|
+
.map((m) => m?.args?.data?.navigationId)
|
|
54
|
+
.filter((id) => typeof id === 'string' && id.length > 0));
|
|
55
|
+
if (navigationsMap && navigationsMap.size > 0) {
|
|
56
|
+
for (const [navId, navEvent] of navigationsMap.entries()) {
|
|
57
|
+
seenIds.add(navId);
|
|
58
|
+
const frameId = navEvent?.args?.frame || navEvent?.args?.data?.frame;
|
|
59
|
+
const isMainFrame = Boolean(navEvent?.args?.data?.isOutermostMainFrame ??
|
|
60
|
+
navEvent?.args?.data?.isInPrimaryMainFrame ??
|
|
61
|
+
navEvent?.args?.data?.isMainFrame ??
|
|
62
|
+
mainFrameNavIdSet.has(navId) ??
|
|
63
|
+
(frameId && frameId === mainFrameId));
|
|
64
|
+
navigations.push(buildNavigationItem(navId, navEvent, finalUrls, isMainFrame, parsedTrace));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (mainFrameNavs && mainFrameNavs.length > 0) {
|
|
68
|
+
for (const navEvent of mainFrameNavs) {
|
|
69
|
+
const navId = navEvent?.args?.data?.navigationId;
|
|
70
|
+
if (navId && !seenIds.has(navId)) {
|
|
71
|
+
seenIds.add(navId);
|
|
72
|
+
navigations.push(buildNavigationItem(navId, navEvent, finalUrls, true, parsedTrace));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
navigations.sort((a, b) => a.timestampMs - b.timestampMs);
|
|
77
|
+
return {
|
|
78
|
+
total: navigations.length,
|
|
79
|
+
navigations,
|
|
80
|
+
insightSetIds,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type TimeRange } from '../../utils/filter.js';
|
|
2
|
+
import type { LoadedTrace } from '../loader.js';
|
|
3
|
+
import type { NetworkResult } from '../types.js';
|
|
4
|
+
export interface ExtractNetworkOptions {
|
|
5
|
+
renderBlocking?: boolean;
|
|
6
|
+
slowest?: number;
|
|
7
|
+
initiatorChain?: string;
|
|
8
|
+
timeRange?: TimeRange;
|
|
9
|
+
}
|
|
10
|
+
export declare function extractNetwork(loadedTrace: LoadedTrace, options?: ExtractNetworkOptions): NetworkResult;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Copyright 2026 Google LLC
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
import { isEventInBounds } from '../../utils/filter.js';
|
|
15
|
+
import { microToMilli, round } from '../../utils/units.js';
|
|
16
|
+
export function extractNetwork(loadedTrace, options) {
|
|
17
|
+
const { engine, parsedTrace } = loadedTrace;
|
|
18
|
+
const renderBlockingOnly = options?.renderBlocking ?? false;
|
|
19
|
+
const slowest = options?.slowest ?? 20;
|
|
20
|
+
const initiatorTarget = options?.initiatorChain;
|
|
21
|
+
const timeRange = options?.timeRange;
|
|
22
|
+
const rawRequests = (parsedTrace.data.NetworkRequests?.byTime || []);
|
|
23
|
+
const result = {
|
|
24
|
+
requests: [],
|
|
25
|
+
};
|
|
26
|
+
// 1. Check Initiator Chain if requested
|
|
27
|
+
if (initiatorTarget) {
|
|
28
|
+
const targetReq = rawRequests.find((r) => r.args?.data?.requestId === initiatorTarget ||
|
|
29
|
+
Boolean(r.args?.data?.url?.includes(initiatorTarget)));
|
|
30
|
+
if (targetReq) {
|
|
31
|
+
const chain = [];
|
|
32
|
+
let current = targetReq;
|
|
33
|
+
const visited = new Set();
|
|
34
|
+
while (current && !visited.has(current)) {
|
|
35
|
+
visited.add(current);
|
|
36
|
+
const url = current.args?.data?.url || current.name || 'Unknown';
|
|
37
|
+
const initiator = engine.Extras.Initiators.getNetworkInitiator(parsedTrace.data, current);
|
|
38
|
+
const initiatorUrl = initiator?.args?.data?.url || initiator?.name;
|
|
39
|
+
chain.push({
|
|
40
|
+
url,
|
|
41
|
+
initiatorUrl,
|
|
42
|
+
type: current.args?.data?.mimeType || current.name,
|
|
43
|
+
});
|
|
44
|
+
current = initiator;
|
|
45
|
+
}
|
|
46
|
+
result.initiatorChain = chain;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// 2. Identify render-blocking requests from insights if available
|
|
50
|
+
const blockingUrls = new Set();
|
|
51
|
+
if (parsedTrace.insights) {
|
|
52
|
+
for (const val of parsedTrace.insights.values()) {
|
|
53
|
+
const insight = val.model || val;
|
|
54
|
+
if (insight?.RenderBlocking?.renderBlockingRequests) {
|
|
55
|
+
for (const req of insight.RenderBlocking.renderBlockingRequests) {
|
|
56
|
+
if (req.args?.data?.url) {
|
|
57
|
+
blockingUrls.add(req.args.data.url);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// 3. Process requests
|
|
64
|
+
const processed = [];
|
|
65
|
+
for (const req of rawRequests) {
|
|
66
|
+
const startMs = microToMilli(req.ts);
|
|
67
|
+
const durMs = microToMilli(req.dur ?? 0);
|
|
68
|
+
const url = req.args?.data?.url || '';
|
|
69
|
+
if (!isEventInBounds(startMs, durMs, timeRange)) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const isBlocking = req.args?.data?.renderBlocking === 'blocking' ||
|
|
73
|
+
req.args?.data?.renderBlocking === 'in_body_parser_blocking' ||
|
|
74
|
+
blockingUrls.has(url);
|
|
75
|
+
if (renderBlockingOnly && !isBlocking) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const syn = req.args?.data?.syntheticData;
|
|
79
|
+
const queueingMs = syn?.queueing ? microToMilli(syn.queueing) : 0;
|
|
80
|
+
const dnsMs = syn?.dnsLookup ? microToMilli(syn.dnsLookup) : 0;
|
|
81
|
+
const sslMs = syn?.ssl ? microToMilli(syn.ssl) : 0;
|
|
82
|
+
const ttfbMs = syn?.waiting ? microToMilli(syn.waiting) : 0;
|
|
83
|
+
const downloadMs = syn?.download ? microToMilli(syn.download) : 0;
|
|
84
|
+
processed.push({
|
|
85
|
+
requestId: req.args?.data?.requestId,
|
|
86
|
+
url,
|
|
87
|
+
mimeType: req.args?.data?.mimeType || '',
|
|
88
|
+
priority: req.args?.data?.priority || 'Normal',
|
|
89
|
+
timing: {
|
|
90
|
+
queueingMs: round(queueingMs),
|
|
91
|
+
dnsMs: round(dnsMs),
|
|
92
|
+
sslMs: round(sslMs),
|
|
93
|
+
ttfbMs: round(ttfbMs),
|
|
94
|
+
downloadMs: round(downloadMs),
|
|
95
|
+
totalDurationMs: round(durMs),
|
|
96
|
+
},
|
|
97
|
+
transferSizeBytes: req.args?.data?.encodedDataLength ?? 0,
|
|
98
|
+
renderBlocking: isBlocking,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
// Sort by totalDurationMs descending
|
|
102
|
+
processed.sort((a, b) => b.timing.totalDurationMs - a.timing.totalDurationMs);
|
|
103
|
+
result.requests = processed.slice(0, slowest);
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TimeRange } from '../../utils/filter.js';
|
|
2
|
+
import type { LoadedTrace } from '../loader.js';
|
|
3
|
+
import type { OverviewResult } from '../types.js';
|
|
4
|
+
export interface ExtractOverviewOptions {
|
|
5
|
+
timeRange?: TimeRange;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Extracts high-level trace metadata, Core Web Vitals, and main thread category distribution.
|
|
9
|
+
* All duration and timestamp properties are normalized to milliseconds (ms).
|
|
10
|
+
*/
|
|
11
|
+
export declare function extractOverview(loadedTrace: LoadedTrace, options?: ExtractOverviewOptions): OverviewResult;
|