UNPKG

chrome-devtools-frontend

Version:
475 lines (434 loc) • 17.5 kB
// 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 }