chrome-devtools-frontend
Version:
Chrome DevTools UI
475 lines (434 loc) • 17.5 kB
text/typescript
// Copyright 2024 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/components/request_link_icon/request_link_icon.js';
import * as i18n from '../../../core/i18n/i18n.js';
import type * as Platform from '../../../core/platform/platform.js';
import * as SDK from '../../../core/sdk/sdk.js';
import * as Helpers from '../../../models/trace/helpers/helpers.js';
import * as Trace from '../../../models/trace/trace.js';
import * as LegacyComponents from '../../../ui/legacy/components/utils/utils.js';
import * as UI from '../../../ui/legacy/legacy.js';
import * as Lit from '../../../ui/lit/lit.js';
import networkRequestDetailsStyles from './networkRequestDetails.css.js';
import networkRequestTooltipStyles from './networkRequestTooltip.css.js';
import {NetworkRequestTooltip} from './NetworkRequestTooltip.js';
import {colorForNetworkRequest} from './Utils.js';
const {html, render} = Lit;
const MAX_URL_LENGTH = 100;
const UIStrings = {
/**
* @description Text that refers to the network request method
*/
requestMethod: 'Request method',
/**
* @description Text that refers to the network request protocol
*/
protocol: 'Protocol',
/**
* @description Text to show the priority of an item
*/
priority: 'Priority',
/**
* @description Text used when referring to the data sent in a network request that is encoded as a particular file format.
*/
encodedData: 'Encoded data',
/**
* @description Text used to refer to the data sent in a network request that has been decoded.
*/
decodedBody: 'Decoded body',
/**
* @description Text in Timeline indicating that input has happened recently
*/
yes: 'Yes',
/**
* @description Text in Timeline indicating that input has not happened recently
*/
no: 'No',
/**
* @description Text to indicate to the user they are viewing an event representing a network request.
*/
networkRequest: 'Network request',
/**
* @description Text for the data source of a network request.
*/
fromCache: 'From cache',
/**
* @description Text used to show the mime-type of the data transferred with a network request (e.g. "application/json").
*/
mimeType: 'MIME type',
/**
* @description Text used to show the user that a request was served from the browser's in-memory cache.
*/
FromMemoryCache: ' (from memory cache)',
/**
* @description Text used to show the user that a request was served from the browser's file cache.
*/
FromCache: ' (from cache)',
/**
* @description Label for a network request indicating that it was a HTTP2 server push instead of a regular network request, in the Performance panel
*/
FromPush: ' (from push)',
/**
* @description Text used to show a user that a request was served from an installed, active service worker.
*/
FromServiceWorker: ' (from `service worker`)',
/**
* @description Text for the event initiated by another one
*/
initiatedBy: 'Initiated by',
/**
* @description Text that refers to if the network request is blocking
*/
blocking: 'Blocking',
/**
* @description Text that refers to if the network request is in-body parser render blocking
*/
inBodyParserBlocking: 'In-body parser blocking',
/**
* @description Text that refers to if the network request is render blocking
*/
renderBlocking: 'Render blocking',
/**
* @description Text to refer to a 3rd Party entity.
*/
entity: '3rd party',
/**
* @description Label for a column containing the names of timings (performance metric) taken in the server side application.
*/
serverTiming: 'Server timing',
/**
* @description Label for a column containing the values of timings (performance metric) taken in the server side application.
*/
time: 'Time',
/**
* @description Label for a column containing the description of timings (performance metric) taken in the server side application.
*/
description: 'Description',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/NetworkRequestDetails.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class NetworkRequestDetails extends UI.Widget.Widget {
#view: typeof DEFAULT_VIEW;
#request: Trace.Types.Events.SyntheticNetworkRequest|null = null;
#requestPreviewElements = new WeakMap<Trace.Types.Events.SyntheticNetworkRequest, HTMLElement>();
#entityMapper: Trace.EntityMapper.EntityMapper|null = null;
#target: SDK.Target.Target|null = null;
#linkifier: LegacyComponents.Linkifier.Linkifier|null = null;
#serverTimings: SDK.ServerTiming.ServerTiming[]|null = null;
#parsedTrace: Trace.TraceModel.ParsedTrace|null = null;
constructor(element?: HTMLElement, view = DEFAULT_VIEW) {
super(element);
this.#view = view;
this.requestUpdate();
}
set linkifier(linkifier: LegacyComponents.Linkifier.Linkifier|null) {
this.#linkifier = linkifier;
this.requestUpdate();
}
set parsedTrace(parsedTrace: Trace.TraceModel.ParsedTrace|null) {
this.#parsedTrace = parsedTrace;
this.requestUpdate();
}
set target(maybeTarget: SDK.Target.Target|null) {
this.#target = maybeTarget;
this.requestUpdate();
}
set request(event: Trace.Types.Events.SyntheticNetworkRequest) {
this.#request = event;
for (const header of event.args.data.responseHeaders ?? []) {
const headerName = header.name.toLocaleLowerCase();
// Some popular hosting providers like vercel or render get rid of
// Server-Timing headers added by users, so as a workaround we
// also support server timing headers with the `-test` suffix
// while this feature is experimental, to enable easier trials.
if (headerName === 'server-timing' || headerName === 'server-timing-test') {
header.name = 'server-timing';
this.#serverTimings = SDK.ServerTiming.ServerTiming.parseHeaders([header]);
break;
}
}
this.requestUpdate();
}
set entityMapper(mapper: Trace.EntityMapper.EntityMapper|null) {
this.#entityMapper = mapper;
this.requestUpdate();
}
override performUpdate(): Promise<void>|void {
this.#view(
{
request: this.#request,
previewElementsCache: this.#requestPreviewElements,
target: this.#target,
entityMapper: this.#entityMapper,
serverTimings: this.#serverTimings,
linkifier: this.#linkifier,
parsedTrace: this.#parsedTrace,
},
{}, this.contentElement);
}
}
export interface ViewInput {
request: Trace.Types.Events.SyntheticNetworkRequest|null;
target: SDK.Target.Target|null;
previewElementsCache: WeakMap<Trace.Types.Events.SyntheticNetworkRequest, HTMLElement>;
entityMapper: Trace.EntityMapper.EntityMapper|null;
serverTimings: SDK.ServerTiming.ServerTiming[]|null;
linkifier: LegacyComponents.Linkifier.Linkifier|null;
parsedTrace: Trace.TraceModel.ParsedTrace|null;
}
export const DEFAULT_VIEW: (
input: ViewInput, output: object, target: HTMLElement) => void = (input, _output, target) => {
if (!input.request) {
render(Lit.nothing, target);
return;
}
const {request} = input;
const {data} = request.args;
const redirectsHtml = NetworkRequestTooltip.renderRedirects(request);
// clang-format off
render(html`
<style>${networkRequestDetailsStyles}</style>
<style>${networkRequestTooltipStyles}</style>
<div class="network-request-details-content">
${renderTitle(input.request)}
${renderURL(input.request)}
<div class="network-request-details-cols">
${Lit.Directives.until(renderPreviewElement(
input.request,
input.target,
input.previewElementsCache,
))}
<div class="network-request-details-col">
${renderRow(i18nString(UIStrings.requestMethod), data.requestMethod)}
${renderRow(i18nString(UIStrings.protocol), data.protocol)}
${renderRow(i18nString(UIStrings.priority), NetworkRequestTooltip.renderPriorityValue(request))}
${renderRow(i18nString(UIStrings.mimeType), data.mimeType)}
${renderEncodedDataLength(request)}
${renderRow(i18nString(UIStrings.decodedBody), i18n.ByteUtilities.bytesToString(request.args.data.decodedBodyLength))}
${renderBlockingRow(request)}
${renderFromCache(request)}
${renderThirdPartyEntity(request, input.entityMapper)}
</div>
<div class="column-divider"></div>
<div class="network-request-details-col">
<div class="timing-rows">
${NetworkRequestTooltip.renderTimings(request)}
</div>
</div>
${renderServerTimings(input.serverTimings)}
${redirectsHtml ? html `
<div class="column-divider"></div>
<div class="network-request-details-col redirect-details">
${redirectsHtml}
</div>
` : Lit.nothing}
</div>
${renderInitiatedBy(request, input.parsedTrace, input.target, input.linkifier)}
</div>
</div>
`, target);
// clang-format on
};
function renderTitle(request: Trace.Types.Events.SyntheticNetworkRequest): Lit.TemplateResult {
const style = {
backgroundColor: `${colorForNetworkRequest(request)}`,
};
return html`
<div class="network-request-details-title">
<div style=${Lit.Directives.styleMap(style)}></div>
${i18nString(UIStrings.networkRequest)}
</div>
`;
}
function renderURL(request: Trace.Types.Events.SyntheticNetworkRequest): Lit.TemplateResult {
const options: LegacyComponents.Linkifier.LinkifyURLOptions = {
tabStop: true,
showColumnNumber: false,
inlineFrameIndex: 0,
maxLength: MAX_URL_LENGTH,
};
const linkifiedURL = LegacyComponents.Linkifier.Linkifier.linkifyURL(
request.args.data.url as Platform.DevToolsPath.UrlString, options);
// Potentially link to request within Network Panel
const networkRequest = SDK.TraceObject.RevealableNetworkRequest.create(request);
if (networkRequest) {
linkifiedURL.addEventListener('contextmenu', (event: MouseEvent) => {
const contextMenu = new UI.ContextMenu.ContextMenu(event);
contextMenu.appendApplicableItems(networkRequest);
void contextMenu.show();
});
// clang-format off
const urlElement = html`
${linkifiedURL}
<devtools-request-link-icon .data=${{request: networkRequest.networkRequest}}>
</devtools-request-link-icon>
`;
// clang-format on
return html`<div class="network-request-details-item">${urlElement}</div>`;
}
return html`<div class="network-request-details-item">${linkifiedURL}</div>`;
}
async function renderPreviewElement(
request: Trace.Types.Events.SyntheticNetworkRequest, target: SDK.Target.Target|null,
previewElementsCache: WeakMap<Trace.Types.Events.SyntheticNetworkRequest, HTMLElement>): Promise<Lit.LitTemplate> {
if (!request.args.data.url || !target) {
return Lit.nothing;
}
const url = request.args.data.url as Platform.DevToolsPath.UrlString;
if (!previewElementsCache.get(request)) {
const previewOpts = {
imageAltText:
LegacyComponents.ImagePreview.ImagePreview.defaultAltTextForImageURL(url as Platform.DevToolsPath.UrlString),
precomputedFeatures: undefined,
align: LegacyComponents.ImagePreview.Align.START,
hideFileData: true,
};
const previewElement = await LegacyComponents.ImagePreview.ImagePreview.build(
url as Platform.DevToolsPath.UrlString, false, previewOpts);
if (previewElement) {
previewElementsCache.set(request, previewElement);
}
}
const requestPreviewElement = previewElementsCache.get(request);
if (requestPreviewElement) {
// clang-format off
return html`
<div class="network-request-details-col">${requestPreviewElement}</div>
<div class="column-divider"></div>`;
// clang-format on
}
return Lit.nothing;
}
function renderRow(title: string, value?: string|Node|Lit.TemplateResult): Lit.LitTemplate {
if (!value) {
return Lit.nothing;
}
// clang-format off
return html`
<div class="network-request-details-row">
<div class="title">${title}</div>
<div class="value">${value}</div>
</div>`;
// clang-format on
}
function renderEncodedDataLength(request: Trace.Types.Events.SyntheticNetworkRequest): Lit.LitTemplate {
let lengthText = '';
if (request.args.data.syntheticData.isMemoryCached) {
lengthText += i18nString(UIStrings.FromMemoryCache);
} else if (request.args.data.syntheticData.isDiskCached) {
lengthText += i18nString(UIStrings.FromCache);
} else if (request.args.data.timing?.pushStart) {
lengthText += i18nString(UIStrings.FromPush);
}
if (request.args.data.fromServiceWorker) {
lengthText += i18nString(UIStrings.FromServiceWorker);
}
if (request.args.data.encodedDataLength || !lengthText) {
lengthText = `${i18n.ByteUtilities.bytesToString(request.args.data.encodedDataLength)}${lengthText}`;
}
return renderRow(i18nString(UIStrings.encodedData), lengthText);
}
function renderBlockingRow(request: Trace.Types.Events.SyntheticNetworkRequest): Lit.LitTemplate {
if (!Helpers.Network.isSyntheticNetworkRequestEventRenderBlocking(request)) {
return Lit.nothing;
}
let renderBlockingText;
switch (request.args.data.renderBlocking) {
case 'blocking':
renderBlockingText = UIStrings.renderBlocking;
break;
case 'in_body_parser_blocking':
renderBlockingText = UIStrings.inBodyParserBlocking;
break;
default:
// Shouldn't fall to this block, if so, this network request is not
// render blocking, so return null.
return Lit.nothing;
}
return renderRow(i18nString(UIStrings.blocking), renderBlockingText);
}
function renderFromCache(
request: Trace.Types.Events.SyntheticNetworkRequest,
): Lit.LitTemplate {
const cached = request.args.data.syntheticData.isMemoryCached || request.args.data.syntheticData.isDiskCached;
return renderRow(i18nString(UIStrings.fromCache), cached ? i18nString(UIStrings.yes) : i18nString(UIStrings.no));
}
function renderThirdPartyEntity(
request: Trace.Types.Events.SyntheticNetworkRequest,
entityMapper: Trace.EntityMapper.EntityMapper|null): Lit.LitTemplate {
if (!entityMapper) {
return Lit.nothing;
}
const entity = entityMapper.entityForEvent(request);
if (!entity) {
return Lit.nothing;
}
return renderRow(i18nString(UIStrings.entity), entity.name);
}
function renderServerTimings(timings: SDK.ServerTiming.ServerTiming[]|null): Lit.LitTemplate[]|Lit.LitTemplate {
if (!timings || timings.length === 0) {
return Lit.nothing;
}
// clang-format off
return html`
<div class="column-divider"></div>
<div class="network-request-details-col server-timings">
<div class="server-timing-column-header">${i18nString(UIStrings.serverTiming)}</div>
<div class="server-timing-column-header">${i18nString(UIStrings.description)}</div>
<div class="server-timing-column-header">${i18nString(UIStrings.time)}</div>
${timings.map(timing => {
const classes = timing.metric.startsWith('(c') ? 'synthetic value' : 'value';
return html`
<div class=${classes}>${timing.metric || '-'}</div>
<div class=${classes}>${timing.description || '-'}</div>
<div class=${classes}>${timing.value || '-'}</div>
`;
})}
</div>`;
// clang-format on
}
function renderInitiatedBy(
request: Trace.Types.Events.SyntheticNetworkRequest,
parsedTrace: Trace.TraceModel.ParsedTrace|null,
target: SDK.Target.Target|null,
linkifier: LegacyComponents.Linkifier.Linkifier|null,
): Lit.LitTemplate {
if (!linkifier) {
return Lit.nothing;
}
const hasStackTrace = Trace.Helpers.Trace.stackTraceInEvent(request) !== null;
let link: HTMLElement|null = null;
const options: LegacyComponents.Linkifier.LinkifyOptions = {
tabStop: true,
showColumnNumber: true,
inlineFrameIndex: 0,
};
// If we have a stack trace, that is the most reliable way to get the initiator data and display a link to the source.
if (hasStackTrace) {
const topFrame = Trace.Helpers.Trace.getStackTraceTopCallFrameInEventPayload(request) ?? null;
if (topFrame) {
link = linkifier.maybeLinkifyConsoleCallFrame(target, topFrame, options);
}
}
// If we do not, we can see if the network handler found an initiator and try to link by URL
const initiator = parsedTrace?.data.NetworkRequests.eventToInitiator.get(request);
if (initiator) {
link = linkifier.maybeLinkifyScriptLocation(
target,
null, // this would be the scriptId, but we don't have one. The linkifier will fallback to using the URL.
initiator.args.data.url as Platform.DevToolsPath.UrlString,
undefined, // line number
options);
}
if (!link) {
return Lit.nothing;
}
// clang-format off
return html`
<div class="network-request-details-item">
<div class="title">${i18nString(UIStrings.initiatedBy)}</div>
<div class="value focusable-outline">${link}</div>
</div>`;
// clang-format on
}