lighthouse 13.1.0 → 13.2.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/cli/bin.js +5 -0
- package/core/audits/accessibility/autocomplete-valid.d.ts +10 -0
- package/core/audits/accessibility/autocomplete-valid.js +44 -0
- package/core/audits/accessibility/presentation-role-conflict.d.ts +10 -0
- package/core/audits/accessibility/presentation-role-conflict.js +46 -0
- package/core/audits/accessibility/svg-img-alt.d.ts +10 -0
- package/core/audits/accessibility/svg-img-alt.js +44 -0
- package/core/audits/agentic/agent-accessibility-tree.d.ts +19 -0
- package/core/audits/agentic/agent-accessibility-tree.js +115 -0
- package/core/audits/agentic/llms-txt.d.ts +20 -0
- package/core/audits/agentic/llms-txt.js +111 -0
- package/core/audits/insights/insight-audit.d.ts +2 -2
- package/core/audits/insights/insight-audit.js +16 -6
- package/core/audits/layout-shifts.js +1 -1
- package/core/audits/server-response-time.js +3 -3
- package/core/audits/webmcp-form-coverage.d.ts +16 -0
- package/core/audits/webmcp-form-coverage.js +90 -0
- package/core/audits/webmcp-registered-tools.d.ts +21 -0
- package/core/audits/webmcp-registered-tools.js +149 -0
- package/core/audits/webmcp-schema-validity.d.ts +22 -0
- package/core/audits/webmcp-schema-validity.js +141 -0
- package/core/computed/document-urls.js +0 -1
- package/core/computed/main-resource.js +0 -2
- package/core/computed/metrics/lantern-metric.js +4 -4
- package/core/computed/metrics/lcp-breakdown.js +1 -1
- package/core/computed/metrics/time-to-first-byte.js +1 -1
- package/core/computed/navigation-insights.js +2 -1
- package/core/computed/network-analysis.js +0 -1
- package/core/config/agentic-browsing-config.d.ts +12 -0
- package/core/config/agentic-browsing-config.js +73 -0
- package/core/config/default-config.js +6 -0
- package/core/gather/gatherers/accessibility.js +5 -1
- package/core/gather/gatherers/agentic/llms-txt.d.ts +10 -0
- package/core/gather/gatherers/agentic/llms-txt.js +28 -0
- package/core/gather/gatherers/inputs.js +2 -0
- package/core/gather/gatherers/meta-elements.js +1 -1
- package/core/gather/gatherers/trace-elements.js +1 -1
- package/core/gather/gatherers/webmcp-schema.d.ts +25 -0
- package/core/gather/gatherers/webmcp-schema.js +105 -0
- package/core/gather/gatherers/webmcp.d.ts +58 -0
- package/core/gather/gatherers/webmcp.js +159 -0
- package/core/index.d.ts +1 -0
- package/core/index.js +1 -0
- package/core/lib/cdt/generated/SourceMap.js +2 -2
- package/core/lib/navigation-error.js +0 -6
- package/core/lib/network-request.js +0 -1
- package/core/lib/page-functions.d.ts +3 -3
- package/core/lib/page-functions.js +11 -4
- package/core/lib/tracehouse/trace-processor.d.ts +5 -4
- package/core/lib/tracehouse/trace-processor.js +85 -19
- package/core/runner.js +3 -0
- package/core/scoring.d.ts +1 -0
- package/dist/report/bundle.esm.js +1 -1
- package/dist/report/flow.js +3 -3
- package/dist/report/standalone.js +1 -1
- package/flow-report/src/summary/category.tsx +1 -1
- package/package.json +11 -11
- package/report/renderer/category-renderer.js +1 -1
- package/report/renderer/report-utils.d.ts +2 -1
- package/report/renderer/report-utils.js +7 -2
- package/shared/localization/locales/ar-XB.json +72 -36
- package/shared/localization/locales/ar.json +72 -36
- package/shared/localization/locales/bg.json +72 -36
- package/shared/localization/locales/ca.json +72 -36
- package/shared/localization/locales/cs.json +72 -36
- package/shared/localization/locales/da.json +74 -38
- package/shared/localization/locales/de.json +72 -36
- package/shared/localization/locales/el.json +73 -37
- package/shared/localization/locales/en-GB.json +74 -38
- package/shared/localization/locales/en-US.json +245 -17
- package/shared/localization/locales/en-XL.json +245 -17
- package/shared/localization/locales/es-419.json +72 -36
- package/shared/localization/locales/es.json +73 -37
- package/shared/localization/locales/fi.json +72 -36
- package/shared/localization/locales/fil.json +74 -38
- package/shared/localization/locales/fr.json +162 -126
- package/shared/localization/locales/he.json +74 -38
- package/shared/localization/locales/hi.json +73 -37
- package/shared/localization/locales/hr.json +72 -36
- package/shared/localization/locales/hu.json +73 -37
- package/shared/localization/locales/id.json +74 -38
- package/shared/localization/locales/it.json +72 -36
- package/shared/localization/locales/ja.json +72 -36
- package/shared/localization/locales/ko.json +72 -36
- package/shared/localization/locales/lt.json +72 -36
- package/shared/localization/locales/lv.json +72 -36
- package/shared/localization/locales/nl.json +73 -37
- package/shared/localization/locales/no.json +72 -36
- package/shared/localization/locales/pl.json +72 -36
- package/shared/localization/locales/pt-PT.json +72 -36
- package/shared/localization/locales/pt.json +74 -38
- package/shared/localization/locales/ro.json +72 -36
- package/shared/localization/locales/ru.json +72 -36
- package/shared/localization/locales/sk.json +72 -36
- package/shared/localization/locales/sl.json +72 -36
- package/shared/localization/locales/sr-Latn.json +73 -37
- package/shared/localization/locales/sr.json +73 -37
- package/shared/localization/locales/sv.json +75 -39
- package/shared/localization/locales/ta.json +73 -37
- package/shared/localization/locales/te.json +72 -36
- package/shared/localization/locales/th.json +73 -37
- package/shared/localization/locales/tr.json +72 -36
- package/shared/localization/locales/uk.json +72 -36
- package/shared/localization/locales/vi.json +74 -38
- package/shared/localization/locales/zh-HK.json +72 -36
- package/shared/localization/locales/zh-TW.json +74 -38
- package/shared/localization/locales/zh.json +75 -39
- package/types/artifacts.d.ts +33 -0
- package/types/config.d.ts +1 -0
- package/types/lhr/lhr.d.ts +11 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import BaseGatherer from '../base-gatherer.js';
|
|
8
|
+
import {resolveNodeIdToObjectId} from '../driver/dom.js';
|
|
9
|
+
import {pageFunctions} from '../../lib/page-functions.js';
|
|
10
|
+
import {ExecutionContext} from '../driver/execution-context.js';
|
|
11
|
+
|
|
12
|
+
class WebMcpSchemaIssues extends BaseGatherer {
|
|
13
|
+
/** @type {LH.Gatherer.GathererMeta} */
|
|
14
|
+
meta = {
|
|
15
|
+
supportedModes: ['navigation', 'snapshot'],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
super();
|
|
20
|
+
/** @type {LH.Artifacts.WebMcpSchemaIssue[]} */
|
|
21
|
+
this._issues = [];
|
|
22
|
+
this._onIssueAdded = this.onIssueAdded.bind(this);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {Record<string, any>} event
|
|
27
|
+
*/
|
|
28
|
+
onIssueAdded(event) {
|
|
29
|
+
const issue = event.issue;
|
|
30
|
+
if (!issue || issue.code !== 'GenericIssue') return;
|
|
31
|
+
|
|
32
|
+
const details = issue.details?.genericIssueDetails;
|
|
33
|
+
if (!details) return;
|
|
34
|
+
|
|
35
|
+
const errorType = details.errorType;
|
|
36
|
+
if (errorType && (
|
|
37
|
+
errorType === 'FormModelContextMissingToolName' ||
|
|
38
|
+
errorType === 'FormModelContextMissingToolDescription' ||
|
|
39
|
+
errorType === 'FormModelContextRequiredParameterMissingName' ||
|
|
40
|
+
errorType === 'FormModelContextParameterMissingTitleAndDescription' ||
|
|
41
|
+
errorType === 'FormModelContextParameterMissingName'
|
|
42
|
+
)) {
|
|
43
|
+
this._issues.push(details);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {LH.Gatherer.Context} passContext
|
|
49
|
+
*/
|
|
50
|
+
async startInstrumentation(passContext) {
|
|
51
|
+
const session = passContext.driver.defaultSession;
|
|
52
|
+
session.on('Audits.issueAdded', this._onIssueAdded);
|
|
53
|
+
await session.sendCommand('Audits.enable');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {LH.Gatherer.Context} passContext
|
|
58
|
+
*/
|
|
59
|
+
async stopInstrumentation(passContext) {
|
|
60
|
+
const session = passContext.driver.defaultSession;
|
|
61
|
+
session.off('Audits.issueAdded', this._onIssueAdded);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {LH.Gatherer.Context} context
|
|
66
|
+
* @return {Promise<LH.Artifacts.WebMcpSchemaIssue[]>}
|
|
67
|
+
*/
|
|
68
|
+
async getArtifact(context) {
|
|
69
|
+
const session = context.driver.defaultSession;
|
|
70
|
+
|
|
71
|
+
const deps = ExecutionContext.serializeDeps([
|
|
72
|
+
pageFunctions.getNodeDetails,
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const promises = this._issues.map(async (issue) => {
|
|
76
|
+
const processedIssue = {...issue};
|
|
77
|
+
if (issue.violatingNodeId) {
|
|
78
|
+
try {
|
|
79
|
+
const objectId = await resolveNodeIdToObjectId(session, issue.violatingNodeId);
|
|
80
|
+
if (objectId) {
|
|
81
|
+
const response = await session.sendCommand('Runtime.callFunctionOn', {
|
|
82
|
+
objectId,
|
|
83
|
+
functionDeclaration: `function () {
|
|
84
|
+
${deps}
|
|
85
|
+
return getNodeDetails(this);
|
|
86
|
+
}`,
|
|
87
|
+
returnByValue: true,
|
|
88
|
+
awaitPromise: true,
|
|
89
|
+
});
|
|
90
|
+
if (response && response.result && response.result.value) {
|
|
91
|
+
processedIssue.nodeDetails = response.result.value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
// Ignore error
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return processedIssue;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return Promise.all(promises);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export default WebMcpSchemaIssues;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export default WebMCP;
|
|
2
|
+
export type WebMCPTool = {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
inputSchema: Record<string, any>;
|
|
6
|
+
frameId: string;
|
|
7
|
+
backendNodeId?: number | undefined;
|
|
8
|
+
stackTrace?: any;
|
|
9
|
+
nodeDetails?: import("../../index.js").Artifacts.NodeDetails | undefined;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {Object} WebMCPTool
|
|
13
|
+
* @property {string} name
|
|
14
|
+
* @property {string} description
|
|
15
|
+
* @property {Record<string, any>} inputSchema
|
|
16
|
+
* @property {string} frameId
|
|
17
|
+
* @property {number} [backendNodeId]
|
|
18
|
+
* @property {any} [stackTrace]
|
|
19
|
+
* @property {LH.Artifacts.NodeDetails} [nodeDetails]
|
|
20
|
+
*/
|
|
21
|
+
declare class WebMCP extends BaseGatherer {
|
|
22
|
+
/** @type {WebMCPTool[]} */
|
|
23
|
+
_tools: WebMCPTool[];
|
|
24
|
+
_isSupported: boolean;
|
|
25
|
+
_onToolsAdded: (event: {
|
|
26
|
+
tools: WebMCPTool[];
|
|
27
|
+
}) => void;
|
|
28
|
+
_onToolsRemoved: (event: {
|
|
29
|
+
tools: WebMCPTool[];
|
|
30
|
+
}) => void;
|
|
31
|
+
/**
|
|
32
|
+
* @param {{tools: WebMCPTool[]}} event
|
|
33
|
+
*/
|
|
34
|
+
onToolsAdded(event: {
|
|
35
|
+
tools: WebMCPTool[];
|
|
36
|
+
}): void;
|
|
37
|
+
/**
|
|
38
|
+
* @param {{tools: WebMCPTool[]}} event
|
|
39
|
+
*/
|
|
40
|
+
onToolsRemoved(event: {
|
|
41
|
+
tools: WebMCPTool[];
|
|
42
|
+
}): void;
|
|
43
|
+
/**
|
|
44
|
+
* @param {LH.Gatherer.Context} passContext
|
|
45
|
+
*/
|
|
46
|
+
startInstrumentation(passContext: LH.Gatherer.Context): Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* @param {LH.Gatherer.Context} passContext
|
|
49
|
+
*/
|
|
50
|
+
stopInstrumentation(passContext: LH.Gatherer.Context): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* @param {LH.Gatherer.Context} context
|
|
53
|
+
* @return {Promise<LH.Artifacts['WebMCP']>}
|
|
54
|
+
*/
|
|
55
|
+
getArtifact(context: LH.Gatherer.Context): Promise<LH.Artifacts["WebMCP"]>;
|
|
56
|
+
}
|
|
57
|
+
import BaseGatherer from '../base-gatherer.js';
|
|
58
|
+
//# sourceMappingURL=webmcp.d.ts.map
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @fileoverview Capture WebMCP data
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import BaseGatherer from '../base-gatherer.js';
|
|
12
|
+
import {resolveNodeIdToObjectId} from '../driver/dom.js';
|
|
13
|
+
import {pageFunctions} from '../../lib/page-functions.js';
|
|
14
|
+
import {ExecutionContext} from '../driver/execution-context.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} WebMCPTool
|
|
18
|
+
* @property {string} name
|
|
19
|
+
* @property {string} description
|
|
20
|
+
* @property {Record<string, any>} inputSchema
|
|
21
|
+
* @property {string} frameId
|
|
22
|
+
* @property {number} [backendNodeId]
|
|
23
|
+
* @property {any} [stackTrace]
|
|
24
|
+
* @property {LH.Artifacts.NodeDetails} [nodeDetails]
|
|
25
|
+
*/
|
|
26
|
+
class WebMCP extends BaseGatherer {
|
|
27
|
+
/** @type {LH.Gatherer.GathererMeta} */
|
|
28
|
+
meta = {
|
|
29
|
+
supportedModes: ['navigation', 'snapshot'],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
constructor() {
|
|
33
|
+
super();
|
|
34
|
+
/** @type {WebMCPTool[]} */
|
|
35
|
+
this._tools = [];
|
|
36
|
+
this._isSupported = true;
|
|
37
|
+
this._onToolsAdded = this.onToolsAdded.bind(this);
|
|
38
|
+
this._onToolsRemoved = this.onToolsRemoved.bind(this);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {{tools: WebMCPTool[]}} event
|
|
43
|
+
*/
|
|
44
|
+
// TODO: Handle WebMCP tools per frame.
|
|
45
|
+
onToolsAdded(event) {
|
|
46
|
+
// Note that as of M148, there is a bug in WebMCP CDP.
|
|
47
|
+
// While WebMCP is enabled, any newly registered tool will
|
|
48
|
+
// have an empty schema.
|
|
49
|
+
if (event.tools) {
|
|
50
|
+
this._tools.push(...event.tools);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {{tools: WebMCPTool[]}} event
|
|
56
|
+
*/
|
|
57
|
+
onToolsRemoved(event) {
|
|
58
|
+
if (event.tools) {
|
|
59
|
+
const removedNames = new Set(event.tools.map(t => t.name));
|
|
60
|
+
this._tools = this._tools.filter(t => !removedNames.has(t.name));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {LH.Gatherer.Context} passContext
|
|
66
|
+
*/
|
|
67
|
+
async startInstrumentation(passContext) {
|
|
68
|
+
const session = passContext.driver.defaultSession;
|
|
69
|
+
|
|
70
|
+
// @ts-expect-error - WebMCP domain might not be in types yet.
|
|
71
|
+
session.on('WebMCP.toolsAdded', this._onToolsAdded);
|
|
72
|
+
// @ts-expect-error
|
|
73
|
+
session.on('WebMCP.toolsRemoved', this._onToolsRemoved);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await session.sendCommand('WebMCP.enable');
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (err.message.includes('\'WebMCP.enable\' wasn\'t found')) {
|
|
79
|
+
this._isSupported = false;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {LH.Gatherer.Context} passContext
|
|
88
|
+
*/
|
|
89
|
+
async stopInstrumentation(passContext) {
|
|
90
|
+
const session = passContext.driver.defaultSession;
|
|
91
|
+
// @ts-expect-error
|
|
92
|
+
session.off('WebMCP.toolsAdded', this._onToolsAdded);
|
|
93
|
+
// @ts-expect-error
|
|
94
|
+
session.off('WebMCP.toolsRemoved', this._onToolsRemoved);
|
|
95
|
+
try {
|
|
96
|
+
await session.sendCommand('WebMCP.disable');
|
|
97
|
+
} catch (err) {
|
|
98
|
+
// Ignore errors
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {LH.Gatherer.Context} context
|
|
104
|
+
* @return {Promise<LH.Artifacts['WebMCP']>}
|
|
105
|
+
*/
|
|
106
|
+
async getArtifact(context) {
|
|
107
|
+
const isSupported = await context.driver.executionContext.evaluate(
|
|
108
|
+
// @ts-expect-error - modelContext is not in types
|
|
109
|
+
() => typeof navigator.modelContext !== 'undefined',
|
|
110
|
+
{args: [], useIsolation: true}
|
|
111
|
+
);
|
|
112
|
+
if (!isSupported || !this._isSupported) {
|
|
113
|
+
return {isSupported: false, tools: []};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const session = context.driver.defaultSession;
|
|
117
|
+
|
|
118
|
+
// Remove duplicates based on name, keeping the latest occurrence.
|
|
119
|
+
const toolMap = new Map();
|
|
120
|
+
for (const tool of this._tools) {
|
|
121
|
+
toolMap.set(tool.name, tool);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const resolvedTools = [];
|
|
125
|
+
for (const tool of toolMap.values()) {
|
|
126
|
+
if (tool.backendNodeId) {
|
|
127
|
+
try {
|
|
128
|
+
const objectId = await resolveNodeIdToObjectId(session, tool.backendNodeId);
|
|
129
|
+
if (objectId) {
|
|
130
|
+
const deps = ExecutionContext.serializeDeps([
|
|
131
|
+
pageFunctions.getNodeDetails,
|
|
132
|
+
]);
|
|
133
|
+
const response = await session.sendCommand('Runtime.callFunctionOn', {
|
|
134
|
+
objectId,
|
|
135
|
+
functionDeclaration: `function () {
|
|
136
|
+
${deps}
|
|
137
|
+
return getNodeDetails(this);
|
|
138
|
+
}`,
|
|
139
|
+
returnByValue: true,
|
|
140
|
+
awaitPromise: true,
|
|
141
|
+
});
|
|
142
|
+
if (response && response.result && response.result.value) {
|
|
143
|
+
tool.nodeDetails = response.result.value;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} catch (err) {
|
|
147
|
+
// Ignore error
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
resolvedTools.push(tool);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
isSupported: true,
|
|
154
|
+
tools: resolvedTools,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export default WebMCP;
|
package/core/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export { default as Gatherer } from "./gather/base-gatherer.js";
|
|
|
4
4
|
export { NetworkRecords } from "./computed/network-records.js";
|
|
5
5
|
export { default as defaultConfig } from "./config/default-config.js";
|
|
6
6
|
export { default as desktopConfig } from "./config/desktop-config.js";
|
|
7
|
+
export { default as agenticBrowsingConfig } from "./config/agentic-browsing-config.js";
|
|
7
8
|
export * from "../types/lh.js";
|
|
8
9
|
/**
|
|
9
10
|
* Run Lighthouse.
|
package/core/index.js
CHANGED
|
@@ -121,6 +121,7 @@ export {default as Gatherer} from './gather/base-gatherer.js';
|
|
|
121
121
|
export {NetworkRecords} from './computed/network-records.js';
|
|
122
122
|
export {default as defaultConfig} from './config/default-config.js';
|
|
123
123
|
export {default as desktopConfig} from './config/desktop-config.js';
|
|
124
|
+
export {default as agenticBrowsingConfig} from './config/agentic-browsing-config.js';
|
|
124
125
|
export * from '../types/lh.js';
|
|
125
126
|
export {
|
|
126
127
|
startFlow,
|
|
@@ -381,7 +381,7 @@ class SourceMap {
|
|
|
381
381
|
let nameIndex = 0;
|
|
382
382
|
const names = map.names ?? [];
|
|
383
383
|
const tokenIter = new TokenIterator(map.mappings);
|
|
384
|
-
let sourceURL = this.#sourceInfos[sourceIndex]
|
|
384
|
+
let sourceURL = this.#sourceInfos[sourceIndex]?.sourceURL;
|
|
385
385
|
while (true) {
|
|
386
386
|
if (tokenIter.peek() === ',') {
|
|
387
387
|
tokenIter.next();
|
|
@@ -404,7 +404,7 @@ class SourceMap {
|
|
|
404
404
|
const sourceIndexDelta = tokenIter.nextVLQ();
|
|
405
405
|
if (sourceIndexDelta) {
|
|
406
406
|
sourceIndex += sourceIndexDelta;
|
|
407
|
-
sourceURL = this.#sourceInfos[sourceIndex]
|
|
407
|
+
sourceURL = this.#sourceInfos[sourceIndex]?.sourceURL;
|
|
408
408
|
}
|
|
409
409
|
sourceLineNumber += tokenIter.nextVLQ();
|
|
410
410
|
sourceColumnNumber += tokenIter.nextVLQ();
|
|
@@ -131,7 +131,6 @@ function getNonHtmlError(finalRecord) {
|
|
|
131
131
|
function getPageLoadError(navigationError, context) {
|
|
132
132
|
const {url, networkRecords} = context;
|
|
133
133
|
const mainRecordLantern = Lantern.Core.NetworkAnalyzer.findResourceForUrl(
|
|
134
|
-
// @ts-expect-error - trace engine types for InitiatorType are outdated
|
|
135
134
|
networkRecords,
|
|
136
135
|
url
|
|
137
136
|
);
|
|
@@ -144,7 +143,6 @@ function getPageLoadError(navigationError, context) {
|
|
|
144
143
|
record.resourceType === NetworkRequest.TYPES.Document
|
|
145
144
|
);
|
|
146
145
|
if (documentRequests.length) {
|
|
147
|
-
// @ts-expect-error - mainRecord is inferred as a Lantern request from findResourceForUrl, but we assign a raw record here.
|
|
148
146
|
mainRecord = documentRequests.reduce((min, r) => {
|
|
149
147
|
return r.networkRequestTime < min.networkRequestTime ? r : min;
|
|
150
148
|
});
|
|
@@ -164,12 +162,8 @@ function getPageLoadError(navigationError, context) {
|
|
|
164
162
|
context.warnings.push(str_(UIStrings.warningXhtml));
|
|
165
163
|
}
|
|
166
164
|
|
|
167
|
-
// @ts-expect-error - mainRecord may be typed as a Lantern request, but functions expect a raw record.
|
|
168
165
|
const networkError = getNetworkError(mainRecord, context);
|
|
169
|
-
// @ts-expect-error - mainRecord may be typed as a Lantern request, but functions expect a raw record.
|
|
170
166
|
const interstitialError = getInterstitialError(mainRecord, networkRecords);
|
|
171
|
-
// @ts-expect-error - finalRecord may be a Lantern request, which is compatible enough
|
|
172
|
-
// for getNonHtmlError.
|
|
173
167
|
const nonHtmlError = getNonHtmlError(finalRecord);
|
|
174
168
|
|
|
175
169
|
// We want to special-case the interstitial beyond FAILED_DOCUMENT_REQUEST. See https://github.com/GoogleChrome/lighthouse/pull/8865#issuecomment-497507618
|
|
@@ -88,10 +88,10 @@ declare namespace getOuterHTMLSnippet {
|
|
|
88
88
|
*/
|
|
89
89
|
declare function computeBenchmarkIndex(): number;
|
|
90
90
|
/**
|
|
91
|
-
* @param {
|
|
92
|
-
* @return {LH.Artifacts.NodeDetails}
|
|
91
|
+
* @param {Node|ShadowRoot} node
|
|
92
|
+
* @return {LH.Artifacts.NodeDetails | null}
|
|
93
93
|
*/
|
|
94
|
-
declare function getNodeDetails(
|
|
94
|
+
declare function getNodeDetails(node: Node | ShadowRoot): LH.Artifacts.NodeDetails | null;
|
|
95
95
|
declare namespace getNodeDetails {
|
|
96
96
|
function toString(): string;
|
|
97
97
|
}
|
|
@@ -450,16 +450,23 @@ function wrapRequestIdleCallback(cpuSlowdownMultiplier) {
|
|
|
450
450
|
}
|
|
451
451
|
|
|
452
452
|
/**
|
|
453
|
-
* @param {
|
|
454
|
-
* @return {LH.Artifacts.NodeDetails}
|
|
453
|
+
* @param {Node|ShadowRoot} node
|
|
454
|
+
* @return {LH.Artifacts.NodeDetails | null}
|
|
455
455
|
*/
|
|
456
|
-
function getNodeDetails(
|
|
456
|
+
function getNodeDetails(node) {
|
|
457
457
|
// This bookkeeping is for the FullPageScreenshot gatherer.
|
|
458
458
|
if (!window.__lighthouseNodesDontTouchOrAllVarianceGoesAway) {
|
|
459
459
|
window.__lighthouseNodesDontTouchOrAllVarianceGoesAway = new Map();
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
-
|
|
462
|
+
let elem = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
|
|
463
|
+
if (!elem && node instanceof ShadowRoot) {
|
|
464
|
+
elem = node.host;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (!elem) return null;
|
|
468
|
+
|
|
469
|
+
const element = /** @type {Element} */ (elem);
|
|
463
470
|
const selector = getNodeSelector(element);
|
|
464
471
|
|
|
465
472
|
// Create an id that will be unique across all execution contexts.
|
|
@@ -196,9 +196,10 @@ export class TraceProcessor {
|
|
|
196
196
|
*
|
|
197
197
|
* @param {LH.TraceEvent[]} events
|
|
198
198
|
* @param {LH.TraceEvent} timeOriginEvent
|
|
199
|
+
* @param {string|undefined} mainFrameId
|
|
199
200
|
* @return {{lcp: LCPEvent | undefined, invalidated: boolean}}
|
|
200
201
|
*/
|
|
201
|
-
static computeValidLCPAllFrames(events: LH.TraceEvent[], timeOriginEvent: LH.TraceEvent): {
|
|
202
|
+
static computeValidLCPAllFrames(events: LH.TraceEvent[], timeOriginEvent: LH.TraceEvent, mainFrameId: string | undefined): {
|
|
202
203
|
lcp: LCPEvent | undefined;
|
|
203
204
|
invalidated: boolean;
|
|
204
205
|
};
|
|
@@ -217,7 +218,7 @@ export class TraceProcessor {
|
|
|
217
218
|
* @param {LH.Trace} trace
|
|
218
219
|
* @param {{timeOriginDeterminationMethod?: TimeOriginDeterminationMethod}} [options]
|
|
219
220
|
* @return {LH.Artifacts.ProcessedTrace}
|
|
220
|
-
|
|
221
|
+
*/
|
|
221
222
|
static processTrace(trace: LH.Trace, options?: {
|
|
222
223
|
timeOriginDeterminationMethod?: TimeOriginDeterminationMethod;
|
|
223
224
|
}): LH.Artifacts.ProcessedTrace;
|
|
@@ -226,7 +227,7 @@ export class TraceProcessor {
|
|
|
226
227
|
* origin in addition to the standard microsecond monotonic timestamps.
|
|
227
228
|
* @param {LH.Artifacts.ProcessedTrace} processedTrace
|
|
228
229
|
* @return {LH.Artifacts.ProcessedNavigation}
|
|
229
|
-
|
|
230
|
+
*/
|
|
230
231
|
static processNavigation(processedTrace: LH.Artifacts.ProcessedTrace): LH.Artifacts.ProcessedNavigation;
|
|
231
232
|
/**
|
|
232
233
|
* Computes the last observable timestamp in a set of trace events.
|
|
@@ -272,7 +273,7 @@ export class TraceProcessor {
|
|
|
272
273
|
* in addition to the standard microsecond monotonic timestamps.
|
|
273
274
|
* @param {Array<LH.TraceEvent>} frameEvents
|
|
274
275
|
* @param {{timeOriginEvt: LH.TraceEvent}} options
|
|
275
|
-
|
|
276
|
+
*/
|
|
276
277
|
static computeNavigationTimingsForFrame(frameEvents: Array<LH.TraceEvent>, options: {
|
|
277
278
|
timeOriginEvt: LH.TraceEvent;
|
|
278
279
|
}): {
|
|
@@ -349,7 +349,7 @@ class TraceProcessor {
|
|
|
349
349
|
|
|
350
350
|
const ret = this.getMainThreadTopLevelEventDurations(events, startTime, endTime);
|
|
351
351
|
return this._riskPercentiles(ret.durations, totalTime, percentiles,
|
|
352
|
-
|
|
352
|
+
ret.clippedLength);
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
/**
|
|
@@ -479,9 +479,9 @@ class TraceProcessor {
|
|
|
479
479
|
const firstResourceSendEvt = events.find(e => e.name === 'ResourceSendRequest');
|
|
480
480
|
// We know that these properties exist if we found the events, but TSC doesn't.
|
|
481
481
|
if (navStartEvt?.args?.data &&
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
482
|
+
firstResourceSendEvt &&
|
|
483
|
+
firstResourceSendEvt.pid === navStartEvt.pid &&
|
|
484
|
+
firstResourceSendEvt.tid === navStartEvt.tid) {
|
|
485
485
|
const frameId = navStartEvt.args.frame;
|
|
486
486
|
if (frameId) {
|
|
487
487
|
return {
|
|
@@ -552,9 +552,9 @@ class TraceProcessor {
|
|
|
552
552
|
*/
|
|
553
553
|
static isScheduleableTask(evt) {
|
|
554
554
|
return evt.name === SCHEDULABLE_TASK_TITLE_LH ||
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
555
|
+
evt.name === SCHEDULABLE_TASK_TITLE_ALT1 ||
|
|
556
|
+
evt.name === SCHEDULABLE_TASK_TITLE_ALT2 ||
|
|
557
|
+
evt.name === SCHEDULABLE_TASK_TITLE_ALT3;
|
|
558
558
|
}
|
|
559
559
|
|
|
560
560
|
/**
|
|
@@ -563,7 +563,7 @@ class TraceProcessor {
|
|
|
563
563
|
*/
|
|
564
564
|
static isLCPEvent(evt) {
|
|
565
565
|
if (evt.name !== 'largestContentfulPaint::Invalidate' &&
|
|
566
|
-
|
|
566
|
+
evt.name !== 'largestContentfulPaint::Candidate') return false;
|
|
567
567
|
return Boolean(evt.args?.frame);
|
|
568
568
|
}
|
|
569
569
|
|
|
@@ -602,9 +602,10 @@ class TraceProcessor {
|
|
|
602
602
|
*
|
|
603
603
|
* @param {LH.TraceEvent[]} events
|
|
604
604
|
* @param {LH.TraceEvent} timeOriginEvent
|
|
605
|
+
* @param {string|undefined} mainFrameId
|
|
605
606
|
* @return {{lcp: LCPEvent | undefined, invalidated: boolean}}
|
|
606
607
|
*/
|
|
607
|
-
static computeValidLCPAllFrames(events, timeOriginEvent) {
|
|
608
|
+
static computeValidLCPAllFrames(events, timeOriginEvent, mainFrameId) {
|
|
608
609
|
const lcpEvents = events.filter(this.isLCPEvent).reverse();
|
|
609
610
|
|
|
610
611
|
/** @type {Map<string, LCPEvent>} */
|
|
@@ -628,6 +629,53 @@ class TraceProcessor {
|
|
|
628
629
|
}
|
|
629
630
|
}
|
|
630
631
|
|
|
632
|
+
// If no standard LCP candidate is found, try the UKM AllFramesEvents.
|
|
633
|
+
if (!maxLcpAcrossFrames) {
|
|
634
|
+
const ukmEvents = events.filter(
|
|
635
|
+
(e) =>
|
|
636
|
+
e.name.includes('LargestContentfulPaint') && e.name.includes('UKM')
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
// In the rare cases this whole fallback is necessary, the
|
|
640
|
+
// NavStartToLargestContentfulPaint::Candidate::AllFrames::UKM events are missing too.
|
|
641
|
+
// As a result, the only useful signal left is the AllFrames invalidates.
|
|
642
|
+
// Not ideal since they are 1 paint behind, but.. better than the dreaded
|
|
643
|
+
// NO_LCP error
|
|
644
|
+
const targetEventName =
|
|
645
|
+
'NavStartToLargestContentfulPaint::Invalidate::AllFrames::UKM';
|
|
646
|
+
const ukmInvalidates = ukmEvents.filter((e) => e.name === targetEventName);
|
|
647
|
+
|
|
648
|
+
if (ukmInvalidates.length > 0) {
|
|
649
|
+
ukmInvalidates.sort((a, b) => a.ts - b.ts);
|
|
650
|
+
const lastInvalidate = ukmInvalidates[ukmInvalidates.length - 1];
|
|
651
|
+
|
|
652
|
+
log.warn(
|
|
653
|
+
'TraceProcessor',
|
|
654
|
+
'LCP candidate missing, falling back to UKM Invalidate event.'
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
// Construct a mock LCP candidate event
|
|
658
|
+
maxLcpAcrossFrames = /** @type {LCPCandidateEvent} */ (
|
|
659
|
+
/** @type {unknown} */ ({
|
|
660
|
+
name: 'largestContentfulPaint::Candidate',
|
|
661
|
+
cat: 'loading',
|
|
662
|
+
ph: lastInvalidate.ph,
|
|
663
|
+
ts: lastInvalidate.ts,
|
|
664
|
+
pid: lastInvalidate.pid,
|
|
665
|
+
tid: lastInvalidate.tid,
|
|
666
|
+
args: {
|
|
667
|
+
frame: mainFrameId || 'main_frame', // Mocked frame ID
|
|
668
|
+
data: {
|
|
669
|
+
size: 1, // Don't know the actuall size so we assign it 1
|
|
670
|
+
isMainFrame: true,
|
|
671
|
+
isOutermostMainFrame: true,
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
})
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
631
679
|
return {
|
|
632
680
|
lcp: maxLcpAcrossFrames,
|
|
633
681
|
// LCP events were found, but final LCP event of every frame was an invalidate event.
|
|
@@ -669,7 +717,7 @@ class TraceProcessor {
|
|
|
669
717
|
* @param {LH.Trace} trace
|
|
670
718
|
* @param {{timeOriginDeterminationMethod?: TimeOriginDeterminationMethod}} [options]
|
|
671
719
|
* @return {LH.Artifacts.ProcessedTrace}
|
|
672
|
-
|
|
720
|
+
*/
|
|
673
721
|
static processTrace(trace, options) {
|
|
674
722
|
const {timeOriginDeterminationMethod = 'auto'} = options || {};
|
|
675
723
|
|
|
@@ -677,9 +725,9 @@ class TraceProcessor {
|
|
|
677
725
|
// *must* be stable to keep events correctly nested.
|
|
678
726
|
const keyEvents = this.filteredTraceSort(trace.traceEvents, e => {
|
|
679
727
|
return e.cat.includes('blink.user_timing') ||
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
728
|
+
e.cat.includes('loading') ||
|
|
729
|
+
e.cat.includes('devtools.timeline') ||
|
|
730
|
+
e.cat === '__metadata';
|
|
683
731
|
});
|
|
684
732
|
|
|
685
733
|
// Find the inspected frame
|
|
@@ -699,7 +747,7 @@ class TraceProcessor {
|
|
|
699
747
|
// Begin collection of frame tree information with TracingStartedInBrowser,
|
|
700
748
|
// which should be present even without navigations.
|
|
701
749
|
const tracingStartedFrames = keyEvents
|
|
702
|
-
|
|
750
|
+
.find(e => e.name === 'TracingStartedInBrowser')?.args?.data?.frames;
|
|
703
751
|
if (tracingStartedFrames) {
|
|
704
752
|
for (const frame of tracingStartedFrames) {
|
|
705
753
|
framesById.set(frame.frame, {
|
|
@@ -738,12 +786,18 @@ class TraceProcessor {
|
|
|
738
786
|
// Filter to just events matching the main frame ID, just to make sure.
|
|
739
787
|
/** @param {LH.TraceEvent} e */
|
|
740
788
|
function associatedToMainFrame(e) {
|
|
789
|
+
if (e.name === 'NavStartToLargestContentfulPaint::Invalidate::AllFrames::UKM') {
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
741
792
|
const frameId = TraceProcessor.getFrameId(e);
|
|
742
793
|
return frameId === mainFrameInfo.frameId;
|
|
743
794
|
}
|
|
744
795
|
|
|
745
796
|
/** @param {LH.TraceEvent} e */
|
|
746
797
|
function associatedToAllFrames(e) {
|
|
798
|
+
if (e.name.includes('LargestContentfulPaint') && e.name.includes('UKM')) {
|
|
799
|
+
return true;
|
|
800
|
+
}
|
|
747
801
|
const frameId = TraceProcessor.getFrameId(e);
|
|
748
802
|
return frameId ? inspectedTreeFrameIds.includes(frameId) : false;
|
|
749
803
|
}
|
|
@@ -805,9 +859,16 @@ class TraceProcessor {
|
|
|
805
859
|
* origin in addition to the standard microsecond monotonic timestamps.
|
|
806
860
|
* @param {LH.Artifacts.ProcessedTrace} processedTrace
|
|
807
861
|
* @return {LH.Artifacts.ProcessedNavigation}
|
|
808
|
-
|
|
862
|
+
*/
|
|
809
863
|
static processNavigation(processedTrace) {
|
|
810
|
-
const {
|
|
864
|
+
const {
|
|
865
|
+
frameEvents,
|
|
866
|
+
frameTreeEvents,
|
|
867
|
+
timeOriginEvt,
|
|
868
|
+
timings,
|
|
869
|
+
timestamps,
|
|
870
|
+
mainFrameInfo,
|
|
871
|
+
} = processedTrace;
|
|
811
872
|
|
|
812
873
|
// Compute the key frame timings for the main frame.
|
|
813
874
|
const frameTimings = this.computeNavigationTimingsForFrame(frameEvents, {timeOriginEvt});
|
|
@@ -822,7 +883,11 @@ class TraceProcessor {
|
|
|
822
883
|
}
|
|
823
884
|
|
|
824
885
|
// Compute LCP for all frames.
|
|
825
|
-
const lcpAllFramesEvt = this.computeValidLCPAllFrames(
|
|
886
|
+
const lcpAllFramesEvt = this.computeValidLCPAllFrames(
|
|
887
|
+
frameTreeEvents,
|
|
888
|
+
timeOriginEvt,
|
|
889
|
+
mainFrameInfo.frameId
|
|
890
|
+
).lcp;
|
|
826
891
|
|
|
827
892
|
/** @param {number} ts */
|
|
828
893
|
const getTiming = ts => (ts - timeOriginEvt.ts) / 1000;
|
|
@@ -950,7 +1015,7 @@ class TraceProcessor {
|
|
|
950
1015
|
* in addition to the standard microsecond monotonic timestamps.
|
|
951
1016
|
* @param {Array<LH.TraceEvent>} frameEvents
|
|
952
1017
|
* @param {{timeOriginEvt: LH.TraceEvent}} options
|
|
953
|
-
|
|
1018
|
+
*/
|
|
954
1019
|
static computeNavigationTimingsForFrame(frameEvents, options) {
|
|
955
1020
|
const {timeOriginEvt} = options;
|
|
956
1021
|
|
|
@@ -967,7 +1032,8 @@ class TraceProcessor {
|
|
|
967
1032
|
}
|
|
968
1033
|
|
|
969
1034
|
// This function accepts events spanning multiple frames, but this usage will only provide events from the main frame.
|
|
970
|
-
const
|
|
1035
|
+
const frameId = frameEvents.map(e => TraceProcessor.getFrameId(e)).find(Boolean);
|
|
1036
|
+
const lcpResult = this.computeValidLCPAllFrames(frameEvents, timeOriginEvt, frameId);
|
|
971
1037
|
|
|
972
1038
|
const load = frameEvents.find(e => e.name === 'loadEventEnd' && e.ts > timeOriginEvt.ts);
|
|
973
1039
|
const domContentLoaded = frameEvents.find(
|