lighthouse 11.1.0-dev.20231002 → 11.1.0-dev.20231003

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.
@@ -4,20 +4,6 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
4
4
  * @return {!Element}
5
5
  */
6
6
  _renderMetric(audit: LH.ReportResult.AuditRef): Element;
7
- /**
8
- * @param {LH.ReportResult.AuditRef} audit
9
- * @param {number} scale
10
- * @return {!Element}
11
- */
12
- _renderOpportunity(audit: LH.ReportResult.AuditRef, scale: number): Element;
13
- /**
14
- * Get an audit's wastedMs to sort the opportunity by, and scale the sparkline width
15
- * Opportunities with an error won't have a details object, so MIN_VALUE is returned to keep any
16
- * erroring opportunities last in sort order.
17
- * @param {LH.ReportResult.AuditRef} audit
18
- * @return {number}
19
- */
20
- _getWastedMs(audit: LH.ReportResult.AuditRef): number;
21
7
  /**
22
8
  * Get a link to the interactive scoring calculator with the metric values.
23
9
  * @param {LH.ReportResult.AuditRef[]} auditRefs
@@ -25,13 +11,25 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
25
11
  */
26
12
  _getScoringCalculatorHref(auditRefs: LH.ReportResult.AuditRef[]): string;
27
13
  /**
28
- * For performance, audits with no group should be a diagnostic or opportunity.
29
- * The audit details type will determine which of the two groups an audit is in.
14
+ * Returns true if the audit is a general performance insight (i.e. not a metric or hidden audit).
15
+ *
16
+ * @param {LH.ReportResult.AuditRef} audit
17
+ * @return {boolean}
18
+ */
19
+ _isPerformanceInsight(audit: LH.ReportResult.AuditRef): boolean;
20
+ /**
21
+ * Returns overallImpact and linearImpact for an audit.
22
+ * The overallImpact is determined by the audit saving's effect on the overall performance score.
23
+ * We use linearImpact to compare audits where their overallImpact is rounded down to 0.
30
24
  *
31
25
  * @param {LH.ReportResult.AuditRef} audit
32
- * @return {'load-opportunity'|'diagnostic'|null}
26
+ * @param {LH.ReportResult.AuditRef[]} metricAudits
27
+ * @return {{overallImpact: number, overallLinearImpact: number}}
33
28
  */
34
- _classifyPerformanceAudit(audit: LH.ReportResult.AuditRef): 'load-opportunity' | 'diagnostic' | null;
29
+ overallImpact(audit: LH.ReportResult.AuditRef, metricAudits: LH.ReportResult.AuditRef[]): {
30
+ overallImpact: number;
31
+ overallLinearImpact: number;
32
+ };
35
33
  /**
36
34
  * @param {LH.ReportResult.Category} category
37
35
  * @param {Object<string, LH.Result.ReportGroup>} groups
@@ -9,6 +9,7 @@
9
9
  import {CategoryRenderer} from './category-renderer.js';
10
10
  import {ReportUtils} from './report-utils.js';
11
11
  import {Globals} from './report-globals.js';
12
+ import {Util} from '../../shared/util.js';
12
13
  import {createGauge, updateGauge} from './explodey-gauge.js';
13
14
 
14
15
  export class PerformanceCategoryRenderer extends CategoryRenderer {
@@ -44,61 +45,6 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
44
45
  return element;
45
46
  }
46
47
 
47
- /**
48
- * @param {LH.ReportResult.AuditRef} audit
49
- * @param {number} scale
50
- * @return {!Element}
51
- */
52
- _renderOpportunity(audit, scale) {
53
- const oppTmpl = this.dom.createComponent('opportunity');
54
- const element = this.populateAuditValues(audit, oppTmpl);
55
- element.id = audit.result.id;
56
-
57
- if (!audit.result.details || audit.result.scoreDisplayMode === 'error') {
58
- return element;
59
- }
60
- const details = audit.result.details;
61
- if (details.overallSavingsMs === undefined) {
62
- return element;
63
- }
64
-
65
- // Overwrite the displayValue with opportunity's wastedMs
66
- // TODO: normalize this to one tagName.
67
- const displayEl =
68
- this.dom.find('span.lh-audit__display-text, div.lh-audit__display-text', element);
69
- const sparklineWidthPct = `${details.overallSavingsMs / scale * 100}%`;
70
- this.dom.find('div.lh-sparkline__bar', element).style.width = sparklineWidthPct;
71
- displayEl.textContent = Globals.i18n.formatSeconds(details.overallSavingsMs, 0.01);
72
-
73
- // Set [title] tooltips
74
- if (audit.result.displayValue) {
75
- const displayValue = audit.result.displayValue;
76
- this.dom.find('div.lh-load-opportunity__sparkline', element).title = displayValue;
77
- displayEl.title = displayValue;
78
- }
79
-
80
- return element;
81
- }
82
-
83
- /**
84
- * Get an audit's wastedMs to sort the opportunity by, and scale the sparkline width
85
- * Opportunities with an error won't have a details object, so MIN_VALUE is returned to keep any
86
- * erroring opportunities last in sort order.
87
- * @param {LH.ReportResult.AuditRef} audit
88
- * @return {number}
89
- */
90
- _getWastedMs(audit) {
91
- if (audit.result.details) {
92
- const details = audit.result.details;
93
- if (typeof details.overallSavingsMs !== 'number') {
94
- throw new Error('non-opportunity details passed to _getWastedMs');
95
- }
96
- return details.overallSavingsMs;
97
- } else {
98
- return Number.MIN_VALUE;
99
- }
100
- }
101
-
102
48
  /**
103
49
  * Get a link to the interactive scoring calculator with the metric values.
104
50
  * @param {LH.ReportResult.AuditRef[]} auditRefs
@@ -147,18 +93,55 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
147
93
  }
148
94
 
149
95
  /**
150
- * For performance, audits with no group should be a diagnostic or opportunity.
151
- * The audit details type will determine which of the two groups an audit is in.
96
+ * Returns true if the audit is a general performance insight (i.e. not a metric or hidden audit).
152
97
  *
153
98
  * @param {LH.ReportResult.AuditRef} audit
154
- * @return {'load-opportunity'|'diagnostic'|null}
99
+ * @return {boolean}
155
100
  */
