chrome-devtools-frontend
Version:
Chrome DevTools UI
575 lines (525 loc) • 21.7 kB
text/typescript
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../../../../ui/kit/kit.js';
import '../../../../ui/components/report_view/report_view.js';
import './MismatchedPreloadingGrid.js';
import * as Common from '../../../../core/common/common.js';
import * as i18n from '../../../../core/i18n/i18n.js';
import type * as Platform from '../../../../core/platform/platform.js';
import {assertNotNullOrUndefined} from '../../../../core/platform/platform.js';
import * as SDK from '../../../../core/sdk/sdk.js';
import * as Protocol from '../../../../generated/protocol.js';
import * as UI from '../../../../ui/legacy/legacy.js';
import {html, type LitTemplate, nothing, render} from '../../../../ui/lit/lit.js';
import * as VisualLogging from '../../../../ui/visual_logging/visual_logging.js';
import * as PreloadingHelper from '../helper/helper.js';
import {MismatchedPreloadingGrid, type MismatchedPreloadingGridData} from './MismatchedPreloadingGrid.js';
import preloadingGridStyles from './preloadingGrid.css.js';
import {prefetchFailureReason, prerenderFailureReason} from './PreloadingString.js';
import usedPreloadingStyles from './usedPreloadingView.css.js';
const UIStrings = {
/**
* @description Header for preloading status.
*/
speculativeLoadingStatusForThisPage: 'Speculative loading status for this page',
/**
* @description Label for failure reason of preloading
*/
detailsFailureReason: 'Failure reason',
/**
* @description Message that tells this page was prerendered.
*/
downgradedPrefetchUsed:
'The initiating page attempted to prerender this page\'s URL. The prerender failed, but the resulting response body was still used as a prefetch.',
/**
* @description Message that tells this page was prefetched.
*/
prefetchUsed: 'This page was successfully prefetched.',
/**
* @description Message that tells this page was prerendered.
*/
prerenderUsed: 'This page was successfully prerendered.',
/**
* @description Message that tells this page was prefetched.
*/
prefetchFailed:
'The initiating page attempted to prefetch this page\'s URL, but the prefetch failed, so a full navigation was performed instead.',
/**
* @description Message that tells this page was prerendered.
*/
prerenderFailed:
'The initiating page attempted to prerender this page\'s URL, but the prerender failed, so a full navigation was performed instead.',
/**
* @description Message that tells this page was not preloaded.
*/
noPreloads: 'The initiating page did not attempt to speculatively load this page\'s URL.',
/**
* @description Header for current URL.
*/
currentURL: 'Current URL',
/**
* @description Header for mismatched preloads.
*/
preloadedURLs: 'URLs being speculatively loaded by the initiating page',
/**
* @description Header for summary.
*/
speculationsInitiatedByThisPage: 'Speculations initiated by this page',
/**
* @description Link text to reveal rules.
*/
viewAllRules: 'View all speculation rules',
/**
* @description Link text to reveal preloads.
*/
viewAllSpeculations: 'View all speculations',
/**
* @description Link to learn more about Preloading
*/
learnMore: 'Learn more: Speculative loading on developer.chrome.com',
/**
* @description Header for the table of mismatched network request header.
*/
mismatchedHeadersDetail: 'Mismatched HTTP request headers',
/**
* @description Label for badge, indicating speculative load successfully used for this page.
*/
badgeSuccess: 'Success',
/**
* @description Label for badge, indicating speculative load failed for this page.
*/
badgeFailure: 'Failure',
/**
* @description Label for badge, indicating no speculative loads used for this page.
*/
badgeNoSpeculativeLoads: 'No speculative loads',
/**
* @description Label for badge, indicating how many not triggered speculations there are.
*/
badgeNotTriggeredWithCount: '{n, plural, =1 {# not triggered} other {# not triggered}}',
/**
* @description Label for badge, indicating how many in progress speculations there are.
*/
badgeInProgressWithCount: '{n, plural, =1 {# in progress} other {# in progress}}',
/**
* @description Label for badge, indicating how many succeeded speculations there are.
*/
badgeSuccessWithCount: '{n, plural, =1 {# success} other {# success}}',
/**
* @description Label for badge, indicating how many failed speculations there are.
*/
badgeFailureWithCount: '{n, plural, =1 {# failure} other {# failures}}',
/**
* @description The name of the HTTP request header.
*/
headerName: 'Header name',
/**
* @description The value of the HTTP request header in initial navigation.
*/
initialNavigationValue: 'Value in initial navigation',
/**
* @description The value of the HTTP request header in activation navigation.
*/
activationNavigationValue: 'Value in activation navigation',
/**
* @description The string to indicate the value of the header is missing.
*/
missing: '(missing)',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/application/preloading/components/UsedPreloadingView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
const {widgetConfig} = UI.Widget;
export interface UsedPreloadingViewData {
pageURL: Platform.DevToolsPath.UrlString;
previousAttempts: SDK.PreloadingModel.PreloadingAttempt[];
currentAttempts: SDK.PreloadingModel.PreloadingAttempt[];
}
export const enum UsedKind {
DOWNGRADED_PRERENDER_TO_PREFETCH_AND_USED = 'DowngradedPrerenderToPrefetchAndUsed',
PREFETCH_USED = 'PrefetchUsed',
PRERENDER_USED = 'PrerenderUsed',
PREFETCH_FAILED = 'PrefetchFailed',
PRERENDER_FAILED = 'PrerenderFailed',
NO_PRELOADS = 'NoPreloads',
}
type Badge = {
type: 'success',
count?: number,
}|{
type: 'failure',
count?: number,
}|{
type: 'neutral',
message: string,
};
interface MismatchedData {
pageURL: Platform.DevToolsPath.UrlString;
rows: MismatchedPreloadingGridData['rows'];
}
type AttemptWithMismatchedHeaders =
SDK.PreloadingModel.PrerenderAttempt|SDK.PreloadingModel.PrerenderUntilScriptAttempt;
interface SpeculativeLoadingStatusForThisPageData {
kind: UsedKind;
prefetch: SDK.PreloadingModel.PreloadingAttempt|undefined;
prerenderLike: SDK.PreloadingModel.PreloadingAttempt|undefined;
mismatchedData: MismatchedData|undefined;
attemptWithMismatchedHeaders: AttemptWithMismatchedHeaders|undefined;
}
function renderSpeculativeLoadingStatusForThisPageSections(
{kind, prefetch, prerenderLike, mismatchedData, attemptWithMismatchedHeaders}:
SpeculativeLoadingStatusForThisPageData): LitTemplate {
let badge: Badge;
let basicMessage: LitTemplate;
switch (kind) {
case UsedKind.DOWNGRADED_PRERENDER_TO_PREFETCH_AND_USED:
badge = {type: 'success'};
basicMessage = html`${i18nString(UIStrings.downgradedPrefetchUsed)}`;
break;
case UsedKind.PREFETCH_USED:
badge = {type: 'success'};
basicMessage = html`${i18nString(UIStrings.prefetchUsed)}`;
break;
case UsedKind.PRERENDER_USED:
badge = {type: 'success'};
basicMessage = html`${i18nString(UIStrings.prerenderUsed)}`;
break;
case UsedKind.PREFETCH_FAILED:
badge = {type: 'failure'};
basicMessage = html`${i18nString(UIStrings.prefetchFailed)}`;
break;
case UsedKind.PRERENDER_FAILED:
badge = {type: 'failure'};
basicMessage = html`${i18nString(UIStrings.prerenderFailed)}`;
break;
case UsedKind.NO_PRELOADS:
badge = {type: 'neutral', message: i18nString(UIStrings.badgeNoSpeculativeLoads)};
basicMessage = html`${i18nString(UIStrings.noPreloads)}`;
break;
}
let maybeFailureReasonMessage;
if (kind === UsedKind.PREFETCH_FAILED) {
assertNotNullOrUndefined(prefetch);
maybeFailureReasonMessage = prefetchFailureReason(prefetch as SDK.PreloadingModel.PrefetchAttempt);
} else if (kind === UsedKind.PRERENDER_FAILED || kind === UsedKind.DOWNGRADED_PRERENDER_TO_PREFETCH_AND_USED) {
assertNotNullOrUndefined(prerenderLike);
maybeFailureReasonMessage = prerenderFailureReason(
prerenderLike as SDK.PreloadingModel.PrerenderAttempt | SDK.PreloadingModel.PrerenderUntilScriptAttempt);
}
// Disabled until https://crbug.com/1079231 is fixed.
// clang-format off
return html`
<devtools-report-section-header>
${i18nString(UIStrings.speculativeLoadingStatusForThisPage)}
</devtools-report-section-header>
<devtools-report-section>
<div>
<div class="status-badge-container">
${renderBadge(badge)}
</div>
<div>
${basicMessage}
</div>
</div>
</devtools-report-section>
${maybeFailureReasonMessage !== undefined ? html`
<devtools-report-section-header>
${i18nString(UIStrings.detailsFailureReason)}
</devtools-report-section-header>
<devtools-report-section>
${maybeFailureReasonMessage}
</devtools-report-section>` : nothing}
${mismatchedData ? renderMismatchedSections(mismatchedData) : nothing}
${attemptWithMismatchedHeaders ?
renderMismatchedHTTPHeadersSections(attemptWithMismatchedHeaders) : nothing}`;
// clang-format on
}
function renderMismatchedSections(data: MismatchedData): LitTemplate {
// Disabled until https://crbug.com/1079231 is fixed.
// clang-format off
return html`
<devtools-report-section-header>
${i18nString(UIStrings.currentURL)}
</devtools-report-section-header>
<devtools-report-section>
<devtools-link
class="link devtools-link"
href=${data.pageURL}
jslogcontext="current-url"
>${data.pageURL}</devtools-link>
</devtools-report-section>
<devtools-report-section-header>
${i18nString(UIStrings.preloadedURLs)}
</devtools-report-section-header>
<devtools-report-section jslog=${VisualLogging.section('preloaded-urls')}>
<devtools-widget .widgetConfig=${widgetConfig(MismatchedPreloadingGrid, {data})}>
</devtools-widget>
</devtools-report-section>`;
// clang-format on
}
function renderMismatchedHTTPHeadersSections(attempt: AttemptWithMismatchedHeaders): LitTemplate {
// Disabled until https://crbug.com/1079231 is fixed.
// clang-format off
return html`
<devtools-report-section-header>
${i18nString(UIStrings.mismatchedHeadersDetail)}
</devtools-report-section-header>
<devtools-report-section>
<style>${preloadingGridStyles}</style>
<div class="preloading-container">
<devtools-data-grid striped inline>
<table>
<tr>
<th id="header-name" weight="30" sortable>
${i18nString(UIStrings.headerName)}
</th>
<th id="initial-value" weight="30" sortable>
${i18nString(UIStrings.initialNavigationValue)}
</th>
<th id="activation-value" weight="30" sortable>
${i18nString(UIStrings.activationNavigationValue)}
</th>
</tr>
${(attempt.mismatchedHeaders ?? []).map(mismatchedHeaders => html`
<tr>
<td>${mismatchedHeaders.headerName}</td>
<td>${mismatchedHeaders.initialValue ?? i18nString(UIStrings.missing)}</td>
<td>${mismatchedHeaders.activationValue ?? i18nString(UIStrings.missing)}</td>
</tr>
`)}
</table>
</devtools-data-grid>
</div>
</devtools-report-section>`;
// clang-format on
}
interface SpeculationsInitiatedByThisPageSummaryData {
badges: Badge[];
revealRuleSetView: () => void;
revealAttemptViewWithFilter: () => void;
}
interface ViewInput {
speculativeLoadingStatusData: SpeculativeLoadingStatusForThisPageData;
speculationsInitiatedSummaryData: SpeculationsInitiatedByThisPageSummaryData;
}
function renderSpeculationsInitiatedByThisPageSummarySections(
{badges, revealRuleSetView, revealAttemptViewWithFilter}: SpeculationsInitiatedByThisPageSummaryData): LitTemplate {
// Disabled until https://crbug.com/1079231 is fixed.
// clang-format off
return html`
<devtools-report-section-header>
${i18nString(UIStrings.speculationsInitiatedByThisPage)}
</devtools-report-section-header>
<devtools-report-section>
<div>
<div class="status-badge-container">
${badges.map(renderBadge)}
</div>
<div class="reveal-links">
<button class="link devtools-link" @click=${revealRuleSetView}
jslog=${VisualLogging.action('view-all-rules').track({click: true})}>
${i18nString(UIStrings.viewAllRules)}
</button>
・
<button class="link devtools-link" @click=${revealAttemptViewWithFilter}
jslog=${VisualLogging.action('view-all-speculations').track({click: true})}>
${i18nString(UIStrings.viewAllSpeculations)}
</button>
</div>
</div>
</devtools-report-section>`;
// clang-format on
}
function renderBadge(config: Badge): LitTemplate {
const badge = (klass: string, iconName: string, message: string): LitTemplate => {
// Disabled until https://crbug.com/1079231 is fixed.
// clang-format off
return html`
<span class=${klass}>
<devtools-icon name=${iconName}></devtools-icon>
<span>
${message}
</span>
</span>
`;
// clang-format on
};
switch (config.type) {
case 'success': {
let message;
if (config.count === undefined) {
message = i18nString(UIStrings.badgeSuccess);
} else {
message = i18nString(UIStrings.badgeSuccessWithCount, {n: config.count});
}
return badge('status-badge status-badge-success', 'check-circle', message);
}
case 'failure': {
let message;
if (config.count === undefined) {
message = i18nString(UIStrings.badgeFailure);
} else {
message = i18nString(UIStrings.badgeFailureWithCount, {n: config.count});
}
return badge('status-badge status-badge-failure', 'cross-circle', message);
}
case 'neutral':
return badge('status-badge status-badge-neutral', 'clear', config.message);
}
}
type View = (input: ViewInput, output: undefined, target: HTMLElement|ShadowRoot) => void;
const DEFAULT_VIEW: View = (input, _output, target) => {
// Disabled until https://crbug.com/1079231 is fixed.
// clang-format off
render(html`
<style>${usedPreloadingStyles}</style>
<devtools-report>
${renderSpeculativeLoadingStatusForThisPageSections(input.speculativeLoadingStatusData)}
<devtools-report-divider></devtools-report-divider>
${renderSpeculationsInitiatedByThisPageSummarySections(input.speculationsInitiatedSummaryData)}
<devtools-report-divider></devtools-report-divider>
<devtools-report-section>
<devtools-link
class="link devtools-link"
href=${'https://developer.chrome.com/blog/prerender-pages/'}
jslogcontext="learn-more"
>${i18nString(UIStrings.learnMore)}</devtools-link>
</devtools-report-section>
</devtools-report>`, target);
// clang-format on
};
/**
* TODO(kenoss): Rename this class and file once https://crrev.com/c/4933567 landed.
* This also shows summary of speculations initiated by this page.
**/
export class UsedPreloadingView extends UI.Widget.VBox {
readonly #view: View;
constructor(view = DEFAULT_VIEW) {
super({useShadowDom: true});
this.#view = view;
}
#data: UsedPreloadingViewData = {
pageURL: '' as Platform.DevToolsPath.UrlString,
previousAttempts: [],
currentAttempts: [],
};
set data(data: UsedPreloadingViewData) {
this.#data = data;
this.requestUpdate();
}
override performUpdate(): void {
const viewInput: ViewInput = {
speculativeLoadingStatusData: this.#getSpeculativeLoadingStatusForThisPageData(),
speculationsInitiatedSummaryData: this.#getSpeculationsInitiatedByThisPageSummaryData(),
};
this.#view(viewInput, undefined, this.contentElement);
}
#isPrerenderLike(speculationAction: Protocol.Preload.SpeculationAction): boolean {
return [
Protocol.Preload.SpeculationAction.Prerender, Protocol.Preload.SpeculationAction.PrerenderUntilScript
].includes(speculationAction);
}
#isPrerenderAttempt(attempt: SDK.PreloadingModel.PreloadingAttempt): attempt is AttemptWithMismatchedHeaders {
return this.#isPrerenderLike(attempt.action);
}
#getSpeculativeLoadingStatusForThisPageData(): SpeculativeLoadingStatusForThisPageData {
const pageURL = Common.ParsedURL.ParsedURL.urlWithoutHash(this.#data.pageURL);
const forThisPage = this.#data.previousAttempts.filter(
attempt => Common.ParsedURL.ParsedURL.urlWithoutHash(attempt.key.url) === pageURL);
const prefetch =
forThisPage.filter(attempt => attempt.key.action === Protocol.Preload.SpeculationAction.Prefetch)[0];
const prerenderLike = forThisPage.filter(attempt => this.#isPrerenderLike(attempt.action))[0];
let kind = UsedKind.NO_PRELOADS;
// Prerender -> prefetch downgrade case
//
// This code does not handle the case SpecRules designate these preloads rather than prerenderer automatically downgrade prerendering.
// TODO(https://crbug.com/1410709): Improve this logic once automatic downgrade implemented.
if (prerenderLike?.status === SDK.PreloadingModel.PreloadingStatus.FAILURE &&
prefetch?.status === SDK.PreloadingModel.PreloadingStatus.SUCCESS) {
kind = UsedKind.DOWNGRADED_PRERENDER_TO_PREFETCH_AND_USED;
} else if (prefetch?.status === SDK.PreloadingModel.PreloadingStatus.SUCCESS) {
kind = UsedKind.PREFETCH_USED;
} else if (prerenderLike?.status === SDK.PreloadingModel.PreloadingStatus.SUCCESS) {
kind = UsedKind.PRERENDER_USED;
} else if (prefetch?.status === SDK.PreloadingModel.PreloadingStatus.FAILURE) {
kind = UsedKind.PREFETCH_FAILED;
} else if (prerenderLike?.status === SDK.PreloadingModel.PreloadingStatus.FAILURE) {
kind = UsedKind.PRERENDER_FAILED;
} else {
kind = UsedKind.NO_PRELOADS;
}
return {
kind,
prefetch,
prerenderLike,
mismatchedData: this.#getMismatchedData(kind),
attemptWithMismatchedHeaders: this.#getAttemptWithMismatchedHeaders(),
};
}
#getMismatchedData(kind: UsedKind): MismatchedData|undefined {
if (kind !== UsedKind.NO_PRELOADS || this.#data.previousAttempts.length === 0) {
return undefined;
}
const rows = this.#data.previousAttempts.map(attempt => {
return {
url: attempt.key.url,
action: attempt.key.action,
status: attempt.status,
};
});
return {
pageURL: this.#data.pageURL,
rows,
};
}
#getAttemptWithMismatchedHeaders(): AttemptWithMismatchedHeaders|undefined {
const attempt = this.#data.previousAttempts.find(
attempt => this.#isPrerenderAttempt(attempt) && attempt.mismatchedHeaders !== null) as
SDK.PreloadingModel.PrerenderAttempt |
SDK.PreloadingModel.PrerenderUntilScriptAttempt | undefined;
if (!attempt?.mismatchedHeaders) {
return undefined;
}
if (attempt.key.url !== this.#data.pageURL) {
// This place should never be reached since mismatched headers is reported only if the activation is attempted.
// TODO(crbug.com/1456673): remove this check once DevTools support embedder-triggered prerender or prerender
// supports non-vary-search.
throw new Error('unreachable');
}
return attempt;
}
#getSpeculationsInitiatedByThisPageSummaryData(): SpeculationsInitiatedByThisPageSummaryData {
const count = this.#data.currentAttempts.reduce((acc, attempt) => {
acc.set(attempt.status, (acc.get(attempt.status) ?? 0) + 1);
return acc;
}, new Map());
const notTriggeredCount = count.get(SDK.PreloadingModel.PreloadingStatus.NOT_TRIGGERED) ?? 0;
const readyCount = count.get(SDK.PreloadingModel.PreloadingStatus.READY) ?? 0;
const failureCount = count.get(SDK.PreloadingModel.PreloadingStatus.FAILURE) ?? 0;
const inProgressCount = (count.get(SDK.PreloadingModel.PreloadingStatus.PENDING) ?? 0) +
(count.get(SDK.PreloadingModel.PreloadingStatus.RUNNING) ?? 0);
const badges: Badge[] = [];
if (this.#data.currentAttempts.length === 0) {
badges.push({type: 'neutral', message: i18nString(UIStrings.badgeNoSpeculativeLoads)});
}
if (notTriggeredCount > 0) {
badges.push({type: 'neutral', message: i18nString(UIStrings.badgeNotTriggeredWithCount, {n: notTriggeredCount})});
}
if (inProgressCount > 0) {
badges.push({type: 'neutral', message: i18nString(UIStrings.badgeInProgressWithCount, {n: inProgressCount})});
}
if (readyCount > 0) {
badges.push({type: 'success', count: readyCount});
}
if (failureCount > 0) {
badges.push({type: 'failure', count: failureCount});
}
const revealRuleSetView = (): void => {
void Common.Revealer.reveal(new PreloadingHelper.PreloadingForward.RuleSetView(null));
};
const revealAttemptViewWithFilter = (): void => {
void Common.Revealer.reveal(new PreloadingHelper.PreloadingForward.AttemptViewWithFilter(null));
};
return {badges, revealRuleSetView, revealAttemptViewWithFilter};
}
}