chrome-devtools-frontend
Version:
Chrome DevTools UI
436 lines (403 loc) • 16.9 kB
text/typescript
// Copyright 2024 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable rulesdir/no-lit-render-outside-of-view */
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 type * as TimelineUtils from '../utils/utils.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} = 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 HTMLElement {
readonly #shadow = this.attachShadow({mode: 'open'});
#networkRequest: Trace.Types.Events.SyntheticNetworkRequest|null = null;
#maybeTarget: SDK.Target.Target|null = null;
#requestPreviewElements = new WeakMap<Trace.Types.Events.SyntheticNetworkRequest, HTMLElement>();
#linkifier: LegacyComponents.Linkifier.Linkifier;
#parsedTrace: Trace.Handlers.Types.ParsedTrace|null = null;
#entityMapper: TimelineUtils.EntityMapper.EntityMapper|null = null;
#serverTimings: SDK.ServerTiming.ServerTiming[]|null = null;
constructor(linkifier: LegacyComponents.Linkifier.Linkifier) {
super();
this.#linkifier = linkifier;
}
async setData(
parsedTrace: Trace.Handlers.Types.ParsedTrace, networkRequest: Trace.Types.Events.SyntheticNetworkRequest,
maybeTarget: SDK.Target.Target|null, entityMapper: TimelineUtils.EntityMapper.EntityMapper|null): Promise<void> {
if (this.#networkRequest === networkRequest && parsedTrace === this.#parsedTrace) {
return;
}
this.#parsedTrace = parsedTrace;
this.#networkRequest = networkRequest;
this.#maybeTarget = maybeTarget;
this.#entityMapper = entityMapper;
this.#serverTimings = null;
for (const header of networkRequest.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;
}
}
await this.#render();
}
#renderTitle(): Lit.TemplateResult|null {
if (!this.#networkRequest) {
return null;
}
const style = {
backgroundColor: `${colorForNetworkRequest(this.#networkRequest)}`,
};
return html`
<div class="network-request-details-title">
<div style=${Lit.Directives.styleMap(style)}></div>
${i18nString(UIStrings.networkRequest)}
</div>
`;
}
#renderRow(title: string, value?: string|Node|Lit.TemplateResult): Lit.TemplateResult|null {
if (!value) {
return null;
}
// 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
}
#renderServerTimings(): Lit.LitTemplate[]|Lit.LitTemplate {
if (!this.#serverTimings) {
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>
${this.#serverTimings.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
}
#renderURL(): Lit.TemplateResult|null {
if (!this.#networkRequest) {
return null;
}
const options: LegacyComponents.Linkifier.LinkifyURLOptions = {
tabStop: true,
showColumnNumber: false,
inlineFrameIndex: 0,
maxLength: MAX_URL_LENGTH,
};
const linkifiedURL = LegacyComponents.Linkifier.Linkifier.linkifyURL(
this.#networkRequest.args.data.url as Platform.DevToolsPath.UrlString, options);
// Potentially link to request within Network Panel
const networkRequest = SDK.TraceObject.RevealableNetworkRequest.create(this.#networkRequest);
if (networkRequest) {
linkifiedURL.addEventListener('contextmenu', (event: MouseEvent) => {
if (!this.#networkRequest) {
return;
}
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>`;
}
#renderFromCache(): Lit.TemplateResult|null {
if (!this.#networkRequest) {
return null;
}
const cached = this.#networkRequest.args.data.syntheticData.isMemoryCached ||
this.#networkRequest.args.data.syntheticData.isDiskCached;
return this.#renderRow(
i18nString(UIStrings.fromCache), cached ? i18nString(UIStrings.yes) : i18nString(UIStrings.no));
}
#renderThirdPartyEntity(): Lit.TemplateResult|null {
if (!this.#entityMapper || !this.#networkRequest) {
return null;
}
const entity = this.#entityMapper.entityForEvent(this.#networkRequest);
if (!entity) {
return null;
}
return this.#renderRow(i18nString(UIStrings.entity), entity.name);
}
#renderEncodedDataLength(): Lit.TemplateResult|null {
if (!this.#networkRequest) {
return null;
}
let lengthText = '';
if (this.#networkRequest.args.data.syntheticData.isMemoryCached) {
lengthText += i18nString(UIStrings.FromMemoryCache);
} else if (this.#networkRequest.args.data.syntheticData.isDiskCached) {
lengthText += i18nString(UIStrings.FromCache);
} else if (this.#networkRequest.args.data.timing?.pushStart) {
lengthText += i18nString(UIStrings.FromPush);
}
if (this.#networkRequest.args.data.fromServiceWorker) {
lengthText += i18nString(UIStrings.FromServiceWorker);
}
if (this.#networkRequest.args.data.encodedDataLength || !lengthText) {
lengthText = `${i18n.ByteUtilities.bytesToString(this.#networkRequest.args.data.encodedDataLength)}${lengthText}`;
}
return this.#renderRow(i18nString(UIStrings.encodedData), lengthText);
}
#renderInitiatedBy(): Lit.TemplateResult|null {
if (!this.#networkRequest) {
return null;
}
const hasStackTrace = Trace.Helpers.Trace.stackTraceInEvent(this.#networkRequest) !== null;
let link: HTMLElement|null = null;
// 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.getZeroIndexedStackTraceInEventPayload(this.#networkRequest)?.at(0) ?? null;
if (topFrame) {
link = this.#linkifier.maybeLinkifyConsoleCallFrame(
this.#maybeTarget, topFrame, {tabStop: true, inlineFrameIndex: 0, showColumnNumber: true});
}
}
// If we do not, we can see if the network handler found an initiator and try to link by URL
const initiator = this.#parsedTrace?.NetworkRequests.eventToInitiator.get(this.#networkRequest);
if (initiator) {
link = this.#linkifier.maybeLinkifyScriptLocation(
this.#maybeTarget,
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
);
}
if (!link) {
return null;
}
// clang-format off
return html`
<div class="network-request-details-item">
<div class="title">${i18nString(UIStrings.initiatedBy)}</div>
<div class="value">${link}</div>
</div>`;
// clang-format on
}
#renderBlockingRow(): Lit.TemplateResult|null {
if (!this.#networkRequest || !Helpers.Network.isSyntheticNetworkRequestEventRenderBlocking(this.#networkRequest)) {
return null;
}
let renderBlockingText;
switch (this.#networkRequest.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 null;
}
return this.#renderRow(i18nString(UIStrings.blocking), renderBlockingText);
}
async #renderPreviewElement(): Promise<Lit.TemplateResult|null> {
if (!this.#networkRequest || !this.#networkRequest.args.data.url || !this.#maybeTarget) {
return null;
}
if (!this.#requestPreviewElements.get(this.#networkRequest)) {
const previewOpts = {
imageAltText: LegacyComponents.ImagePreview.ImagePreview.defaultAltTextForImageURL(
this.#networkRequest.args.data.url as Platform.DevToolsPath.UrlString),
precomputedFeatures: undefined,
align: LegacyComponents.ImagePreview.Align.START,
hideFileData: true,
};
const previewElement = await LegacyComponents.ImagePreview.ImagePreview.build(
this.#networkRequest.args.data.url as Platform.DevToolsPath.UrlString, false, previewOpts);
previewElement && this.#requestPreviewElements.set(this.#networkRequest, previewElement);
}
const requestPreviewElement = this.#requestPreviewElements.get(this.#networkRequest);
if (requestPreviewElement) {
// clang-format off
return html`
<div class="network-request-details-col">${requestPreviewElement}</div>
<div class="column-divider"></div>`;
// clang-format on
}
return null;
}
async #render(): Promise<void> {
if (!this.#networkRequest) {
return;
}
const networkData = this.#networkRequest.args.data;
const redirectsHtml = NetworkRequestTooltip.renderRedirects(this.#networkRequest);
// clang-format off
const output = html`
<style>${networkRequestDetailsStyles}</style>
<style>${networkRequestTooltipStyles}</style>
<div class="network-request-details-content">
${this.#renderTitle()}
${this.#renderURL()}
<div class="network-request-details-cols">
${await this.#renderPreviewElement()}
<div class="network-request-details-col">
${this.#renderRow(i18nString(UIStrings.requestMethod), networkData.requestMethod)}
${this.#renderRow(i18nString(UIStrings.protocol), networkData.protocol)}
${this.#renderRow(i18nString(UIStrings.priority), NetworkRequestTooltip.renderPriorityValue(this.#networkRequest))}
${this.#renderRow(i18nString(UIStrings.mimeType), networkData.mimeType)}
${this.#renderEncodedDataLength()}
${this.#renderRow(i18nString(UIStrings.decodedBody), i18n.ByteUtilities.bytesToString(this.#networkRequest.args.data.decodedBodyLength))}
${this.#renderBlockingRow()}
${this.#renderFromCache()}
${this.#renderThirdPartyEntity()}
</div>
<div class="column-divider"></div>
<div class="network-request-details-col">
<div class="timing-rows">
${NetworkRequestTooltip.renderTimings(this.#networkRequest)}
</div>
</div>
${this.#renderServerTimings()}
${redirectsHtml ? html `
<div class="column-divider"></div>
<div class="network-request-details-col redirect-details">
${redirectsHtml}
</div>
` : Lit.nothing}
</div>
${this.#renderInitiatedBy()}
</div>
`; // The last items are outside the 2 column layout because InitiatedBy can be very wide
// clang-format on
Lit.render(output, this.#shadow, {host: this});
}
}
declare global {
interface HTMLElementTagNameMap {
'devtools-performance-network-request-details': NetworkRequestDetails;
}
}
customElements.define('devtools-performance-network-request-details', NetworkRequestDetails);