156
- _classifyPerformanceAudit(audit) {
157
- if (audit.group) return null;
158
- if (audit.result.details?.overallSavingsMs !== undefined) {
159
- return 'load-opportunity';
101
+ _isPerformanceInsight(audit) {
102
+ return !audit.group;
103
+ }
104
+
105
+ /**
106
+ * Returns overallImpact and linearImpact for an audit.
107
+ * The overallImpact is determined by the audit saving's effect on the overall performance score.
108
+ * We use linearImpact to compare audits where their overallImpact is rounded down to 0.
109
+ *
110
+ * @param {LH.ReportResult.AuditRef} audit
111
+ * @param {LH.ReportResult.AuditRef[]} metricAudits
112
+ * @return {{overallImpact: number, overallLinearImpact: number}}
113
+ */
114
+ overallImpact(audit, metricAudits) {
115
+ if (!audit.result.metricSavings) {
116
+ return {overallImpact: 0, overallLinearImpact: 0};
117
+ }
118
+
119
+ let overallImpact = 0;
120
+ let overallLinearImpact = 0;
121
+ for (const [k, savings] of Object.entries(audit.result.metricSavings)) {
122
+ // Get metric savings for individual audit.
123
+ if (savings === undefined) continue;
124
+
125
+ // Get the metric data.
126
+ const mAudit = metricAudits.find(audit => audit.acronym === k);
127
+ if (!mAudit) continue;
128
+ if (mAudit.result.score === null) continue;
129
+
130
+ const mValue = mAudit.result.numericValue;
131
+ if (!mValue) continue;
132
+
133
+ const linearImpact = savings / mValue * mAudit.weight;
134
+ overallLinearImpact += linearImpact;
135
+
136
+ const scoringOptions = mAudit.result.scoringOptions;
137
+ if (!scoringOptions) continue;
138
+
139
+ const newMetricScore = Util.computeLogNormalScore(scoringOptions, mValue - savings);
140
+
141
+ const weightedMetricImpact = (newMetricScore - mAudit.result.score) * mAudit.weight;
142
+ overallImpact += weightedMetricImpact;
160
143
  }
161
- return 'diagnostic';
144
+ return {overallImpact, overallLinearImpact};
162
145
  }
163
146
 
164
147
  /**
@@ -228,53 +211,54 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
228
211
  filmstripEl && timelineEl.append(filmstripEl);
229
212
  }
230
213
 
231
- // Opportunities
232
- const opportunityAudits = category.auditRefs
233
- .filter(audit => this._classifyPerformanceAudit(audit) === 'load-opportunity')
234
- .filter(audit => !ReportUtils.showAsPassed(audit.result))
235
- .sort((auditA, auditB) => this._getWastedMs(auditB) - this._getWastedMs(auditA));
236
-
237
214
  const filterableMetrics = metricAudits.filter(a => !!a.relevantAudits);
238
215
  // TODO: only add if there are opportunities & diagnostics rendered.
239
216
  if (filterableMetrics.length) {
240
217
  this.renderMetricAuditFilter(filterableMetrics, element);
241
218
  }
242
219
 
243
- if (opportunityAudits.length) {
244
- // Scale the sparklines relative to savings, minimum 2s to not overstate small savings
245
- const minimumScale = 2000;
246
- const wastedMsValues = opportunityAudits.map(audit => this._getWastedMs(audit));
247
- const maxWaste = Math.max(...wastedMsValues);
248
- const scale = Math.max(Math.ceil(maxWaste / 1000) * 1000, minimumScale);
249
- const [groupEl, footerEl] = this.renderAuditGroup(groups['load-opportunities']);
250
- const tmpl = this.dom.createComponent('opportunityHeader');
251
-
252
- this.dom.find('.lh-load-opportunity__col--one', tmpl).textContent =
253
- strings.opportunityResourceColumnLabel;
254
- this.dom.find('.lh-load-opportunity__col--two', tmpl).textContent =
255
- strings.opportunitySavingsColumnLabel;
256
-
257
- const headerEl = this.dom.find('.lh-load-opportunity__header', tmpl);
258
- groupEl.insertBefore(headerEl, footerEl);
259
- opportunityAudits.forEach(item =>
260
- groupEl.insertBefore(this._renderOpportunity(item, scale), footerEl));
261
- groupEl.classList.add('lh-audit-group--load-opportunities');
262
- element.append(groupEl);
263
- }
264
-
265
220
  // Diagnostics
266
221
  const diagnosticAudits = category.auditRefs
267
- .filter(audit => this._classifyPerformanceAudit(audit) === 'diagnostic')
268
- .filter(audit => !ReportUtils.showAsPassed(audit.result))
269
- .sort((a, b) => {
270
- const scoreA = a.result.scoreDisplayMode === 'informative' ? 100 : Number(a.result.score);
271
- const scoreB = b.result.scoreDisplayMode === 'informative' ? 100 : Number(b.result.score);
272
- return scoreA - scoreB;
273
- });
274
-
275
- if (diagnosticAudits.length) {
222
+ .filter(audit => this._isPerformanceInsight(audit))
223
+ .filter(audit => !ReportUtils.showAsPassed(audit.result));
224
+
225
+ /** @type {Array<{auditRef:LH.ReportResult.AuditRef, overallImpact: number, overallLinearImpact: number, guidanceLevel: number}>} */
226
+ const auditImpacts = [];
227
+ diagnosticAudits.forEach(audit => {
228
+ const {
229
+ overallImpact: overallImpact,
230
+ overallLinearImpact: overallLinearImpact,
231
+ } = this.overallImpact(audit, metricAudits);
232
+ const guidanceLevel = audit.result.guidanceLevel ?? 1;
233
+ auditImpacts.push({auditRef: audit, overallImpact, overallLinearImpact, guidanceLevel});
234
+ });
235
+
236
+ auditImpacts.sort((a, b) => {
237
+ // Sort audits by impact, prioritizing those with a higher overallImpact first,
238
+ // then falling back to linearImpact, guidance level and score.
239
+ if (a.overallImpact !== b.overallImpact) return b.overallImpact - a.overallImpact;
240
+
241
+ if (
242
+ a.overallImpact === 0 && b.overallImpact === 0 &&
243
+ a.overallLinearImpact !== b.overallLinearImpact
244
+ ) {
245
+ return b.overallLinearImpact - a.overallLinearImpact;
246
+ }
247
+
248
+ if (a.guidanceLevel !== b.guidanceLevel) return b.guidanceLevel - a.guidanceLevel;
249
+
250
+ const scoreA = a.auditRef.result.scoreDisplayMode === 'informative'
251
+ ? 100
252
+ : Number(a.auditRef.result.score);
253
+ const scoreB = b.auditRef.result.scoreDisplayMode === 'informative'
254
+ ? 100
255
+ : Number(b.auditRef.result.score);
256
+ return scoreA - scoreB;
257
+ });
258
+
259
+ if (auditImpacts.length) {
276
260
  const [groupEl, footerEl] = this.renderAuditGroup(groups['diagnostics']);
277
- diagnosticAudits.forEach(item => groupEl.insertBefore(this.renderAudit(item), footerEl));
261
+ auditImpacts.forEach(item => groupEl.insertBefore(this.renderAudit(item.auditRef), footerEl));
278
262
  groupEl.classList.add('lh-audit-group--diagnostics');
279
263
  element.append(groupEl);
280
264
  }
@@ -282,7 +266,7 @@ export class PerformanceCategoryRenderer extends CategoryRenderer {
282
266
  // Passed audits
283
267
  const passedAudits = category.auditRefs
284
268
  .filter(audit =>
285
- this._classifyPerformanceAudit(audit) && ReportUtils.showAsPassed(audit.result));
269
+ this._isPerformanceInsight(audit) && ReportUtils.showAsPassed(audit.result));
286
270
 
287
271
  if (!passedAudits.length) return element;
288
272
 
@@ -774,7 +774,7 @@
774
774
  "message": "Syntax"
775
775
  },
776
776
  "core/audits/csp-xss.js | metaTagMessage": {
777
- "message": "The page contains a CSP defined in a <meta> tag. Consider moving the CSP to an HTTP header or defining another strict CSP in an HTTP header."
777
+ "message": "The page contains a CSP defined in a `<meta>` tag. Consider moving the CSP to an HTTP header or defining another strict CSP in an HTTP header."
778
778
  },
779
779
  "core/audits/csp-xss.js | noCsp": {
780
780
  "message": "No CSP found in enforcement mode"
@@ -1026,7 +1026,7 @@
1026
1026
  "message": "Page is loaded in an incognito window"
1027
1027
  },
1028
1028
  "core/audits/installable-manifest.js | manifest-display-not-supported": {
1029
- "message": "Manifest 'display' property must be one of 'standalone', 'fullscreen', or 'minimal-ui'"
1029
+ "message": "Manifest `display` property must be one of `standalone`, `fullscreen`, or `minimal-ui`"
1030
1030
  },
1031
1031
  "core/audits/installable-manifest.js | manifest-display-override-not-supported": {
1032
1032
  "message": "Manifest contains 'display_override' field, and the first supported display mode must be one of 'standalone', 'fullscreen', or 'minimal-ui'"
@@ -1038,7 +1038,7 @@
1038
1038
  "message": "Manifest URL changed while the manifest was being fetched."
1039
1039
  },
1040
1040
  "core/audits/installable-manifest.js | manifest-missing-name-or-short-name": {
1041
- "message": "Manifest does not contain a 'name' or 'short_name' field"
1041
+ "message": "Manifest does not contain a `name` or `short_name` field"
1042
1042
  },
1043
1043
  "core/audits/installable-manifest.js | manifest-missing-suitable-icon": {
1044
1044
  "message": "Manifest does not contain a suitable icon - PNG, SVG or WebP format of at least {value0} px is required, the sizes attribute must be set, and the purpose attribute, if set, must include \"any\"."
@@ -2211,25 +2211,25 @@
2211
2211
  "message": "Pages that use WebXR are not currently eligible for back/forward cache."
2212
2212
  },
2213
2213
  "core/lib/csp-evaluator.js | allowlistFallback": {
2214
- "message": "Consider adding https: and http: URL schemes (ignored by browsers supporting 'strict-dynamic') to be backward compatible with older browsers."
2214
+ "message": "Consider adding https: and http: URL schemes (ignored by browsers supporting `'strict-dynamic'`) to be backward compatible with older browsers."
2215
2215
  },
2216
2216
  "core/lib/csp-evaluator.js | deprecatedDisownOpener": {
2217
- "message": "disown-opener is deprecated since CSP3. Please, use the Cross-Origin-Opener-Policy header instead."
2217
+ "message": "`disown-opener` is deprecated since CSP3. Please, use the Cross-Origin-Opener-Policy header instead."
2218
2218
  },
2219
2219
  "core/lib/csp-evaluator.js | deprecatedReferrer": {
2220
- "message": "referrer is deprecated since CSP2. Please, use the Referrer-Policy header instead."
2220
+ "message": "`referrer` is deprecated since CSP2. Please, use the Referrer-Policy header instead."
2221
2221
  },
2222
2222
  "core/lib/csp-evaluator.js | deprecatedReflectedXSS": {
2223
- "message": "reflected-xss is deprecated since CSP2. Please, use the X-XSS-Protection header instead."
2223
+ "message": "`reflected-xss` is deprecated since CSP2. Please, use the X-XSS-Protection header instead."
2224
2224
  },
2225
2225
  "core/lib/csp-evaluator.js | missingBaseUri": {
2226
- "message": "Missing base-uri allows injected <base> tags to set the base URL for all relative URLs (e.g. scripts) to an attacker controlled domain. Consider setting base-uri to 'none' or 'self'."
2226
+ "message": "Missing `base-uri` allows injected `<base>` tags to set the base URL for all relative URLs (e.g. scripts) to an attacker controlled domain. Consider setting `base-uri` to `'none'` or `'self'`."
2227
2227
  },
2228
2228
  "core/lib/csp-evaluator.js | missingObjectSrc": {
2229
- "message": "Missing object-src allows the injection of plugins that execute unsafe scripts. Consider setting object-src to 'none' if you can."
2229
+ "message": "Missing `object-src` allows the injection of plugins that execute unsafe scripts. Consider setting `object-src` to `'none'` if you can."
2230
2230
  },
2231
2231
  "core/lib/csp-evaluator.js | missingScriptSrc": {
2232
- "message": "script-src directive is missing. This can allow the execution of unsafe scripts."
2232
+ "message": "`script-src` directive is missing. This can allow the execution of unsafe scripts."
2233
2233
  },
2234
2234
  "core/lib/csp-evaluator.js | missingSemicolon": {
2235
2235
  "message": "Did you forget the semicolon? {keyword} seems to be a directive, not a keyword."
@@ -2250,10 +2250,10 @@
2250
2250
  "message": "No CSP configures a reporting destination. This makes it difficult to maintain the CSP over time and monitor for any breakages."
2251
2251
  },
2252
2252
  "core/lib/csp-evaluator.js | reportToOnly": {
2253
- "message": "The reporting destination is only configured via the report-to directive. This directive is only supported in Chromium-based browsers so it is recommended to also use a report-uri directive."
2253
+ "message": "The reporting destination is only configured via the report-to directive. This directive is only supported in Chromium-based browsers so it is recommended to also use a `report-uri` directive."
2254
2254
  },
2255
2255
  "core/lib/csp-evaluator.js | strictDynamic": {
2256
- "message": "Host allowlists can frequently be bypassed. Consider using CSP nonces or hashes instead, along with 'strict-dynamic' if necessary."
2256
+ "message": "Host allowlists can frequently be bypassed. Consider using CSP nonces or hashes instead, along with `'strict-dynamic'` if necessary."
2257
2257
  },
2258
2258
  "core/lib/csp-evaluator.js | unknownDirective": {
2259
2259
  "message": "Unknown CSP directive."
@@ -2262,10 +2262,10 @@
2262
2262
  "message": "{keyword} seems to be an invalid keyword."
2263
2263
  },
2264
2264
  "core/lib/csp-evaluator.js | unsafeInline": {
2265
- "message": "'unsafe-inline' allows the execution of unsafe in-page scripts and event handlers. Consider using CSP nonces or hashes to allow scripts individually."
2265
+ "message": "`'unsafe-inline'` allows the execution of unsafe in-page scripts and event handlers. Consider using CSP nonces or hashes to allow scripts individually."
2266
2266
  },
2267
2267
  "core/lib/csp-evaluator.js | unsafeInlineFallback": {
2268
- "message": "Consider adding 'unsafe-inline' (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers."
2268
+ "message": "Consider adding `'unsafe-inline'` (ignored by browsers supporting nonces/hashes) to be backward compatible with older browsers."
2269
2269
  },
2270
2270
  "core/lib/deprecation-description.js | feature": {
2271
2271
  "message": "Check the feature status page for more details."
@@ -774,7 +774,7 @@
774
774
  "message": "Ŝýn̂t́âx́"
775
775
  },
776
776
  "core/audits/csp-xss.js | metaTagMessage": {
777
- "message": "T̂h́ê ṕâǵê ćôńt̂áîńŝ á ĈŚP̂ d́êf́îńêd́ îń â <ḿêt́â> t́âǵ. Ĉón̂śîd́êŕ m̂óv̂ín̂ǵ t̂h́ê ĆŜṔ t̂ó âń ĤT́T̂Ṕ ĥéâd́êŕ ôŕ d̂éf̂ín̂ín̂ǵ âńôt́ĥér̂ śt̂ŕîćt̂ ĆŜṔ îń âń ĤT́T̂Ṕ ĥéâd́êŕ."
777
+ "message": "T̂h́ê ṕâǵê ćôńt̂áîńŝ á ĈŚP̂ d́êf́îńêd́ îń â `<meta>` t́âǵ. Ĉón̂śîd́êŕ m̂óv̂ín̂ǵ t̂h́ê ĆŜṔ t̂ó âń ĤT́T̂Ṕ ĥéâd́êŕ ôŕ d̂éf̂ín̂ín̂ǵ âńôt́ĥér̂ śt̂ŕîćt̂ ĆŜṔ îń âń ĤT́T̂Ṕ ĥéâd́êŕ."
778
778
  },
779
779
  "core/audits/csp-xss.js | noCsp": {
780
780
  "message": "N̂ó ĈŚP̂ f́ôún̂d́ îń êńf̂ór̂ćêḿêńt̂ ḿôd́ê"
@@ -1026,7 +1026,7 @@
1026
1026
  "message": "P̂áĝé îś l̂óâd́êd́ îń âń îńĉóĝńît́ô ẃîńd̂óŵ"
1027
1027
  },
1028
1028
  "core/audits/installable-manifest.js | manifest-display-not-supported": {
1029
- "message": "M̂án̂íf̂éŝt́ 'd̂íŝṕl̂áŷ' ṕr̂óp̂ér̂t́ŷûś b́ê ón̂é ôf́ 'ŝt́âd̂ál̂n̂é', 'f̂úl̂ĺŝćéêń', ôŕ 'm̂ín̂ím̂ál̂-úî'"
1029
+ "message": "M̂án̂íf̂éŝt́ `display` p̂ŕôṕêŕt̂ý m̂úŝt́ b̂é ôńê f̂ `standalone`, `fullscreen`, ór̂ `minimal-ui`"
1030
1030
  },
1031
1031
  "core/audits/installable-manifest.js | manifest-display-override-not-supported": {
1032
1032
  "message": "M̂án̂íf̂éŝt́ ĉón̂t́âín̂ś 'd̂íŝṕl̂áŷ_óv̂ér̂ŕîd́ê' f́îél̂d́, âńd̂ t́ĥé f̂ír̂śt̂ śûṕp̂ór̂t́êd́ d̂íŝṕl̂áŷ ḿôd́ê ḿûśt̂ b́ê ón̂é ôf́ 'ŝt́âńd̂ál̂ón̂é', 'f̂úl̂ĺŝćr̂éêń', ôŕ 'm̂ín̂ím̂ál̂-úî'"
@@ -1038,7 +1038,7 @@
1038
1038
  "message": "M̂án̂íf̂éŝt́ ÛŔL̂ ćĥán̂ǵêd́ ŵh́îĺê t́ĥé m̂án̂íf̂éŝt́ ŵáŝ b́êín̂ǵ f̂ét̂ćĥéd̂."
1039
1039
  },
1040
1040
  "core/audits/installable-manifest.js | manifest-missing-name-or-short-name": {
1041
- "message": "M̂án̂íf̂éŝt́ d̂óêś n̂ót̂ ćôńt̂áîń â 'ńâḿê' ór̂ 'śĥór̂t́_n̂ám̂' f̂íêĺd̂"
1041
+ "message": "M̂án̂íf̂éŝt́ d̂óêś n̂ót̂ ćôńt̂áîń â `name` ór̂ `short_name` f́îél̂d́"
1042
1042
  },
1043
1043
  "core/audits/installable-manifest.js | manifest-missing-suitable-icon": {
1044
1044
  "message": "M̂án̂íf̂éŝt́ d̂óêś n̂ót̂ ćôńt̂áîń â śûít̂áb̂ĺê íĉón̂ - ṔN̂Ǵ, ŜV́Ĝ ór̂ Ẃêb́P̂ f́ôŕm̂át̂ óf̂ át̂ ĺêáŝt́ {value0} p̂x́ îś r̂éq̂úîŕêd́, t̂h́ê śîźêś ât́t̂ŕîb́ût́ê ḿûśt̂ b́ê śêt́, âńd̂ t́ĥé p̂úr̂ṕôśê át̂t́r̂íb̂út̂é, îf́ ŝét̂, ḿûśt̂ ín̂ćl̂úd̂é \"âńŷ\"."
@@ -2211,25 +2211,25 @@
2211
2211
  "message": "P̂áĝéŝ t́ĥát̂ úŝé Ŵéb̂X́R̂ ár̂é n̂ót̂ ćûŕr̂én̂t́l̂ý êĺîǵîb́l̂é f̂ór̂ b́âćk̂/f́ôŕŵár̂d́ ĉáĉh́ê."
2212
2212
  },
2213
2213
  "core/lib/csp-evaluator.js | allowlistFallback": {
2214
- "message": "Ĉón̂śîd́êŕ âd́d̂ín̂ǵ ĥt́t̂ṕŝ: án̂d́ ĥt́t̂ṕ: ÛŔL̂ śĉh́êḿêś (îǵn̂ór̂éd̂ b́ŷ b́r̂óŵśêŕŝ śûṕp̂ór̂t́îńĝ 'śt̂ŕîćt̂-d́ŷńâḿîć') t̂ó b̂é b̂áĉḱŵár̂ ĉóm̂ṕâîb́l̂é ŵít̂h́ ĺd̂ér̂ b́r̂óŵśêŕŝ."
2214
+ "message": "Ĉón̂śîd́êŕ âd́d̂ín̂ǵ ĥt́t̂ṕŝ: án̂d́ ĥt́t̂ṕ: ÛŔL̂ śĉh́êḿêś (îǵn̂ór̂éd̂ b́ŷ b́r̂óŵśêŕŝ śûṕp̂ór̂t́îńĝ `'strict-dynamic'`) t́ô b́ê b́âk̂ẃâŕd̂ ćôḿp̂át̂íb̂ĺê ẃît́ĥ ól̂êŕ b̂ŕôẃŝér̂ś."
2215
2215
  },
2216
2216
  "core/lib/csp-evaluator.js | deprecatedDisownOpener": {
2217
- "message": "d̂íŝóŵń-ôṕêńêŕ îś d̂ép̂ŕêćât́êd́ ŝín̂ćê ĆŜṔ3. P̂ĺêáŝé, ûśê t́ĥé Ĉŕôśŝ-Ór̂íĝín̂-Óp̂én̂ér̂-Ṕôĺîćŷ h́êád̂ér̂ ín̂śt̂éâd́."
2217
+ "message": "`disown-opener` îś d̂ép̂ŕêćât́êd́ ŝín̂ćê ĆŜṔ3. P̂ĺêáŝé, ûśê t́ĥé Ĉŕôśŝ-Ór̂íĝín̂-Óp̂én̂ér̂-Ṕôĺîćŷ h́êád̂ér̂ ín̂śt̂éâd́."
2218
2218
  },
2219
2219
  "core/lib/csp-evaluator.js | deprecatedReferrer": {
2220
- "message": "r̂éf̂ér̂ŕêŕ îś d̂ép̂ŕêćât́êd́ ŝín̂ćê ĆŜṔ2. P̂ĺêáŝé, ûśê t́ĥé R̂éf̂ér̂ŕêŕ-P̂ól̂íĉý ĥéâd́êŕ îńŝt́êád̂."
2220
+ "message": "`referrer` îś d̂ép̂ŕêćât́êd́ ŝín̂ćê ĆŜṔ2. P̂ĺêáŝé, ûśê t́ĥé R̂éf̂ér̂ŕêŕ-P̂ól̂íĉý ĥéâd́êŕ îńŝt́êád̂."
2221
2221
  },
2222
2222
  "core/lib/csp-evaluator.js | deprecatedReflectedXSS": {
2223
- "message": "r̂éf̂ĺêćt̂éd̂-x́ŝś îś d̂ép̂ŕêćât́êd́ ŝín̂ćê ĆŜṔ2. P̂ĺêáŝé, ûśê t́ĥé X̂-X́ŜŚ-P̂ŕôt́êćt̂íôń ĥéâd́êŕ îńŝt́êád̂."
2223
+ "message": "`reflected-xss` îś d̂ép̂ŕêćât́êd́ ŝín̂ćê ĆŜṔ2. P̂ĺêáŝé, ûśê t́ĥé X̂-X́ŜŚ-P̂ŕôt́êćt̂íôń ĥéâd́êŕ îńŝt́êád̂."
2224
2224
  },
2225
2225
  "core/lib/csp-evaluator.js | missingBaseUri": {
2226
- "message": "M̂íŝśîńĝ âśê-úr̂í âl̂óŵńĵéĉt́êd́ <b̂áé> t̂áĝś t̂ó ŝé t́ĥáŝŔô âĺêĺât́îv́R̂Ĺ (é.ĝ.ĉŕîṕt̂) t̂ ât́t̂áĉḱêón̂t́r̂ól̂ĺêd́ d̂óm̂áń.ón̂śîd́êét̂t́îĝ b́âśê-úr̂í t̂ó 'n̂ón̂é' ôŕ 'ŝél̂f́'."
2226
+ "message": "M̂íŝśîńĝ `base-uri` l̂ĺôẃŝ ín̂j́êćt̂d̂ `<base>` âǵŝ t́ô t́ t̂h́ê b́âśê ÚR̂Ĺ f̂ór̂ ál̂ĺ r̂él̂át̂ív̂é ÛŔL̂ś (ê.ǵ. ŝćr̂íp̂t́ŝ) t́ô án̂ át̂t́âćk̂ér̂ ćôńt̂ŕôĺl̂éd̂ ôḿâín̂. Ćôńŝíd̂ér̂ śêt́t̂ín̂ǵ `base-uri` t̂ó `'none'` ôŕ `'self'`."
2227
2227
  },
2228
2228
  "core/lib/csp-evaluator.js | missingObjectSrc": {
2229
- "message": "M̂íŝśîńĝ ób̂j́êćt̂-śr̂ć âĺl̂óŵh́n̂j́êćt̂íôĺûǵîńĥáx̂éĉút̂ńŝáf̂ćr̂íp̂ŝ.ôńŝd̂ér̂ śt́t̂ín̂ǵ ôb́ĵĉt́-ŝŕĉ t́ô 'ńôńê' íf̂ ýôú ĉán̂."
2229
+ "message": "M̂íŝśîńĝ `object-src` ál̂ĺôẃŝ t́ĥé îńĵéĉt́îón̂ óf̂ ṕl̂úĝín̂ś t̂h́ât́ êx́êćût́ê ún̂śâf́ê śĉŕîṕt̂ś. Ĉón̂śîd́êŕ ŝét̂t́îńĝ `object-src` t́ô `'none'` íf̂ ýôú ĉán̂."
2230
2230
  },
2231
2231
  "core/lib/csp-evaluator.js | missingScriptSrc": {
2232
- "message": "ŝćr̂íp̂t́-ŝŕĉ d́ŕêćt̂v̂é îś m̂íŝśîńĝ.ĥíâ âĺl̂óĥé êx́êćût́îón̂śâf́ĉŕîṕś."
2232
+ "message": "`script-src` d̂ír̂éĉt́îv́ê ŝ ḿîśŝín̂ǵ. T̂h́îś ĉán̂ ál̂ĺôẃ t̂h́ê ĉút̂íôń ôf́ ûńŝáf̂é ŝćr̂íp̂t́ŝ."
2233
2233
  },
2234
2234
  "core/lib/csp-evaluator.js | missingSemicolon": {
2235
2235
  "message": "D̂íd̂ ýôú f̂ór̂ǵêt́ t̂h́ê śêḿîćôĺôń? {keyword} ŝéêḿŝ t́ô b́ê á d̂ír̂éĉt́îv́ê, ńôt́ â ḱêýŵór̂d́."
@@ -2250,10 +2250,10 @@
2250
2250
  "message": "N̂ó ĈŚP̂ ćôńf̂íĝúr̂éŝ á r̂ép̂ór̂t́îńĝ d́êśt̂ín̂át̂íôń. T̂h́îś m̂ák̂éŝ ít̂ d́îf́f̂íĉúl̂t́ t̂ó m̂áîńt̂áîń t̂h́ê ĆŜṔ ôv́êŕ t̂ím̂é âńd̂ ḿôńît́ôŕ f̂ór̂ án̂ý b̂ŕêák̂áĝéŝ."
2251
2251
  },
2252
2252
  "core/lib/csp-evaluator.js | reportToOnly": {
2253
- "message": "T̂h́ê ŕêṕôŕt̂ín̂ǵ d̂éŝt́îńât́îón̂ íŝ ón̂ĺŷ ćôńf̂íĝúr̂éd̂ v́îá t̂h́ê ŕêṕôŕt̂-t́ô d́îŕêćt̂ív̂é. T̂h́îś d̂ír̂éĉt́îv́ê íŝ ón̂ĺŷ śûṕp̂ór̂t́êd́ îń Ĉh́r̂óm̂íûḿ-b̂áŝéd̂ b́r̂óŵśêŕŝ śô ít̂ íŝ ŕêćôḿm̂én̂d́êd́ t̂ó âĺŝó ûśê á r̂ép̂ór̂t́-ûŕî d́îŕêćt̂ív̂é."
2253
+ "message": "T̂h́ê ŕêṕôŕt̂ín̂ǵ d̂éŝt́îńât́îón̂ íŝ ón̂ĺŷ ćôńf̂íĝúr̂éd̂ v́îá t̂h́ê ŕêṕôŕt̂-t́ô d́îŕêćt̂ív̂é. T̂h́îś d̂ír̂éĉt́îv́ê íŝ ón̂ĺŷ śûṕp̂ór̂t́êd́ îń Ĉh́r̂óm̂íûḿ-b̂áŝéd̂ b́r̂óŵśêŕŝ śô ít̂ íŝ ŕêćôḿm̂én̂d́êd́ t̂ó âĺŝó ûśê á `report-uri` d̂ír̂éĉt́îv́ê."
2254
2254
  },
2255
2255
  "core/lib/csp-evaluator.js | strictDynamic": {
2256
- "message": "Ĥóŝt́ âĺl̂óŵĺîśt̂ś ĉán̂ f́r̂éq̂úêńt̂ĺŷ b́ê b́ŷṕâśŝéd̂. Ćôńŝíd̂ér̂ úŝín̂ǵ ĈŚP̂ ńôńĉéŝ ór̂ h́âśĥéŝ ín̂śt̂éâd́, âĺôńĝ ẃît́ĥ 'śt̂ŕîćt̂-d́ŷńâḿî' îf́ n̂éĉéŝśâŷ."
2256
+ "message": "Ĥóŝt́ âĺl̂óŵĺîśt̂ś ĉán̂ f́r̂éq̂úêńt̂ĺŷ b́ê b́ŷṕâśŝéd̂. Ćôńŝíd̂ér̂ úŝín̂ǵ ĈŚP̂ ńôńĉéŝ ór̂ h́âśĥéŝ ín̂śt̂éâd́, âĺôńĝ ẃît́ĥ `'strict-dynamic'` f̂ êćêśŝár̂ý."
2257
2257
  },
2258
2258
  "core/lib/csp-evaluator.js | unknownDirective": {
2259
2259
  "message": "Ûńk̂ńôẃn̂ ĆŜṔ d̂ír̂éĉt́îv́ê."
@@ -2262,10 +2262,10 @@
2262
2262
  "message": "{keyword} ŝéêḿŝ t́ô b́ê án̂ ín̂v́âĺîd́ k̂éŷẃôŕd̂."
2263
2263
  },
2264
2264
  "core/lib/csp-evaluator.js | unsafeInline": {
2265
- "message": "'ûńŝáf̂é-îńl̂ín̂é' âĺl̂óŵś t̂h́ê éx̂éĉút̂íôń ôf́ ûńŝáf̂é îń-p̂áĝé ŝćr̂íp̂t́ŝ án̂d́ êv́êńt̂ h́âńd̂ĺêŕŝ. Ćôńŝíd̂ér̂ úŝín̂ǵ ĈŚP̂ ńôńĉéŝ ór̂ h́âśĥéŝ t́ô ál̂ĺôẃ ŝćr̂íp̂t́ŝ ín̂d́îv́îd́ûál̂ĺŷ."
2265
+ "message": "`'unsafe-inline'` âĺl̂óŵś t̂h́ê éx̂éĉút̂íôń ôf́ ûńŝáf̂é îń-p̂áĝé ŝćr̂íp̂t́ŝ án̂d́ êv́êńt̂ h́âńd̂ĺêŕŝ. Ćôńŝíd̂ér̂ úŝín̂ǵ ĈŚP̂ ńôńĉéŝ ór̂ h́âśĥéŝ t́ô ál̂ĺôẃ ŝćr̂íp̂t́ŝ ín̂d́îv́îd́ûál̂ĺŷ."
2266
2266
  },
2267
2267
  "core/lib/csp-evaluator.js | unsafeInlineFallback": {
2268
- "message": "Ĉón̂śîd́êŕ âd́d̂ín̂ǵ 'ûńŝáf̂é-îńl̂ín̂é' (îǵn̂ór̂éd̂ b́ŷ b́r̂óŵśêŕŝ śûṕp̂ór̂t́îńĝ ńôńĉéŝ/h́âśĥéŝ) t́ô b́ê b́âćk̂ẃâŕd̂ ćôḿp̂át̂íb̂ĺê ẃît́ĥ ól̂d́êŕ b̂ŕôẃŝér̂ś."
2268
+ "message": "Ĉón̂śîd́êŕ âd́d̂ín̂ǵ `'unsafe-inline'` (îǵn̂ór̂éd̂ b́ŷ b́r̂óŵśêŕŝ śûṕp̂ór̂t́îńĝ ńôńĉéŝ/h́âśĥéŝ) t́ô b́ê b́âćk̂ẃâŕd̂ ćôḿp̂át̂íb̂ĺê ẃît́ĥ ól̂d́êŕ b̂ŕôẃŝér̂ś."
2269
2269
  },
2270
2270
  "core/lib/deprecation-description.js | feature": {
2271
2271
  "message": "Ĉh́êćk̂ t́ĥé f̂éât́ûŕê śt̂át̂úŝ ṕâǵê f́ôŕ m̂ór̂é d̂ét̂áîĺŝ."
package/shared/util.d.ts CHANGED
@@ -130,5 +130,27 @@ export class Util {
130
130
  lineNumber: number;
131
131
  truncated?: boolean | undefined;
132
132
  }[];
133
+ /**
134
+ * Computes a score between 0 and 1 based on the measured `value`. Score is determined by
135
+ * considering a log-normal distribution governed by two control points (the 10th
136
+ * percentile value and the median value) and represents the percentage of sites that are
137
+ * greater than `value`.
138
+ *
139
+ * Score characteristics:
140
+ * - within [0, 1]
141
+ * - rounded to two digits
142
+ * - value must meet or beat a controlPoint value to meet or exceed its percentile score:
143
+ * - value > median will give a score < 0.5; value ≤ median will give a score ≥ 0.5.
144
+ * - value > p10 will give a score < 0.9; value ≤ p10 will give a score ≥ 0.9.
145
+ * - values < p10 will get a slight boost so a score of 1 is achievable by a
146
+ * `value` other than those close to 0. Scores of > ~0.99524 end up rounded to 1.
147
+ * @param {{median: number, p10: number}} controlPoints
148
+ * @param {number} value
149
+ * @return {number}
150
+ */
151
+ static computeLogNormalScore(controlPoints: {
152
+ median: number;
153
+ p10: number;
154
+ }, value: number): number;
133
155
  }
134
156
  //# sourceMappingURL=util.d.ts.map
package/shared/util.js CHANGED
@@ -4,6 +4,8 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
+ import * as statistics from './statistics.js';
8
+
7
9
  /** @typedef {import('../types/lhr/audit-details').default.SnippetValue} SnippetValue */
8
10
 
9
11
  const ELLIPSIS = '\u2026';
@@ -362,6 +364,35 @@ class Util {
362
364
 
363
365
  return lines.filter(line => lineNumbersToKeep.has(line.lineNumber));
364
366
  }
367
+
368
+ /**
369
+ * Computes a score between 0 and 1 based on the measured `value`. Score is determined by
370
+ * considering a log-normal distribution governed by two control points (the 10th
371
+ * percentile value and the median value) and represents the percentage of sites that are
372
+ * greater than `value`.
373
+ *
374
+ * Score characteristics:
375
+ * - within [0, 1]
376
+ * - rounded to two digits
377
+ * - value must meet or beat a controlPoint value to meet or exceed its percentile score:
378
+ * - value > median will give a score < 0.5; value ≤ median will give a score ≥ 0.5.
379
+ * - value > p10 will give a score < 0.9; value ≤ p10 will give a score ≥ 0.9.
380
+ * - values < p10 will get a slight boost so a score of 1 is achievable by a
381
+ * `value` other than those close to 0. Scores of > ~0.99524 end up rounded to 1.
382
+ * @param {{median: number, p10: number}} controlPoints
383
+ * @param {number} value
384
+ * @return {number}
385
+ */
386
+ static computeLogNormalScore(controlPoints, value) {
387
+ let percentile = statistics.getLogNormalScore(controlPoints, value);
388
+ // Add a boost to scores of 90+, linearly ramping from 0 at 0.9 to half a
389
+ // point (0.005) at 1. Expands scores in (0.9, 1] to (0.9, 1.005], so more top
390
+ // scores will be a perfect 1 after the two-digit `Math.floor()` rounding below.
391
+ if (percentile > 0.9) { // getLogNormalScore ensures `percentile` can't exceed 1.
392
+ percentile += 0.05 * (percentile - 0.9);
393
+ }
394
+ return Math.floor(percentile * 100) / 100;
395
+ }
365
396
  }
366
397
 
367
398
  export {
package/types/audit.d.ts CHANGED
@@ -86,8 +86,10 @@ declare module Audit {
86
86
  details?: AuditDetails;
87
87
  /** If an audit encounters unusual execution circumstances, strings can be put in this optional array to add top-level warnings to the LHR. */
88
88
  runWarnings?: Array<IcuMessage>;
89
- /** [EXPERIMENTAL] Estimates of how much this audit affects various performance metrics. Values will be in the unit of the respective metrics. */
89
+ /** Estimates of how much this audit affects various performance metrics. Values will be in the unit of the respective metrics. */
90
90
  metricSavings?: MetricSavings;
91
+ /** Score details including p10 and median for calculating an audit's log-normal score. */
92
+ scoringOptions?: ScoreOptions;
91
93
  }
92
94
 
93
95
  /** The Audit.Product type for audits that do not return a `numericValue`. */
@@ -66,4 +66,13 @@ export interface Result {
66
66
  numericUnit?: string;
67
67
  /** Extra information about the page provided by some types of audits, in one of several possible forms that can be rendered in the HTML report. */
68
68
  details?: FormattedIcu<AuditDetails>;
69
+ /** Estimates of how much this audit affects various performance metrics. Values will be in the unit of the respective metrics. */
70
+ metricSavings?: MetricSavings
71
+ /** Score details including p10 and median for calculating an audit's log-normal score. */
72
+ scoringOptions?: {
73
+ p10: number;
74
+ median: number;
75
+ };
76
+ /** A number indicating how much guidance Lighthouse provides to solve the problem in this audit on a 1-3 scale. Higher means more guidance. */
77
+ guidanceLevel?: number;
69
78
  }
File without changes
File without changes