| // Copyright 2021 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. |
| |
| /* |
| * Copyright (C) 2007, 2008 Apple Inc. All rights reserved. |
| * Copyright (C) 2008, 2009 Anthony Ricaud <rik@webkit.org> |
| * Copyright (C) 2011 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of |
| * its contributors may be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| /* eslint-disable rulesdir/no_underscored_properties */ |
| |
| import * as Common from '../../core/common/common.js'; |
| import * as Host from '../../core/host/host.js'; |
| import * as i18n from '../../core/i18n/i18n.js'; |
| import * as Platform from '../../core/platform/platform.js'; |
| import * as SDK from '../../core/sdk/sdk.js'; |
| import * as Protocol from '../../generated/protocol.js'; |
| import * as Bindings from '../../models/bindings/bindings.js'; |
| import * as HAR from '../../models/har/har.js'; |
| import * as IssuesManager from '../../models/issues_manager/issues_manager.js'; |
| import * as Logs from '../../models/logs/logs.js'; |
| import * as TextUtils from '../../models/text_utils/text_utils.js'; |
| import * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js'; |
| import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js'; |
| import * as Components from '../../ui/legacy/components/utils/utils.js'; |
| import * as UI from '../../ui/legacy/legacy.js'; |
| import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js'; |
| |
| import type {NetworkLogViewInterface, NetworkNode} from './NetworkDataGridNode.js'; |
| import {Events, NetworkGroupNode, NetworkRequestNode} from './NetworkDataGridNode.js'; // eslint-disable-line no-unused-vars |
| import {NetworkFrameGrouper} from './NetworkFrameGrouper.js'; |
| import {NetworkLogViewColumns} from './NetworkLogViewColumns.js'; |
| import type {FilterOptions} from './NetworkPanel.js'; // eslint-disable-line no-unused-vars |
| import type {NetworkTimeCalculator} from './NetworkTimeCalculator.js'; |
| import {NetworkTimeBoundary, NetworkTransferDurationCalculator, NetworkTransferTimeCalculator} from './NetworkTimeCalculator.js'; // eslint-disable-line no-unused-vars |
| |
| const UIStrings = { |
| /** |
| *@description Text in Network Log View of the Network panel |
| */ |
| hideDataUrls: 'Hide data URLs', |
| /** |
| *@description Data urlfilter ui element title in Network Log View of the Network panel |
| */ |
| hidesDataAndBlobUrls: 'Hides data: and blob: URLs', |
| /** |
| *@description Aria accessible name in Network Log View of the Network panel |
| */ |
| resourceTypesToInclude: 'Resource types to include', |
| /** |
| *@description Label for a filter in the Network panel |
| */ |
| hasBlockedCookies: 'Has blocked cookies', |
| /** |
| *@description Tooltip for a checkbox in the Network panel. The response to a network request may include a |
| * cookie (https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies). Such response cookies can |
| * be malformed or otherwise invalid and the browser may choose to ignore or not accept invalid cookies. |
| */ |
| onlyShowRequestsWithBlocked: 'Only show requests with blocked response cookies', |
| /** |
| *@description Label for a filter in the Network panel |
| */ |
| blockedRequests: 'Blocked Requests', |
| /** |
| *@description Tooltip for a filter in the Network panel |
| */ |
| onlyShowBlockedRequests: 'Only show blocked requests', |
| /** |
| *@description Text that appears when user drag and drop something (for example, a file) in Network Log View of the Network panel |
| */ |
| dropHarFilesHere: 'Drop HAR files here', |
| /** |
| *@description Recording text text content in Network Log View of the Network panel |
| */ |
| recordingNetworkActivity: 'Recording network activity…', |
| /** |
| *@description Text in Network Log View of the Network panel |
| *@example {Ctrl + R} PH1 |
| */ |
| performARequestOrHitSToRecordThe: 'Perform a request or hit {PH1} to record the reload.', |
| /** |
| *@description Shown in the Network Log View of the Network panel when the user has not yet |
| * recorded any network activity. This is an instruction to the user to start recording in order to |
| * show network activity in the current UI. |
| *@example {Ctrl + E} PH1 |
| */ |
| recordSToDisplayNetworkActivity: 'Record ({PH1}) to display network activity.', |
| /** |
| *@description Text that is usually a hyperlink to more documentation |
| */ |
| learnMore: 'Learn more', |
| /** |
| *@description Text to announce to screen readers that network data is available. |
| */ |
| networkDataAvailable: 'Network Data Available', |
| /** |
| *@description Text in Network Log View of the Network panel |
| *@example {3} PH1 |
| *@example {5} PH2 |
| */ |
| sSRequests: '{PH1} / {PH2} requests', |
| /** |
| *@description Message in the summary toolbar at the bottom of the Network log that shows the compressed size of the |
| * resources transferred during a selected time frame over the compressed size of all resources transferred during |
| * the whole network log. |
| *@example {5 B} PH1 |
| *@example {10 B} PH2 |
| */ |
| sSTransferred: '{PH1} / {PH2} transferred', |
| /** |
| *@description Message in a tooltip that shows the compressed size of the resources transferred during a selected |
| * time frame over the compressed size of all resources transferred during the whole network log. |
| *@example {10} PH1 |
| *@example {15} PH2 |
| */ |
| sBSBTransferredOverNetwork: '{PH1} B / {PH2} B transferred over network', |
| /** |
| * @description Text in Network Log View of the Network panel. Appears when a particular network |
| * resource is selected by the user. Shows how large the selected resource was (PH1) out of the |
| * total size (PH2). |
| * @example {40MB} PH1 |
| * @example {50MB} PH2 |
| */ |
| sSResources: '{PH1} / {PH2} resources', |
| /** |
| *@description Text in Network Log View of the Network panel |
| *@example {40} PH1 |
| *@example {50} PH2 |
| */ |
| sBSBResourcesLoadedByThePage: '{PH1} B / {PH2} B resources loaded by the page', |
| /** |
| *@description Text in Network Log View of the Network panel |
| *@example {6} PH1 |
| */ |
| sRequests: '{PH1} requests', |
| /** |
| *@description Message in the summary toolbar at the bottom of the Network log that shows the compressed size of |
| * all resources transferred over network during a network activity log. |
| *@example {4 B} PH1 |
| */ |
| sTransferred: '{PH1} transferred', |
| /** |
| *@description Message in a tooltip that shows the compressed size of all resources transferred over network during |
| * a network activity log. |
| *@example {4} PH1 |
| */ |
| sBTransferredOverNetwork: '{PH1} B transferred over network', |
| /** |
| *@description Text in Network Log View of the Network panel |
| *@example {4} PH1 |
| */ |
| sResources: '{PH1} resources', |
| /** |
| *@description Text in Network Log View of the Network panel |
| *@example {10} PH1 |
| */ |
| sBResourcesLoadedByThePage: '{PH1} B resources loaded by the page', |
| /** |
| *@description Text in Network Log View of the Network panel |
| *@example {120ms} PH1 |
| */ |
| finishS: 'Finish: {PH1}', |
| /** |
| *@description Text in Network Log View of the Network panel |
| *@example {3000ms} PH1 |
| */ |
| domcontentloadedS: 'DOMContentLoaded: {PH1}', |
| /** |
| *@description Text in Network Log View of the Network panel |
| *@example {40ms} PH1 |
| */ |
| loadS: 'Load: {PH1}', |
| /** |
| *@description Text for copying |
| */ |
| copy: 'Copy', |
| /** |
| *@description Text in Network Log View of the Network panel |
| */ |
| copyRequestHeaders: 'Copy request headers', |
| /** |
| *@description Text in Network Log View of the Network panel |
| */ |
| copyResponseHeaders: 'Copy response headers', |
| /** |
| *@description Text in Network Log View of the Network panel |
| */ |
| copyResponse: 'Copy response', |
| /** |
| *@description Text in Network Log View of the Network panel |
| */ |
| copyStacktrace: 'Copy stack trace', |
| /** |
| * @description A context menu command in the Network panel, for copying to the clipboard. |
| * PowerShell refers to the format the data will be copied as. |
| */ |
| copyAsPowershell: 'Copy as `PowerShell`', |
| /** |
| *@description A context menu command in the Network panel, for copying to the clipboard. 'fetch' |
| * refers to the format the data will be copied as, which is compatible with the fetch web API. |
| */ |
| copyAsFetch: 'Copy as `fetch`', |
| /** |
| * @description Text in Network Log View of the Network panel. An action that copies a command to |
| * the developer's clipboard. The command allows the developer to replay this specific network |
| * request in Node.js, a desktop application/framework. 'Node.js fetch' is a noun phrase for the |
| * type of request that will be copied. |
| */ |
| copyAsNodejsFetch: 'Copy as `Node.js` `fetch`', |
| /** |
| *@description Text in Network Log View of the Network panel. An action that copies a command to |
| *the clipboard. It will copy the command in the format compatible with cURL (a program, not |
| *translatable). |
| */ |
| copyAsCurlCmd: 'Copy as `cURL` (`cmd`)', |
| /** |
| *@description Text in Network Log View of the Network panel. An action that copies a command to |
| *the clipboard. It will copy the command in the format compatible with a Bash script. |
| */ |
| copyAsCurlBash: 'Copy as `cURL` (`bash`)', |
| /** |
| *@description Text in Network Log View of the Network panel. An action that copies a command to |
| *the clipboard. It will copy the command in the format compatible with a PowerShell script. |
| */ |
| copyAllAsPowershell: 'Copy all as `PowerShell`', |
| /** |
| *@description Text in Network Log View of the Network panel. An action that copies a command to |
| *the clipboard. It will copy the command in the format compatible with a 'fetch' command (fetch |
| *should not be translated). |
| */ |
| copyAllAsFetch: 'Copy all as `fetch`', |
| /** |
| *@description Text in Network Log View of the Network panel. An action that copies a command to |
| *the clipboard. It will copy the command in the format compatible with a Node.js 'fetch' command |
| *(fetch and Node.js should not be translated). |
| */ |
| copyAllAsNodejsFetch: 'Copy all as `Node.js` `fetch`', |
| /** |
| *@description Text in Network Log View of the Network panel. An action that copies a command to |
| *the clipboard. It will copy the command in the format compatible with cURL (a program, not |
| *translatable). |
| */ |
| copyAllAsCurlCmd: 'Copy all as `cURL` (`cmd`)', |
| /** |
| *@description Text in Network Log View of the Network panel. An action that copies a command to |
| *the clipboard. It will copy the command in the format compatible with a Bash script. |
| */ |
| copyAllAsCurlBash: 'Copy all as `cURL` (`bash`)', |
| /** |
| *@description Text in Network Log View of the Network panel. An action that copies a command to |
| *the clipboard. It will copy the command in the format compatible with cURL (a program, not |
| *translatable). |
| */ |
| copyAsCurl: 'Copy as `cURL`', |
| /** |
| *@description Text in Network Log View of the Network panel. An action that copies a command to |
| *the clipboard. It will copy the command in the format compatible with cURL (a program, not |
| *translatable). |
| */ |
| copyAllAsCurl: 'Copy all as `cURL`', |
| /** |
| * @description Text in Network Log View of the Network panel. An action that copies data to the |
| * clipboard. It will copy the data in the HAR (not translatable) format. 'all' refers to every |
| * network request that is currently shown. |
| */ |
| copyAllAsHar: 'Copy all as `HAR`', |
| /** |
| *@description A context menu item in the Network Log View of the Network panel |
| */ |
| saveAllAsHarWithContent: 'Save all as `HAR` with content', |
| /** |
| *@description A context menu item in the Network Log View of the Network panel |
| */ |
| clearBrowserCache: 'Clear browser cache', |
| /** |
| *@description A context menu item in the Network Log View of the Network panel |
| */ |
| clearBrowserCookies: 'Clear browser cookies', |
| /** |
| *@description A context menu item in the Network Log View of the Network panel |
| */ |
| blockRequestUrl: 'Block request URL', |
| /** |
| *@description A context menu item in the Network Log View of the Network panel |
| *@example {example.com} PH1 |
| */ |
| unblockS: 'Unblock {PH1}', |
| /** |
| *@description A context menu item in the Network Log View of the Network panel |
| */ |
| blockRequestDomain: 'Block request domain', |
| /** |
| *@description Text to replay an XHR request |
| */ |
| replayXhr: 'Replay XHR', |
| /** |
| *@description Text in Network Log View of the Network panel |
| */ |
| areYouSureYouWantToClearBrowser: 'Are you sure you want to clear browser cache?', |
| /** |
| *@description Text in Network Log View of the Network panel |
| */ |
| areYouSureYouWantToClearBrowserCookies: 'Are you sure you want to clear browser cookies?', |
| }; |
| const str_ = i18n.i18n.registerUIStrings('panels/network/NetworkLogView.ts', UIStrings); |
| const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); |
| export class NetworkLogView extends UI.Widget.VBox implements |
| SDK.TargetManager.SDKModelObserver<SDK.NetworkManager.NetworkManager>, NetworkLogViewInterface { |
| _networkHideDataURLSetting: Common.Settings.Setting<boolean>; |
| _networkShowIssuesOnlySetting: Common.Settings.Setting<boolean>; |
| _networkOnlyBlockedRequestsSetting: Common.Settings.Setting<boolean>; |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| _networkResourceTypeFiltersSetting: Common.Settings.Setting<any>; |
| _rawRowHeight: number; |
| _progressBarContainer: Element; |
| _networkLogLargeRowsSetting: Common.Settings.Setting<boolean>; |
| _rowHeight: number; |
| _timeCalculator: NetworkTransferTimeCalculator; |
| _durationCalculator: NetworkTransferDurationCalculator; |
| _calculator: NetworkTransferTimeCalculator; |
| _columns: NetworkLogViewColumns; |
| _staleRequests: Set<SDK.NetworkRequest.NetworkRequest>; |
| _mainRequestLoadTime: number; |
| _mainRequestDOMContentLoadedTime: number; |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| _highlightedSubstringChanges: any; |
| _filters: Filter[]; |
| _timeFilter: Filter|null; |
| _hoveredNode: NetworkNode|null; |
| _recordingHint: Element|null; |
| _refreshRequestId: number|null; |
| _highlightedNode: NetworkRequestNode|null; |
| _linkifier: Components.Linkifier.Linkifier; |
| _recording: boolean; |
| _needsRefresh: boolean; |
| _headerHeight: number; |
| _groupLookups: Map<string, GroupLookupInterface>; |
| _activeGroupLookup: GroupLookupInterface|null; |
| _textFilterUI: UI.FilterBar.TextFilterUI; |
| _dataURLFilterUI: UI.FilterBar.CheckboxFilterUI; |
| _resourceCategoryFilterUI: UI.FilterBar.NamedBitSetFilterUI; |
| _onlyIssuesFilterUI: UI.FilterBar.CheckboxFilterUI; |
| _onlyBlockedRequestsUI: UI.FilterBar.CheckboxFilterUI; |
| _filterParser: TextUtils.TextUtils.FilterParser; |
| _suggestionBuilder: UI.FilterSuggestionBuilder.FilterSuggestionBuilder; |
| _dataGrid: DataGrid.SortableDataGrid.SortableDataGrid<NetworkNode>; |
| _summaryToolbar: UI.Toolbar.Toolbar; |
| _filterBar: UI.FilterBar.FilterBar; |
| _textFilterSetting: Common.Settings.Setting<string>; |
| |
| constructor( |
| filterBar: UI.FilterBar.FilterBar, progressBarContainer: Element, |
| networkLogLargeRowsSetting: Common.Settings.Setting<boolean>) { |
| super(); |
| this.setMinimumSize(50, 64); |
| this.registerRequiredCSS('panels/network/networkLogView.css', {enableLegacyPatching: false}); |
| |
| this.element.id = 'network-container'; |
| this.element.classList.add('no-node-selected'); |
| |
| this._networkHideDataURLSetting = Common.Settings.Settings.instance().createSetting('networkHideDataURL', false); |
| this._networkShowIssuesOnlySetting = |
| Common.Settings.Settings.instance().createSetting('networkShowIssuesOnly', false); |
| this._networkOnlyBlockedRequestsSetting = |
| Common.Settings.Settings.instance().createSetting('networkOnlyBlockedRequests', false); |
| this._networkResourceTypeFiltersSetting = |
| Common.Settings.Settings.instance().createSetting('networkResourceTypeFilters', {}); |
| |
| this._rawRowHeight = 0; |
| this._progressBarContainer = progressBarContainer; |
| this._networkLogLargeRowsSetting = networkLogLargeRowsSetting; |
| this._networkLogLargeRowsSetting.addChangeListener(updateRowHeight.bind(this), this); |
| |
| function updateRowHeight(this: NetworkLogView): void { |
| this._rawRowHeight = Boolean(this._networkLogLargeRowsSetting.get()) ? 41 : 21; |
| this._rowHeight = this._computeRowHeight(); |
| } |
| this._rawRowHeight = 0; |
| this._rowHeight = 0; |
| updateRowHeight.call(this); |
| |
| this._timeCalculator = new NetworkTransferTimeCalculator(); |
| this._durationCalculator = new NetworkTransferDurationCalculator(); |
| this._calculator = this._timeCalculator; |
| |
| this._columns = |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration |
| // @ts-expect-error |
| new NetworkLogViewColumns(this, this._timeCalculator, this._durationCalculator, networkLogLargeRowsSetting); |
| this._columns.show(this.element); |
| |
| this._staleRequests = new Set(); |
| this._mainRequestLoadTime = -1; |
| this._mainRequestDOMContentLoadedTime = -1; |
| this._highlightedSubstringChanges = []; |
| |
| this._filters = []; |
| this._timeFilter = null; |
| this._hoveredNode = null; |
| this._recordingHint = null; |
| this._refreshRequestId = null; |
| this._highlightedNode = null; |
| |
| this._linkifier = new Components.Linkifier.Linkifier(); |
| |
| this._recording = false; |
| this._needsRefresh = false; |
| |
| this._headerHeight = 0; |
| |
| this._groupLookups = new Map(); |
| this._groupLookups.set('Frame', new NetworkFrameGrouper(this)); |
| |
| this._activeGroupLookup = null; |
| |
| this._textFilterUI = new UI.FilterBar.TextFilterUI(); |
| this._textFilterUI.addEventListener(UI.FilterBar.FilterUI.Events.FilterChanged, this._filterChanged, this); |
| filterBar.addFilter(this._textFilterUI); |
| |
| this._dataURLFilterUI = new UI.FilterBar.CheckboxFilterUI( |
| 'hide-data-url', i18nString(UIStrings.hideDataUrls), true, this._networkHideDataURLSetting); |
| this._dataURLFilterUI.addEventListener( |
| UI.FilterBar.FilterUI.Events.FilterChanged, this._filterChanged.bind(this), this); |
| UI.Tooltip.Tooltip.install(this._dataURLFilterUI.element(), i18nString(UIStrings.hidesDataAndBlobUrls)); |
| filterBar.addFilter(this._dataURLFilterUI); |
| |
| const filterItems = |
| Object.values(Common.ResourceType.resourceCategories) |
| .map( |
| category => |
| ({name: category.title(), label: (): string => category.shortTitle(), title: category.title()})); |
| this._resourceCategoryFilterUI = |
| new UI.FilterBar.NamedBitSetFilterUI(filterItems, this._networkResourceTypeFiltersSetting); |
| UI.ARIAUtils.setAccessibleName( |
| this._resourceCategoryFilterUI.element(), i18nString(UIStrings.resourceTypesToInclude)); |
| this._resourceCategoryFilterUI.addEventListener( |
| UI.FilterBar.FilterUI.Events.FilterChanged, this._filterChanged.bind(this), this); |
| filterBar.addFilter(this._resourceCategoryFilterUI); |
| |
| this._onlyIssuesFilterUI = new UI.FilterBar.CheckboxFilterUI( |
| 'only-show-issues', i18nString(UIStrings.hasBlockedCookies), true, this._networkShowIssuesOnlySetting); |
| this._onlyIssuesFilterUI.addEventListener( |
| UI.FilterBar.FilterUI.Events.FilterChanged, this._filterChanged.bind(this), this); |
| UI.Tooltip.Tooltip.install(this._onlyIssuesFilterUI.element(), i18nString(UIStrings.onlyShowRequestsWithBlocked)); |
| filterBar.addFilter(this._onlyIssuesFilterUI); |
| |
| this._onlyBlockedRequestsUI = new UI.FilterBar.CheckboxFilterUI( |
| 'only-show-blocked-requests', i18nString(UIStrings.blockedRequests), true, |
| this._networkOnlyBlockedRequestsSetting); |
| this._onlyBlockedRequestsUI.addEventListener( |
| UI.FilterBar.FilterUI.Events.FilterChanged, this._filterChanged.bind(this), this); |
| UI.Tooltip.Tooltip.install(this._onlyBlockedRequestsUI.element(), i18nString(UIStrings.onlyShowBlockedRequests)); |
| filterBar.addFilter(this._onlyBlockedRequestsUI); |
| |
| this._filterParser = new TextUtils.TextUtils.FilterParser(_searchKeys); |
| this._suggestionBuilder = |
| new UI.FilterSuggestionBuilder.FilterSuggestionBuilder(_searchKeys, NetworkLogView._sortSearchValues); |
| this._resetSuggestionBuilder(); |
| |
| this._dataGrid = this._columns.dataGrid(); |
| this._setupDataGrid(); |
| this._columns.sortByCurrentColumn(); |
| filterBar.filterButton().addEventListener( |
| UI.Toolbar.ToolbarButton.Events.Click, |
| this._dataGrid.scheduleUpdate.bind(this._dataGrid, true /* isFromUser */)); |
| |
| this._summaryToolbar = new UI.Toolbar.Toolbar('network-summary-bar', this.element); |
| this._summaryToolbar.element.setAttribute('role', 'status'); |
| |
| new UI.DropTarget.DropTarget( |
| this.element, [UI.DropTarget.Type.File], i18nString(UIStrings.dropHarFilesHere), this._handleDrop.bind(this)); |
| |
| Common.Settings.Settings.instance() |
| .moduleSetting('networkColorCodeResourceTypes') |
| .addChangeListener(this._invalidateAllItems.bind(this, false), this); |
| |
| SDK.TargetManager.TargetManager.instance().observeModels(SDK.NetworkManager.NetworkManager, this); |
| Logs.NetworkLog.NetworkLog.instance().addEventListener( |
| Logs.NetworkLog.Events.RequestAdded, this._onRequestUpdated, this); |
| Logs.NetworkLog.NetworkLog.instance().addEventListener( |
| Logs.NetworkLog.Events.RequestUpdated, this._onRequestUpdated, this); |
| Logs.NetworkLog.NetworkLog.instance().addEventListener(Logs.NetworkLog.Events.Reset, this._reset, this); |
| |
| this._updateGroupByFrame(); |
| Common.Settings.Settings.instance() |
| .moduleSetting('network.group-by-frame') |
| .addChangeListener(() => this._updateGroupByFrame()); |
| |
| this._filterBar = filterBar; |
| |
| this._textFilterSetting = Common.Settings.Settings.instance().createSetting('networkTextFilter', ''); |
| if (this._textFilterSetting.get()) { |
| this._textFilterUI.setValue(this._textFilterSetting.get()); |
| } |
| } |
| |
| _updateGroupByFrame(): void { |
| const value = Common.Settings.Settings.instance().moduleSetting('network.group-by-frame').get(); |
| this._setGrouping(value ? 'Frame' : null); |
| } |
| |
| static _sortSearchValues(key: string, values: string[]): void { |
| if (key === FilterType.Priority) { |
| values.sort((a, b) => { |
| const aPriority = (PerfUI.NetworkPriorities.uiLabelToNetworkPriority(a) as Protocol.Network.ResourcePriority); |
| const bPriority = (PerfUI.NetworkPriorities.uiLabelToNetworkPriority(b) as Protocol.Network.ResourcePriority); |
| return PerfUI.NetworkPriorities.networkPriorityWeight(aPriority) - |
| PerfUI.NetworkPriorities.networkPriorityWeight(bPriority); |
| }); |
| } else { |
| values.sort(); |
| } |
| } |
| |
| static _negativeFilter(filter: Filter, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return !filter(request); |
| } |
| |
| static _requestPathFilter(regex: RegExp|null, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| if (!regex) { |
| return false; |
| } |
| |
| return regex.test(request.path() + '/' + request.name()); |
| } |
| |
| static _subdomains(domain: string): string[] { |
| const result = [domain]; |
| let indexOfPeriod = domain.indexOf('.'); |
| while (indexOfPeriod !== -1) { |
| result.push('*' + domain.substring(indexOfPeriod)); |
| indexOfPeriod = domain.indexOf('.', indexOfPeriod + 1); |
| } |
| return result; |
| } |
| |
| static _createRequestDomainFilter(value: string): Filter { |
| const escapedPattern = value.split('*').map(Platform.StringUtilities.escapeForRegExp).join('.*'); |
| return NetworkLogView._requestDomainFilter.bind(null, new RegExp('^' + escapedPattern + '$', 'i')); |
| } |
| |
| static _requestDomainFilter(regex: RegExp, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return regex.test(request.domain); |
| } |
| |
| static _runningRequestFilter(request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return !request.finished; |
| } |
| |
| static _fromCacheRequestFilter(request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.cached(); |
| } |
| |
| static _interceptedByServiceWorkerFilter(request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.fetchedViaServiceWorker; |
| } |
| |
| static _initiatedByServiceWorkerFilter(request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.initiatedByServiceWorker(); |
| } |
| |
| static _requestResponseHeaderFilter(value: string, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.responseHeaderValue(value) !== undefined; |
| } |
| |
| static _requestMethodFilter(value: string, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.requestMethod === value; |
| } |
| |
| static _requestPriorityFilter(value: string, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.priority() === value; |
| } |
| |
| static _requestMimeTypeFilter(value: string, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.mimeType === value; |
| } |
| |
| static _requestMixedContentFilter(value: MixedContentFilterValues, request: SDK.NetworkRequest.NetworkRequest): |
| boolean { |
| if (value === MixedContentFilterValues.Displayed) { |
| return request.mixedContentType === Protocol.Security.MixedContentType.OptionallyBlockable; |
| } |
| if (value === MixedContentFilterValues.Blocked) { |
| return request.mixedContentType === Protocol.Security.MixedContentType.Blockable && request.wasBlocked(); |
| } |
| if (value === MixedContentFilterValues.BlockOverridden) { |
| return request.mixedContentType === Protocol.Security.MixedContentType.Blockable && !request.wasBlocked(); |
| } |
| if (value === MixedContentFilterValues.All) { |
| return request.mixedContentType !== Protocol.Security.MixedContentType.None; |
| } |
| |
| return false; |
| } |
| |
| static _requestSchemeFilter(value: string, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.scheme === value; |
| } |
| |
| static _requestCookieDomainFilter(value: string, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.allCookiesIncludingBlockedOnes().some(cookie => cookie.domain() === value); |
| } |
| |
| static _requestCookieNameFilter(value: string, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.allCookiesIncludingBlockedOnes().some(cookie => cookie.name() === value); |
| } |
| |
| static _requestCookiePathFilter(value: string, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.allCookiesIncludingBlockedOnes().some(cookie => cookie.path() === value); |
| } |
| |
| static _requestCookieValueFilter(value: string, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.allCookiesIncludingBlockedOnes().some(cookie => cookie.value() === value); |
| } |
| |
| static _requestSetCookieDomainFilter(value: string, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.responseCookies.some(cookie => cookie.domain() === value); |
| } |
| |
| static _requestSetCookieNameFilter(value: string, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.responseCookies.some(cookie => cookie.name() === value); |
| } |
| |
| static _requestSetCookieValueFilter(value: string, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.responseCookies.some(cookie => cookie.value() === value); |
| } |
| |
| static _requestSizeLargerThanFilter(value: number, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.transferSize >= value; |
| } |
| |
| static _statusCodeFilter(value: string, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return (String(request.statusCode)) === value; |
| } |
| |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) |
| // eslint-disable-next-line @typescript-eslint/naming-convention |
| static HTTPRequestsFilter(request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.parsedURL.isValid && (request.scheme in HTTPSchemas); |
| } |
| |
| static _resourceTypeFilter(value: string, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| return request.resourceType().name() === value; |
| } |
| |
| static _requestUrlFilter(value: string, request: SDK.NetworkRequest.NetworkRequest): boolean { |
| const regex = new RegExp(Platform.StringUtilities.escapeForRegExp(value), 'i'); |
| return regex.test(request.url()); |
| } |
| |
| static _requestTimeFilter(windowStart: number, windowEnd: number, request: SDK.NetworkRequest.NetworkRequest): |
| boolean { |
| if (request.issueTime() > windowEnd) { |
| return false; |
| } |
| if (request.endTime !== -1 && request.endTime < windowStart) { |
| return false; |
| } |
| return true; |
| } |
| |
| static _copyRequestHeaders(request: SDK.NetworkRequest.NetworkRequest): void { |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(request.requestHeadersText()); |
| } |
| |
| static _copyResponseHeaders(request: SDK.NetworkRequest.NetworkRequest): void { |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(request.responseHeadersText); |
| } |
| |
| static async _copyResponse(request: SDK.NetworkRequest.NetworkRequest): Promise<void> { |
| const contentData = await request.contentData(); |
| let content: (string|null)|string = contentData.content || ''; |
| if (!request.contentType().isTextType()) { |
| content = TextUtils.ContentProvider.contentAsDataURL(content, request.mimeType, contentData.encoded); |
| } else if (contentData.encoded && content) { |
| content = window.atob(content); |
| } |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(content); |
| } |
| |
| _handleDrop(dataTransfer: DataTransfer): void { |
| const items = dataTransfer.items; |
| if (!items.length) { |
| return; |
| } |
| const entry = items[0].webkitGetAsEntry(); |
| if (entry.isDirectory) { |
| return; |
| } |
| |
| entry.file(this.onLoadFromFile.bind(this)); |
| } |
| |
| async onLoadFromFile(file: File): Promise<void> { |
| const outputStream = new Common.StringOutputStream.StringOutputStream(); |
| const reader = new Bindings.FileUtils.ChunkedFileReader(file, /* chunkSize */ 10000000); |
| const success = await reader.read(outputStream); |
| if (!success) { |
| const error = reader.error(); |
| if (error) { |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| this._harLoadFailed((error as any).message); |
| } |
| return; |
| } |
| let harRoot; |
| try { |
| // HARRoot and JSON.parse might throw. |
| harRoot = new HAR.HARFormat.HARRoot(JSON.parse(outputStream.data())); |
| } catch (e) { |
| this._harLoadFailed(e); |
| return; |
| } |
| Logs.NetworkLog.NetworkLog.instance().importRequests(HAR.Importer.Importer.requestsFromHARLog(harRoot.log)); |
| } |
| |
| _harLoadFailed(message: string): void { |
| Common.Console.Console.instance().error('Failed to load HAR file with following error: ' + message); |
| } |
| |
| _setGrouping(groupKey: string|null): void { |
| if (this._activeGroupLookup) { |
| this._activeGroupLookup.reset(); |
| } |
| const groupLookup = groupKey ? this._groupLookups.get(groupKey) || null : null; |
| this._activeGroupLookup = groupLookup; |
| this._invalidateAllItems(); |
| } |
| |
| _computeRowHeight(): number { |
| return Math.round(this._rawRowHeight * window.devicePixelRatio) / window.devicePixelRatio; |
| } |
| |
| nodeForRequest(request: SDK.NetworkRequest.NetworkRequest): NetworkRequestNode|null { |
| return networkRequestToNode.get(request) || null; |
| } |
| |
| headerHeight(): number { |
| return this._headerHeight; |
| } |
| |
| setRecording(recording: boolean): void { |
| this._recording = recording; |
| this._updateSummaryBar(); |
| } |
| |
| modelAdded(networkManager: SDK.NetworkManager.NetworkManager): void { |
| // TODO(allada) Remove dependency on networkManager and instead use NetworkLog and PageLoad for needed data. |
| if (networkManager.target().parentTarget()) { |
| return; |
| } |
| const resourceTreeModel = networkManager.target().model(SDK.ResourceTreeModel.ResourceTreeModel); |
| if (resourceTreeModel) { |
| resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.Load, this._loadEventFired, this); |
| resourceTreeModel.addEventListener( |
| SDK.ResourceTreeModel.Events.DOMContentLoaded, this._domContentLoadedEventFired, this); |
| } |
| } |
| |
| modelRemoved(networkManager: SDK.NetworkManager.NetworkManager): void { |
| if (!networkManager.target().parentTarget()) { |
| const resourceTreeModel = networkManager.target().model(SDK.ResourceTreeModel.ResourceTreeModel); |
| if (resourceTreeModel) { |
| resourceTreeModel.removeEventListener(SDK.ResourceTreeModel.Events.Load, this._loadEventFired, this); |
| resourceTreeModel.removeEventListener( |
| SDK.ResourceTreeModel.Events.DOMContentLoaded, this._domContentLoadedEventFired, this); |
| } |
| } |
| } |
| |
| linkifier(): Components.Linkifier.Linkifier { |
| return this._linkifier; |
| } |
| |
| setWindow(start: number, end: number): void { |
| if (!start && !end) { |
| this._timeFilter = null; |
| this._timeCalculator.setWindow(null); |
| } else { |
| this._timeFilter = NetworkLogView._requestTimeFilter.bind(null, start, end); |
| this._timeCalculator.setWindow(new NetworkTimeBoundary(start, end)); |
| } |
| this._filterRequests(); |
| } |
| |
| resetFocus(): void { |
| this._dataGrid.element.focus(); |
| } |
| |
| _resetSuggestionBuilder(): void { |
| this._suggestionBuilder.clear(); |
| this._suggestionBuilder.addItem(FilterType.Is, IsFilterType.Running); |
| this._suggestionBuilder.addItem(FilterType.Is, IsFilterType.FromCache); |
| this._suggestionBuilder.addItem(FilterType.Is, IsFilterType.ServiceWorkerIntercepted); |
| this._suggestionBuilder.addItem(FilterType.Is, IsFilterType.ServiceWorkerInitiated); |
| this._suggestionBuilder.addItem(FilterType.LargerThan, '100'); |
| this._suggestionBuilder.addItem(FilterType.LargerThan, '10k'); |
| this._suggestionBuilder.addItem(FilterType.LargerThan, '1M'); |
| this._textFilterUI.setSuggestionProvider(this._suggestionBuilder.completions.bind(this._suggestionBuilder)); |
| } |
| |
| _filterChanged(_event: Common.EventTarget.EventTargetEvent): void { |
| this.removeAllNodeHighlights(); |
| this._parseFilterQuery(this._textFilterUI.value()); |
| this._filterRequests(); |
| this._textFilterSetting.set(this._textFilterUI.value()); |
| } |
| |
| async resetFilter(): Promise<void> { |
| this._textFilterUI.clear(); |
| } |
| |
| _showRecordingHint(): void { |
| this._hideRecordingHint(); |
| this._recordingHint = this.element.createChild('div', 'network-status-pane fill'); |
| const hintText = this._recordingHint.createChild('div', 'recording-hint'); |
| |
| if (this._recording) { |
| let reloadShortcutNode: Element|null = null; |
| const reloadShortcut = |
| UI.ShortcutRegistry.ShortcutRegistry.instance().shortcutsForAction('inspector_main.reload')[0]; |
| if (reloadShortcut) { |
| reloadShortcutNode = this._recordingHint.createChild('b'); |
| reloadShortcutNode.textContent = reloadShortcut.title(); |
| } |
| |
| const recordingText = hintText.createChild('span'); |
| recordingText.textContent = i18nString(UIStrings.recordingNetworkActivity); |
| if (reloadShortcutNode) { |
| hintText.createChild('br'); |
| hintText.appendChild(i18n.i18n.getFormatLocalizedString( |
| str_, UIStrings.performARequestOrHitSToRecordThe, {PH1: reloadShortcutNode})); |
| } |
| } else { |
| const recordNode = hintText.createChild('b'); |
| recordNode.textContent = |
| UI.ShortcutRegistry.ShortcutRegistry.instance().shortcutTitleForAction('network.toggle-recording') || ''; |
| hintText.appendChild( |
| i18n.i18n.getFormatLocalizedString(str_, UIStrings.recordSToDisplayNetworkActivity, {PH1: recordNode})); |
| } |
| hintText.createChild('br'); |
| hintText.appendChild(UI.XLink.XLink.create( |
| 'https://developer.chrome.com/docs/devtools/network/?utm_source=devtools&utm_campaign=2019Q1', |
| i18nString(UIStrings.learnMore))); |
| |
| this._setHidden(true); |
| } |
| |
| _hideRecordingHint(): void { |
| this._setHidden(false); |
| if (this._recordingHint) { |
| this._recordingHint.remove(); |
| } |
| UI.ARIAUtils.alert(i18nString(UIStrings.networkDataAvailable)); |
| this._recordingHint = null; |
| } |
| |
| _setHidden(value: boolean): void { |
| this._columns.setHidden(value); |
| UI.ARIAUtils.setHidden(this._summaryToolbar.element, value); |
| } |
| |
| elementsToRestoreScrollPositionsFor(): Element[] { |
| if (!this._dataGrid) // Not initialized yet. |
| { |
| return []; |
| } |
| return [this._dataGrid.scrollContainer]; |
| } |
| |
| columnExtensionResolved(): void { |
| this._invalidateAllItems(true); |
| } |
| |
| _setupDataGrid(): DataGrid.SortableDataGrid.SortableDataGrid<NetworkNode> { |
| this._dataGrid.setRowContextMenuCallback((contextMenu, node) => { |
| const request = (node as NetworkNode).request(); |
| if (request) { |
| this.handleContextMenuForRequest(contextMenu, request); |
| } |
| }); |
| this._dataGrid.setStickToBottom(true); |
| this._dataGrid.setName('networkLog'); |
| this._dataGrid.setResizeMethod(DataGrid.DataGrid.ResizeMethod.Last); |
| this._dataGrid.element.classList.add('network-log-grid'); |
| this._dataGrid.element.addEventListener('mousedown', this._dataGridMouseDown.bind(this), true); |
| this._dataGrid.element.addEventListener('mousemove', this._dataGridMouseMove.bind(this), true); |
| this._dataGrid.element.addEventListener('mouseleave', () => this._setHoveredNode(null), true); |
| this._dataGrid.element.addEventListener('keydown', event => { |
| if (event.key === 'ArrowRight' && this._dataGrid.selectedNode) { |
| const initiatorLink = this._dataGrid.selectedNode.element().querySelector('span.devtools-link'); |
| if (initiatorLink) { |
| (initiatorLink as HTMLElement).focus(); |
| } |
| } |
| if (isEnterOrSpaceKey(event)) { |
| this.dispatchEventToListeners(Events.RequestActivated, {showPanel: true, takeFocus: true}); |
| event.consume(true); |
| } |
| }); |
| this._dataGrid.element.addEventListener('focus', this._onDataGridFocus.bind(this), true); |
| this._dataGrid.element.addEventListener('blur', this._onDataGridBlur.bind(this), true); |
| return this._dataGrid; |
| } |
| |
| _dataGridMouseMove(event: Event): void { |
| const mouseEvent = (event as MouseEvent); |
| const node = (this._dataGrid.dataGridNodeFromNode((mouseEvent.target as Node))); |
| const highlightInitiatorChain = mouseEvent.shiftKey; |
| this._setHoveredNode(node as NetworkNode, highlightInitiatorChain); |
| } |
| |
| hoveredNode(): NetworkNode|null { |
| return this._hoveredNode; |
| } |
| |
| _setHoveredNode(node: NetworkNode|null, highlightInitiatorChain?: boolean): void { |
| if (this._hoveredNode) { |
| this._hoveredNode.setHovered(false, false); |
| } |
| this._hoveredNode = node; |
| if (this._hoveredNode) { |
| this._hoveredNode.setHovered(true, Boolean(highlightInitiatorChain)); |
| } |
| } |
| |
| _dataGridMouseDown(event: Event): void { |
| const mouseEvent = (event as MouseEvent); |
| if (!this._dataGrid.selectedNode && mouseEvent.button) { |
| mouseEvent.consume(); |
| } |
| } |
| |
| _updateSummaryBar(): void { |
| this._hideRecordingHint(); |
| |
| let transferSize = 0; |
| let resourceSize = 0; |
| let selectedNodeNumber = 0; |
| let selectedTransferSize = 0; |
| let selectedResourceSize = 0; |
| let baseTime = -1; |
| let maxTime = -1; |
| |
| let nodeCount = 0; |
| for (const request of Logs.NetworkLog.NetworkLog.instance().requests()) { |
| const node = networkRequestToNode.get(request); |
| if (!node) { |
| continue; |
| } |
| nodeCount++; |
| const requestTransferSize = request.transferSize; |
| transferSize += requestTransferSize; |
| const requestResourceSize = request.resourceSize; |
| resourceSize += requestResourceSize; |
| if (!filteredNetworkRequests.has(node)) { |
| selectedNodeNumber++; |
| selectedTransferSize += requestTransferSize; |
| selectedResourceSize += requestResourceSize; |
| } |
| const networkManager = SDK.NetworkManager.NetworkManager.forRequest(request); |
| // TODO(allada) inspectedURL should be stored in PageLoad used instead of target so HAR requests can have an |
| // inspected url. |
| if (networkManager && request.url() === networkManager.target().inspectedURL() && |
| request.resourceType() === Common.ResourceType.resourceTypes.Document && |
| !networkManager.target().parentTarget()) { |
| baseTime = request.startTime; |
| } |
| if (request.endTime > maxTime) { |
| maxTime = request.endTime; |
| } |
| } |
| |
| if (!nodeCount) { |
| this._showRecordingHint(); |
| return; |
| } |
| |
| this._summaryToolbar.removeToolbarItems(); |
| const appendChunk = (chunk: string, title?: string): HTMLDivElement => { |
| const toolbarText = new UI.Toolbar.ToolbarText(chunk); |
| toolbarText.setTitle(title ? title : chunk); |
| this._summaryToolbar.appendToolbarItem(toolbarText); |
| return toolbarText.element as HTMLDivElement; |
| }; |
| |
| if (selectedNodeNumber !== nodeCount) { |
| appendChunk(i18nString(UIStrings.sSRequests, {PH1: selectedNodeNumber, PH2: nodeCount})); |
| this._summaryToolbar.appendSeparator(); |
| appendChunk( |
| i18nString(UIStrings.sSTransferred, { |
| PH1: Platform.NumberUtilities.bytesToString(selectedTransferSize), |
| PH2: Platform.NumberUtilities.bytesToString(transferSize), |
| }), |
| i18nString(UIStrings.sBSBTransferredOverNetwork, {PH1: selectedTransferSize, PH2: transferSize})); |
| this._summaryToolbar.appendSeparator(); |
| appendChunk( |
| i18nString(UIStrings.sSResources, { |
| PH1: Platform.NumberUtilities.bytesToString(selectedResourceSize), |
| PH2: Platform.NumberUtilities.bytesToString(resourceSize), |
| }), |
| i18nString(UIStrings.sBSBResourcesLoadedByThePage, {PH1: selectedResourceSize, PH2: resourceSize})); |
| } else { |
| appendChunk(i18nString(UIStrings.sRequests, {PH1: nodeCount})); |
| this._summaryToolbar.appendSeparator(); |
| appendChunk( |
| i18nString(UIStrings.sTransferred, {PH1: Platform.NumberUtilities.bytesToString(transferSize)}), |
| i18nString(UIStrings.sBTransferredOverNetwork, {PH1: transferSize})); |
| this._summaryToolbar.appendSeparator(); |
| appendChunk( |
| i18nString(UIStrings.sResources, {PH1: Platform.NumberUtilities.bytesToString(resourceSize)}), |
| i18nString(UIStrings.sBResourcesLoadedByThePage, {PH1: resourceSize})); |
| } |
| |
| if (baseTime !== -1 && maxTime !== -1) { |
| this._summaryToolbar.appendSeparator(); |
| appendChunk(i18nString(UIStrings.finishS, {PH1: Number.secondsToString(maxTime - baseTime)})); |
| if (this._mainRequestDOMContentLoadedTime !== -1 && this._mainRequestDOMContentLoadedTime > baseTime) { |
| this._summaryToolbar.appendSeparator(); |
| const domContentLoadedText = i18nString( |
| UIStrings.domcontentloadedS, |
| {PH1: Number.secondsToString(this._mainRequestDOMContentLoadedTime - baseTime)}); |
| appendChunk(domContentLoadedText).style.color = NetworkLogView.getDCLEventColor(); |
| } |
| if (this._mainRequestLoadTime !== -1) { |
| this._summaryToolbar.appendSeparator(); |
| const loadText = |
| i18nString(UIStrings.loadS, {PH1: Number.secondsToString(this._mainRequestLoadTime - baseTime)}); |
| appendChunk(loadText).style.color = NetworkLogView.getLoadEventColor(); |
| } |
| } |
| } |
| |
| scheduleRefresh(): void { |
| if (this._needsRefresh) { |
| return; |
| } |
| |
| this._needsRefresh = true; |
| |
| if (this.isShowing() && !this._refreshRequestId) { |
| this._refreshRequestId = this.element.window().requestAnimationFrame(this._refresh.bind(this)); |
| } |
| } |
| |
| addFilmStripFrames(times: number[]): void { |
| this._columns.addEventDividers(times, 'network-frame-divider'); |
| } |
| |
| selectFilmStripFrame(time: number): void { |
| this._columns.selectFilmStripFrame(time); |
| } |
| |
| clearFilmStripFrame(): void { |
| this._columns.clearFilmStripFrame(); |
| } |
| |
| _refreshIfNeeded(): void { |
| if (this._needsRefresh) { |
| this._refresh(); |
| } |
| } |
| |
| _invalidateAllItems(deferUpdate?: boolean): void { |
| this._staleRequests = new Set(Logs.NetworkLog.NetworkLog.instance().requests()); |
| if (deferUpdate) { |
| this.scheduleRefresh(); |
| } else { |
| this._refresh(); |
| } |
| } |
| |
| timeCalculator(): NetworkTimeCalculator { |
| return this._timeCalculator; |
| } |
| |
| calculator(): NetworkTimeCalculator { |
| return this._calculator; |
| } |
| |
| setCalculator(x: NetworkTimeCalculator): void { |
| if (!x || this._calculator === x) { |
| return; |
| } |
| |
| if (this._calculator !== x) { |
| this._calculator = x; |
| this._columns.setCalculator(this._calculator); |
| } |
| this._calculator.reset(); |
| |
| if (this._calculator.startAtZero) { |
| this._columns.hideEventDividers(); |
| } else { |
| this._columns.showEventDividers(); |
| } |
| |
| this._invalidateAllItems(); |
| } |
| |
| _loadEventFired(event: Common.EventTarget.EventTargetEvent): void { |
| if (!this._recording) { |
| return; |
| } |
| |
| const time = (event.data.loadTime as number); |
| if (time) { |
| this._mainRequestLoadTime = time; |
| this._columns.addEventDividers([time], 'network-load-divider'); |
| } |
| } |
| |
| _domContentLoadedEventFired(event: Common.EventTarget.EventTargetEvent): void { |
| if (!this._recording) { |
| return; |
| } |
| const data = (event.data as number); |
| if (data) { |
| this._mainRequestDOMContentLoadedTime = data; |
| this._columns.addEventDividers([data], 'network-dcl-divider'); |
| } |
| } |
| |
| wasShown(): void { |
| this._refreshIfNeeded(); |
| this._columns.wasShown(); |
| } |
| |
| willHide(): void { |
| this._columns.willHide(); |
| } |
| |
| onResize(): void { |
| this._rowHeight = this._computeRowHeight(); |
| } |
| |
| flatNodesList(): NetworkNode[] { |
| const rootNode = |
| (this._dataGrid.rootNode() as |
| DataGrid.ViewportDataGrid.ViewportDataGridNode<DataGrid.SortableDataGrid.SortableDataGridNode<NetworkNode>>); |
| return rootNode.flatChildren() as NetworkNode[]; |
| } |
| |
| _onDataGridFocus(): void { |
| if (this._dataGrid.element.matches(':focus-visible')) { |
| this.element.classList.add('grid-focused'); |
| } |
| this.updateNodeBackground(); |
| } |
| |
| _onDataGridBlur(): void { |
| this.element.classList.remove('grid-focused'); |
| this.updateNodeBackground(); |
| } |
| |
| updateNodeBackground(): void { |
| if (this._dataGrid.selectedNode) { |
| (this._dataGrid.selectedNode as NetworkNode).updateBackgroundColor(); |
| } |
| } |
| |
| updateNodeSelectedClass(isSelected: boolean): void { |
| if (isSelected) { |
| this.element.classList.remove('no-node-selected'); |
| } else { |
| this.element.classList.add('no-node-selected'); |
| } |
| } |
| |
| stylesChanged(): void { |
| this._columns.scheduleRefresh(); |
| } |
| |
| _refresh(): void { |
| this._needsRefresh = false; |
| |
| if (this._refreshRequestId) { |
| this.element.window().cancelAnimationFrame(this._refreshRequestId); |
| this._refreshRequestId = null; |
| } |
| |
| this.removeAllNodeHighlights(); |
| |
| this._timeCalculator.updateBoundariesForEventTime(this._mainRequestLoadTime); |
| this._durationCalculator.updateBoundariesForEventTime(this._mainRequestLoadTime); |
| this._timeCalculator.updateBoundariesForEventTime(this._mainRequestDOMContentLoadedTime); |
| this._durationCalculator.updateBoundariesForEventTime(this._mainRequestDOMContentLoadedTime); |
| |
| const nodesToInsert = new Map<NetworkNode, NetworkNode>(); |
| const nodesToRefresh: NetworkNode[] = []; |
| |
| const staleNodes = new Set<NetworkRequestNode>(); |
| |
| // While creating nodes it may add more entries into _staleRequests because redirect request nodes update the parent |
| // node so we loop until we have no more stale requests. |
| while (this._staleRequests.size) { |
| const request = this._staleRequests.values().next().value; |
| this._staleRequests.delete(request); |
| let node = networkRequestToNode.get(request); |
| if (!node) { |
| node = this._createNodeForRequest(request); |
| } |
| staleNodes.add(node); |
| } |
| |
| for (const node of staleNodes) { |
| const isFilteredOut = !this._applyFilter(node); |
| if (isFilteredOut && node === this._hoveredNode) { |
| this._setHoveredNode(null); |
| } |
| |
| if (!isFilteredOut) { |
| nodesToRefresh.push(node); |
| } |
| const request = node.request(); |
| this._timeCalculator.updateBoundaries(request); |
| this._durationCalculator.updateBoundaries(request); |
| const newParent = this._parentNodeForInsert(node); |
| const wasAlreadyFiltered = filteredNetworkRequests.has(node); |
| if (wasAlreadyFiltered === isFilteredOut && node.parent === newParent) { |
| continue; |
| } |
| if (isFilteredOut) { |
| filteredNetworkRequests.add(node); |
| } else { |
| filteredNetworkRequests.delete(node); |
| } |
| const removeFromParent = node.parent && (isFilteredOut || node.parent !== newParent); |
| if (removeFromParent) { |
| let parent: NetworkNode| |
| (DataGrid.DataGrid.DataGridNode<DataGrid.ViewportDataGrid.ViewportDataGridNode< |
| DataGrid.SortableDataGrid.SortableDataGridNode<NetworkNode>>>| |
| null) = node.parent; |
| if (!parent) { |
| continue; |
| } |
| parent.removeChild(node); |
| while (parent && !parent.hasChildren() && parent.dataGrid && parent.dataGrid.rootNode() !== parent) { |
| const grandparent = (parent.parent as NetworkNode); |
| grandparent.removeChild(parent); |
| parent = grandparent; |
| } |
| } |
| |
| if (!newParent || isFilteredOut) { |
| continue; |
| } |
| |
| if (!newParent.dataGrid && !nodesToInsert.has(newParent)) { |
| nodesToInsert.set(newParent, (this._dataGrid.rootNode() as NetworkNode)); |
| nodesToRefresh.push(newParent); |
| } |
| nodesToInsert.set(node, newParent); |
| } |
| |
| for (const node of nodesToInsert.keys()) { |
| (nodesToInsert.get(node) as NetworkNode).appendChild(node); |
| } |
| |
| for (const node of nodesToRefresh) { |
| node.refresh(); |
| } |
| |
| this._updateSummaryBar(); |
| |
| if (nodesToInsert.size) { |
| this._columns.sortByCurrentColumn(); |
| } |
| |
| this._dataGrid.updateInstantly(); |
| this._didRefreshForTest(); |
| } |
| |
| _didRefreshForTest(): void { |
| } |
| |
| _parentNodeForInsert(node: NetworkRequestNode): NetworkNode|null { |
| if (!this._activeGroupLookup) { |
| return this._dataGrid.rootNode() as NetworkNode; |
| } |
| |
| const groupNode = this._activeGroupLookup.groupNodeForRequest(node.request()); |
| if (!groupNode) { |
| return this._dataGrid.rootNode() as NetworkNode; |
| } |
| return groupNode; |
| } |
| |
| _reset(): void { |
| this.dispatchEventToListeners(Events.RequestActivated, {showPanel: false}); |
| |
| this._setHoveredNode(null); |
| this._columns.reset(); |
| |
| this._timeFilter = null; |
| this._calculator.reset(); |
| |
| this._timeCalculator.setWindow(null); |
| this._linkifier.reset(); |
| |
| if (this._activeGroupLookup) { |
| this._activeGroupLookup.reset(); |
| } |
| this._staleRequests.clear(); |
| this._resetSuggestionBuilder(); |
| |
| this._mainRequestLoadTime = -1; |
| this._mainRequestDOMContentLoadedTime = -1; |
| |
| this._dataGrid.rootNode().removeChildren(); |
| this._updateSummaryBar(); |
| this._dataGrid.setStickToBottom(true); |
| this.scheduleRefresh(); |
| } |
| |
| setTextFilterValue(filterString: string): void { |
| this._textFilterUI.setValue(filterString); |
| this._dataURLFilterUI.setChecked(false); |
| this._onlyIssuesFilterUI.setChecked(false); |
| this._onlyBlockedRequestsUI.setChecked(false); |
| this._resourceCategoryFilterUI.reset(); |
| } |
| |
| _createNodeForRequest(request: SDK.NetworkRequest.NetworkRequest): NetworkRequestNode { |
| const node = new NetworkRequestNode(this, request); |
| networkRequestToNode.set(request, node); |
| filteredNetworkRequests.add(node); |
| |
| for (let redirect = request.redirectSource(); redirect; redirect = redirect.redirectSource()) { |
| this._refreshRequest(redirect); |
| } |
| return node; |
| } |
| |
| _onRequestUpdated(event: Common.EventTarget.EventTargetEvent): void { |
| const request = (event.data as SDK.NetworkRequest.NetworkRequest); |
| this._refreshRequest(request); |
| } |
| |
| _refreshRequest(request: SDK.NetworkRequest.NetworkRequest): void { |
| NetworkLogView._subdomains(request.domain) |
| .forEach(this._suggestionBuilder.addItem.bind(this._suggestionBuilder, FilterType.Domain)); |
| this._suggestionBuilder.addItem(FilterType.Method, request.requestMethod); |
| this._suggestionBuilder.addItem(FilterType.MimeType, request.mimeType); |
| this._suggestionBuilder.addItem(FilterType.Scheme, String(request.scheme)); |
| this._suggestionBuilder.addItem(FilterType.StatusCode, String(request.statusCode)); |
| this._suggestionBuilder.addItem(FilterType.ResourceType, request.resourceType().name()); |
| this._suggestionBuilder.addItem(FilterType.Url, request.securityOrigin()); |
| |
| const priority = request.priority(); |
| if (priority) { |
| this._suggestionBuilder.addItem( |
| FilterType.Priority, PerfUI.NetworkPriorities.uiLabelForNetworkPriority(priority)); |
| } |
| |
| if (request.mixedContentType !== Protocol.Security.MixedContentType.None) { |
| this._suggestionBuilder.addItem(FilterType.MixedContent, MixedContentFilterValues.All); |
| } |
| |
| if (request.mixedContentType === Protocol.Security.MixedContentType.OptionallyBlockable) { |
| this._suggestionBuilder.addItem(FilterType.MixedContent, MixedContentFilterValues.Displayed); |
| } |
| |
| if (request.mixedContentType === Protocol.Security.MixedContentType.Blockable) { |
| const suggestion = |
| request.wasBlocked() ? MixedContentFilterValues.Blocked : MixedContentFilterValues.BlockOverridden; |
| this._suggestionBuilder.addItem(FilterType.MixedContent, suggestion); |
| } |
| |
| const responseHeaders = request.responseHeaders; |
| for (let i = 0, l = responseHeaders.length; i < l; ++i) { |
| this._suggestionBuilder.addItem(FilterType.HasResponseHeader, responseHeaders[i].name); |
| } |
| |
| for (const cookie of request.responseCookies) { |
| this._suggestionBuilder.addItem(FilterType.SetCookieDomain, cookie.domain()); |
| this._suggestionBuilder.addItem(FilterType.SetCookieName, cookie.name()); |
| this._suggestionBuilder.addItem(FilterType.SetCookieValue, cookie.value()); |
| } |
| |
| for (const cookie of request.allCookiesIncludingBlockedOnes()) { |
| this._suggestionBuilder.addItem(FilterType.CookieDomain, cookie.domain()); |
| this._suggestionBuilder.addItem(FilterType.CookieName, cookie.name()); |
| this._suggestionBuilder.addItem(FilterType.CookiePath, cookie.path()); |
| this._suggestionBuilder.addItem(FilterType.CookieValue, cookie.value()); |
| } |
| |
| this._staleRequests.add(request); |
| this.scheduleRefresh(); |
| } |
| |
| rowHeight(): number { |
| return this._rowHeight; |
| } |
| |
| switchViewMode(gridMode: boolean): void { |
| this._columns.switchViewMode(gridMode); |
| } |
| |
| handleContextMenuForRequest(contextMenu: UI.ContextMenu.ContextMenu, request: SDK.NetworkRequest.NetworkRequest): |
| void { |
| contextMenu.appendApplicableItems(request); |
| let copyMenu = contextMenu.clipboardSection().appendSubMenuItem(i18nString(UIStrings.copy)); |
| const footerSection = copyMenu.footerSection(); |
| if (request) { |
| copyMenu.defaultSection().appendItem( |
| UI.UIUtils.copyLinkAddressLabel(), |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText.bind( |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance, request.contentURL())); |
| if (request.requestHeadersText()) { |
| copyMenu.defaultSection().appendItem( |
| i18nString(UIStrings.copyRequestHeaders), NetworkLogView._copyRequestHeaders.bind(null, request)); |
| } |
| |
| if (request.responseHeadersText) { |
| copyMenu.defaultSection().appendItem( |
| i18nString(UIStrings.copyResponseHeaders), NetworkLogView._copyResponseHeaders.bind(null, request)); |
| } |
| |
| if (request.finished) { |
| copyMenu.defaultSection().appendItem( |
| i18nString(UIStrings.copyResponse), NetworkLogView._copyResponse.bind(null, request)); |
| } |
| |
| const initiator = request.initiator(); |
| |
| if (initiator) { |
| const stack = initiator.stack; |
| if (stack) { |
| // We proactively compute the stacktrace text, as we can't determine whether the stacktrace |
| // has any context solely based on the top frame. Sometimes, the top frame does not have |
| // any callFrames, but its parent frames do. |
| const stackTraceText = computeStackTraceText(stack); |
| if (stackTraceText !== '') { |
| copyMenu.defaultSection().appendItem(i18nString(UIStrings.copyStacktrace), () => { |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(stackTraceText); |
| }); |
| } |
| } |
| } |
| |
| const disableIfBlob = request.isBlobRequest(); |
| if (Host.Platform.isWin()) { |
| footerSection.appendItem( |
| i18nString(UIStrings.copyAsPowershell), this._copyPowerShellCommand.bind(this, request), disableIfBlob); |
| footerSection.appendItem( |
| i18nString(UIStrings.copyAsFetch), this._copyFetchCall.bind(this, request, false), disableIfBlob); |
| footerSection.appendItem( |
| i18nString(UIStrings.copyAsNodejsFetch), this._copyFetchCall.bind(this, request, true), disableIfBlob); |
| footerSection.appendItem( |
| i18nString(UIStrings.copyAsCurlCmd), this._copyCurlCommand.bind(this, request, 'win'), disableIfBlob); |
| footerSection.appendItem( |
| i18nString(UIStrings.copyAsCurlBash), this._copyCurlCommand.bind(this, request, 'unix'), disableIfBlob); |
| footerSection.appendItem(i18nString(UIStrings.copyAllAsPowershell), this._copyAllPowerShellCommand.bind(this)); |
| footerSection.appendItem(i18nString(UIStrings.copyAllAsFetch), this._copyAllFetchCall.bind(this, false)); |
| footerSection.appendItem(i18nString(UIStrings.copyAllAsNodejsFetch), this._copyAllFetchCall.bind(this, true)); |
| footerSection.appendItem(i18nString(UIStrings.copyAllAsCurlCmd), this._copyAllCurlCommand.bind(this, 'win')); |
| footerSection.appendItem(i18nString(UIStrings.copyAllAsCurlBash), this._copyAllCurlCommand.bind(this, 'unix')); |
| } else { |
| footerSection.appendItem( |
| i18nString(UIStrings.copyAsFetch), this._copyFetchCall.bind(this, request, false), disableIfBlob); |
| footerSection.appendItem( |
| i18nString(UIStrings.copyAsNodejsFetch), this._copyFetchCall.bind(this, request, true), disableIfBlob); |
| footerSection.appendItem( |
| i18nString(UIStrings.copyAsCurl), this._copyCurlCommand.bind(this, request, 'unix'), disableIfBlob); |
| footerSection.appendItem(i18nString(UIStrings.copyAllAsFetch), this._copyAllFetchCall.bind(this, false)); |
| footerSection.appendItem(i18nString(UIStrings.copyAllAsNodejsFetch), this._copyAllFetchCall.bind(this, true)); |
| footerSection.appendItem(i18nString(UIStrings.copyAllAsCurl), this._copyAllCurlCommand.bind(this, 'unix')); |
| } |
| } else { |
| copyMenu = contextMenu.clipboardSection().appendSubMenuItem(i18nString(UIStrings.copy)); |
| } |
| footerSection.appendItem(i18nString(UIStrings.copyAllAsHar), this._copyAll.bind(this)); |
| |
| contextMenu.saveSection().appendItem(i18nString(UIStrings.saveAllAsHarWithContent), this.exportAll.bind(this)); |
| |
| contextMenu.editSection().appendItem(i18nString(UIStrings.clearBrowserCache), this._clearBrowserCache.bind(this)); |
| contextMenu.editSection().appendItem( |
| i18nString(UIStrings.clearBrowserCookies), this._clearBrowserCookies.bind(this)); |
| |
| if (request) { |
| const maxBlockedURLLength = 20; |
| const manager = SDK.NetworkManager.MultitargetNetworkManager.instance(); |
| let patterns = manager.blockedPatterns(); |
| |
| function addBlockedURL(url: string): void { |
| patterns.push({enabled: true, url: url}); |
| manager.setBlockedPatterns(patterns); |
| manager.setBlockingEnabled(true); |
| UI.ViewManager.ViewManager.instance().showView('network.blocked-urls'); |
| } |
| |
| function removeBlockedURL(url: string): void { |
| patterns = patterns.filter(pattern => pattern.url !== url); |
| manager.setBlockedPatterns(patterns); |
| UI.ViewManager.ViewManager.instance().showView('network.blocked-urls'); |
| } |
| |
| const urlWithoutScheme = request.parsedURL.urlWithoutScheme(); |
| if (urlWithoutScheme && !patterns.find(pattern => pattern.url === urlWithoutScheme)) { |
| contextMenu.debugSection().appendItem( |
| i18nString(UIStrings.blockRequestUrl), addBlockedURL.bind(null, urlWithoutScheme)); |
| } else if (urlWithoutScheme) { |
| const croppedURL = Platform.StringUtilities.trimMiddle(urlWithoutScheme, maxBlockedURLLength); |
| contextMenu.debugSection().appendItem( |
| i18nString(UIStrings.unblockS, {PH1: croppedURL}), removeBlockedURL.bind(null, urlWithoutScheme)); |
| } |
| |
| const domain = request.parsedURL.domain(); |
| if (domain && !patterns.find(pattern => pattern.url === domain)) { |
| contextMenu.debugSection().appendItem( |
| i18nString(UIStrings.blockRequestDomain), addBlockedURL.bind(null, domain)); |
| } else if (domain) { |
| const croppedDomain = Platform.StringUtilities.trimMiddle(domain, maxBlockedURLLength); |
| contextMenu.debugSection().appendItem( |
| i18nString(UIStrings.unblockS, {PH1: croppedDomain}), removeBlockedURL.bind(null, domain)); |
| } |
| |
| if (SDK.NetworkManager.NetworkManager.canReplayRequest(request)) { |
| contextMenu.debugSection().appendItem( |
| i18nString(UIStrings.replayXhr), SDK.NetworkManager.NetworkManager.replayRequest.bind(null, request)); |
| } |
| } |
| } |
| |
| _harRequests(): SDK.NetworkRequest.NetworkRequest[] { |
| return Logs.NetworkLog.NetworkLog.instance() |
| .requests() |
| .filter(NetworkLogView.HTTPRequestsFilter) |
| .filter(request => { |
| return request.finished || |
| (request.resourceType() === Common.ResourceType.resourceTypes.WebSocket && request.responseReceivedTime); |
| }); |
| } |
| |
| async _copyAll(): Promise<void> { |
| const harArchive = {log: await HAR.Log.Log.build(this._harRequests())}; |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(JSON.stringify(harArchive, null, 2)); |
| } |
| |
| async _copyCurlCommand(request: SDK.NetworkRequest.NetworkRequest, platform: string): Promise<void> { |
| const command = await this._generateCurlCommand(request, platform); |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(command); |
| } |
| |
| async _copyAllCurlCommand(platform: string): Promise<void> { |
| const commands = await this._generateAllCurlCommand(Logs.NetworkLog.NetworkLog.instance().requests(), platform); |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(commands); |
| } |
| |
| async _copyFetchCall(request: SDK.NetworkRequest.NetworkRequest, includeCookies: boolean): Promise<void> { |
| const command = await this._generateFetchCall(request, includeCookies); |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(command); |
| } |
| |
| async _copyAllFetchCall(includeCookies: boolean): Promise<void> { |
| const commands = await this._generateAllFetchCall(Logs.NetworkLog.NetworkLog.instance().requests(), includeCookies); |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(commands); |
| } |
| |
| async _copyPowerShellCommand(request: SDK.NetworkRequest.NetworkRequest): Promise<void> { |
| const command = await this._generatePowerShellCommand(request); |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(command); |
| } |
| |
| async _copyAllPowerShellCommand(): Promise<void> { |
| const commands = await this._generateAllPowerShellCommand(Logs.NetworkLog.NetworkLog.instance().requests()); |
| Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(commands); |
| } |
| |
| async exportAll(): Promise<void> { |
| const mainTarget = SDK.TargetManager.TargetManager.instance().mainTarget(); |
| if (!mainTarget) { |
| return; |
| } |
| const url = mainTarget.inspectedURL(); |
| const parsedURL = Common.ParsedURL.ParsedURL.fromString(url); |
| const filename = parsedURL ? parsedURL.host : 'network-log'; |
| const stream = new Bindings.FileUtils.FileOutputStream(); |
| |
| if (!await stream.open(filename + '.har')) { |
| return; |
| } |
| |
| const progressIndicator = new UI.ProgressIndicator.ProgressIndicator(); |
| this._progressBarContainer.appendChild(progressIndicator.element); |
| await HAR.Writer.Writer.write(stream, this._harRequests(), progressIndicator); |
| progressIndicator.done(); |
| stream.close(); |
| } |
| |
| _clearBrowserCache(): void { |
| if (confirm(i18nString(UIStrings.areYouSureYouWantToClearBrowser))) { |
| SDK.NetworkManager.MultitargetNetworkManager.instance().clearBrowserCache(); |
| } |
| } |
| |
| _clearBrowserCookies(): void { |
| if (confirm(i18nString(UIStrings.areYouSureYouWantToClearBrowserCookies))) { |
| SDK.NetworkManager.MultitargetNetworkManager.instance().clearBrowserCookies(); |
| } |
| } |
| |
| _removeAllHighlights(): void { |
| this.removeAllNodeHighlights(); |
| for (let i = 0; i < this._highlightedSubstringChanges.length; ++i) { |
| UI.UIUtils.revertDomChanges(this._highlightedSubstringChanges[i]); |
| } |
| this._highlightedSubstringChanges = []; |
| } |
| |
| _applyFilter(node: NetworkRequestNode): boolean { |
| const request = node.request(); |
| if (this._timeFilter && !this._timeFilter(request)) { |
| return false; |
| } |
| const categoryName = request.resourceType().category().title(); |
| if (!this._resourceCategoryFilterUI.accept(categoryName)) { |
| return false; |
| } |
| if (this._dataURLFilterUI.checked() && (request.parsedURL.isDataURL() || request.parsedURL.isBlobURL())) { |
| return false; |
| } |
| if (this._onlyIssuesFilterUI.checked() && |
| !IssuesManager.RelatedIssue.hasIssueOfCategory(request, IssuesManager.Issue.IssueCategory.SameSiteCookie)) { |
| return false; |
| } |
| if (this._onlyBlockedRequestsUI.checked() && !request.wasBlocked() && !request.corsErrorStatus()) { |
| return false; |
| } |
| for (let i = 0; i < this._filters.length; ++i) { |
| if (!this._filters[i](request)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| _parseFilterQuery(query: string): void { |
| const descriptors = this._filterParser.parse(query); |
| this._filters = descriptors.map(descriptor => { |
| const key = descriptor.key; |
| const text = descriptor.text || ''; |
| const regex = descriptor.regex; |
| let filter; |
| if (key) { |
| const defaultText = Platform.StringUtilities.escapeForRegExp(key + ':' + text); |
| filter = this._createSpecialFilter((key as FilterType), text) || |
| NetworkLogView._requestPathFilter.bind(null, new RegExp(defaultText, 'i')); |
| } else if (descriptor.regex) { |
| filter = NetworkLogView._requestPathFilter.bind(null, (regex as RegExp)); |
| } else { |
| filter = NetworkLogView._requestPathFilter.bind( |
| null, new RegExp(Platform.StringUtilities.escapeForRegExp(text), 'i')); |
| } |
| return descriptor.negative ? NetworkLogView._negativeFilter.bind(null, filter) : filter; |
| }); |
| } |
| |
| _createSpecialFilter(type: FilterType, value: string): Filter|null { |
| switch (type) { |
| case FilterType.Domain: |
| return NetworkLogView._createRequestDomainFilter(value); |
| |
| case FilterType.HasResponseHeader: |
| return NetworkLogView._requestResponseHeaderFilter.bind(null, value); |
| |
| case FilterType.Is: |
| if (value.toLowerCase() === IsFilterType.Running) { |
| return NetworkLogView._runningRequestFilter; |
| } |
| if (value.toLowerCase() === IsFilterType.FromCache) { |
| return NetworkLogView._fromCacheRequestFilter; |
| } |
| if (value.toLowerCase() === IsFilterType.ServiceWorkerIntercepted) { |
| return NetworkLogView._interceptedByServiceWorkerFilter; |
| } |
| if (value.toLowerCase() === IsFilterType.ServiceWorkerInitiated) { |
| return NetworkLogView._initiatedByServiceWorkerFilter; |
| } |
| break; |
| |
| case FilterType.LargerThan: |
| return this._createSizeFilter(value.toLowerCase()); |
| |
| case FilterType.Method: |
| return NetworkLogView._requestMethodFilter.bind(null, value); |
| |
| case FilterType.MimeType: |
| return NetworkLogView._requestMimeTypeFilter.bind(null, value); |
| |
| case FilterType.MixedContent: |
| return NetworkLogView._requestMixedContentFilter.bind(null, (value as MixedContentFilterValues)); |
| |
| case FilterType.Scheme: |
| return NetworkLogView._requestSchemeFilter.bind(null, value); |
| |
| case FilterType.SetCookieDomain: |
| return NetworkLogView._requestSetCookieDomainFilter.bind(null, value); |
| |
| case FilterType.SetCookieName: |
| return NetworkLogView._requestSetCookieNameFilter.bind(null, value); |
| |
| case FilterType.SetCookieValue: |
| return NetworkLogView._requestSetCookieValueFilter.bind(null, value); |
| |
| case FilterType.CookieDomain: |
| return NetworkLogView._requestCookieDomainFilter.bind(null, value); |
| |
| case FilterType.CookieName: |
| return NetworkLogView._requestCookieNameFilter.bind(null, value); |
| |
| case FilterType.CookiePath: |
| return NetworkLogView._requestCookiePathFilter.bind(null, value); |
| |
| case FilterType.CookieValue: |
| return NetworkLogView._requestCookieValueFilter.bind(null, value); |
| |
| case FilterType.Priority: |
| return NetworkLogView._requestPriorityFilter.bind( |
| null, PerfUI.NetworkPriorities.uiLabelToNetworkPriority(value)); |
| |
| case FilterType.StatusCode: |
| return NetworkLogView._statusCodeFilter.bind(null, value); |
| |
| case FilterType.ResourceType: |
| return NetworkLogView._resourceTypeFilter.bind(null, value); |
| |
| case FilterType.Url: |
| return NetworkLogView._requestUrlFilter.bind(null, value); |
| } |
| return null; |
| } |
| |
| _createSizeFilter(value: string): Filter|null { |
| let multiplier = 1; |
| if (value.endsWith('k')) { |
| multiplier = 1000; |
| value = value.substring(0, value.length - 1); |
| } else if (value.endsWith('m')) { |
| multiplier = 1000 * 1000; |
| value = value.substring(0, value.length - 1); |
| } |
| const quantity = Number(value); |
| if (isNaN(quantity)) { |
| return null; |
| } |
| return NetworkLogView._requestSizeLargerThanFilter.bind(null, quantity * multiplier); |
| } |
| |
| _filterRequests(): void { |
| this._removeAllHighlights(); |
| this._invalidateAllItems(); |
| } |
| |
| _reveal(request: SDK.NetworkRequest.NetworkRequest): NetworkRequestNode|null { |
| this.removeAllNodeHighlights(); |
| const node = networkRequestToNode.get(request); |
| if (!node || !node.dataGrid) { |
| return null; |
| } |
| // Viewport datagrid nodes do not reveal if not in the root node |
| // list of flatChildren. For children of grouped frame nodes: |
| // reveal and expand parent to ensure child is revealable. |
| if (node.parent && node.parent instanceof NetworkGroupNode) { |
| node.parent.reveal(); |
| node.parent.expand(); |
| } |
| node.reveal(); |
| return node; |
| } |
| |
| revealAndHighlightRequest(request: SDK.NetworkRequest.NetworkRequest): void { |
| const node = this._reveal(request); |
| if (node) { |
| this._highlightNode(node); |
| } |
| } |
| |
| selectRequest(request: SDK.NetworkRequest.NetworkRequest, options?: FilterOptions): void { |
| const defaultOptions = {clearFilter: true}; |
| const {clearFilter} = options || defaultOptions; |
| if (clearFilter) { |
| this.setTextFilterValue(''); |
| } |
| const node = this._reveal(request); |
| if (node) { |
| node.select(); |
| } |
| } |
| |
| removeAllNodeHighlights(): void { |
| if (this._highlightedNode) { |
| this._highlightedNode.element().classList.remove('highlighted-row'); |
| this._highlightedNode = null; |
| } |
| } |
| |
| _highlightNode(node: NetworkRequestNode): void { |
| UI.UIUtils.runCSSAnimationOnce(node.element(), 'highlighted-row'); |
| this._highlightedNode = node; |
| } |
| |
| _filterOutBlobRequests(requests: SDK.NetworkRequest.NetworkRequest[]): SDK.NetworkRequest.NetworkRequest[] { |
| return requests.filter(request => !request.isBlobRequest()); |
| } |
| |
| async _generateFetchCall(request: SDK.NetworkRequest.NetworkRequest, includeCookies: boolean): Promise<string> { |
| const ignoredHeaders = new Set<string>([ |
| // Internal headers |
| 'method', |
| 'path', |
| 'scheme', |
| 'version', |
| |
| // Unsafe headers |
| // Keep this list synchronized with src/net/http/http_util.cc |
| 'accept-charset', |
| 'accept-encoding', |
| 'access-control-request-headers', |
| 'access-control-request-method', |
| 'connection', |
| 'content-length', |
| 'cookie', |
| 'cookie2', |
| 'date', |
| 'dnt', |
| 'expect', |
| 'host', |
| 'keep-alive', |
| 'origin', |
| 'referer', |
| 'te', |
| 'trailer', |
| 'transfer-encoding', |
| 'upgrade', |
| 'via', |
| // TODO(phistuck) - remove this once crbug.com/571722 is fixed. |
| 'user-agent', |
| ]); |
| |
| const credentialHeaders = new Set<string>(['cookie', 'authorization']); |
| |
| const url = JSON.stringify(request.url()); |
| |
| const requestHeaders = request.requestHeaders(); |
| const headerData: Headers = requestHeaders.reduce((result, header) => { |
| const name = header.name; |
| |
| if (!ignoredHeaders.has(name.toLowerCase()) && !name.includes(':')) { |
| result.append(name, header.value); |
| } |
| |
| return result; |
| }, new Headers()); |
| |
| const headers: HeadersInit = {}; |
| for (const headerArray of headerData) { |
| headers[headerArray[0]] = headerArray[1]; |
| } |
| |
| const credentials = request.includedRequestCookies().length || |
| requestHeaders.some(({name}) => credentialHeaders.has(name.toLowerCase())) ? |
| 'include' : |
| 'omit'; |
| |
| const referrerHeader = requestHeaders.find(({name}) => name.toLowerCase() === 'referer'); |
| |
| const referrer = referrerHeader ? referrerHeader.value : void 0; |
| |
| const referrerPolicy = request.referrerPolicy() || void 0; |
| |
| const requestBody = await request.requestFormData(); |
| |
| const fetchOptions: RequestInit = { |
| headers: Object.keys(headers).length ? headers : void 0, |
| referrer, |
| referrerPolicy, |
| body: requestBody, |
| method: request.requestMethod, |
| mode: 'cors', |
| }; |
| |
| if (includeCookies) { |
| const cookieHeader = requestHeaders.find(header => header.name.toLowerCase() === 'cookie'); |
| if (cookieHeader) { |
| fetchOptions.headers = { |
| ...headers, |
| 'cookie': cookieHeader.value, |
| }; |
| } |
| } else { |
| fetchOptions.credentials = credentials; |
| } |
| |
| const options = JSON.stringify(fetchOptions, null, 2); |
| return `fetch(${url}, ${options});`; |
| } |
| |
| async _generateAllFetchCall(requests: SDK.NetworkRequest.NetworkRequest[], includeCookies: boolean): Promise<string> { |
| const nonBlobRequests = this._filterOutBlobRequests(requests); |
| const commands = |
| await Promise.all(nonBlobRequests.map(request => this._generateFetchCall(request, includeCookies))); |
| return commands.join(' ;\n'); |
| } |
| |
| async _generateCurlCommand(request: SDK.NetworkRequest.NetworkRequest, platform: string): Promise<string> { |
| let command: string[] = []; |
| // Most of these headers are derived from the URL and are automatically added by cURL. |
| // The |Accept-Encoding| header is ignored to prevent decompression errors. crbug.com/1015321 |
| const ignoredHeaders = new Set<string>(['accept-encoding', 'host', 'method', 'path', 'scheme', 'version']); |
| |
| function escapeStringWin(str: string): string { |
| /* If there are no new line characters do not escape the " characters |
| since it only uglifies the command. |
| |
| Because cmd.exe parser and MS Crt arguments parsers use some of the |
| same escape characters, they can interact with each other in |
| horrible ways, the order of operations is critical. |
| |
| Replace \ with \\ first because it is an escape character for certain |
| conditions in both parsers. |
| |
| Replace all " with \" to ensure the first parser does not remove it. |
| |
| Then escape all characters we are not sure about with ^ to ensure it |
| gets to MS Crt parser safely. |
| |
| The % character is special because MS Crt parser will try and look for |
| ENV variables and fill them in it's place. We cannot escape them with % |
| and cannot escape them with ^ (because it's cmd.exe's escape not MS Crt |
| parser); So we can get cmd.exe parser to escape the character after it, |
| if it is followed by a valid beginning character of an ENV variable. |
| This ensures we do not try and double escape another ^ if it was placed |
| by the previous replace. |
| |
| Lastly we replace new lines with ^ and TWO new lines because the first |
| new line is there to enact the escape command the second is the character |
| to escape (in this case new line). |
| */ |
| const encapsChars = /[\r\n]/.test(str) ? '^"' : '"'; |
| return encapsChars + |
| str.replace(/\\/g, '\\\\') |
| .replace(/"/g, '\\"') |
| .replace(/[^a-zA-Z0-9\s_\-:=+~'\/.',?;()*`&]/g, '^$&') |
| .replace(/%(?=[a-zA-Z0-9_])/g, '%^') |
| .replace(/\r?\n/g, '^\n\n') + |
| encapsChars; |
| } |
| |
| function escapeStringPosix(str: string): string { |
| function escapeCharacter(x: string): string { |
| const code = x.charCodeAt(0); |
| let hexString = code.toString(16); |
| // Zero pad to four digits to comply with ANSI-C Quoting: |
| // http://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html |
| while (hexString.length < 4) { |
| hexString = '0' + hexString; |
| } |
| |
| return '\\u' + hexString; |
| } |
| |
| if (/[\0-\x1F\x7F-\x9F!]|\'/.test(str)) { |
| // Use ANSI-C quoting syntax. |
| return '$\'' + |
| str.replace(/\\/g, '\\\\') |
| .replace(/\'/g, '\\\'') |
| .replace(/\n/g, '\\n') |
| .replace(/\r/g, '\\r') |
| .replace(/[\0-\x1F\x7F-\x9F!]/g, escapeCharacter) + |
| '\''; |
| } |
| // Use single quote syntax. |
| return '\'' + str + '\''; |
| } |
| |
| // cURL command expected to run on the same platform that DevTools run |
| // (it may be different from the inspected page platform). |
| const escapeString = platform === 'win' ? escapeStringWin : escapeStringPosix; |
| |
| command.push(escapeString(request.url()).replace(/[[{}\]]/g, '\\$&')); |
| |
| let inferredMethod = 'GET'; |
| const data = []; |
| const formData = await request.requestFormData(); |
| if (formData) { |
| // Note that formData is not necessarily urlencoded because it might for example |
| // come from a fetch request made with an explicitly unencoded body. |
| data.push('--data-raw ' + escapeString(formData)); |
| ignoredHeaders.add('content-length'); |
| inferredMethod = 'POST'; |
| } |
| |
| if (request.requestMethod !== inferredMethod) { |
| command.push('-X ' + escapeString(request.requestMethod)); |
| } |
| |
| const requestHeaders = request.requestHeaders(); |
| for (let i = 0; i < requestHeaders.length; i++) { |
| const header = requestHeaders[i]; |
| const name = header.name.replace(/^:/, ''); // Translate SPDY v3 headers to HTTP headers. |
| if (ignoredHeaders.has(name.toLowerCase())) { |
| continue; |
| } |
| command.push('-H ' + escapeString(name + ': ' + header.value)); |
| } |
| command = command.concat(data); |
| command.push('--compressed'); |
| |
| if (request.securityState() === Protocol.Security.SecurityState.Insecure) { |
| command.push('--insecure'); |
| } |
| return 'curl ' + command.join(command.length >= 3 ? (platform === 'win' ? ' ^\n ' : ' \\\n ') : ' '); |
| } |
| |
| async _generateAllCurlCommand(requests: SDK.NetworkRequest.NetworkRequest[], platform: string): Promise<string> { |
| const nonBlobRequests = this._filterOutBlobRequests(requests); |
| const commands = await Promise.all(nonBlobRequests.map(request => this._generateCurlCommand(request, platform))); |
| if (platform === 'win') { |
| return commands.join(' &\r\n'); |
| } |
| return commands.join(' ;\n'); |
| } |
| |
| async _generatePowerShellCommand(request: SDK.NetworkRequest.NetworkRequest): Promise<string> { |
| const command = []; |
| const ignoredHeaders = new Set<string>( |
| ['host', 'connection', 'proxy-connection', 'content-length', 'expect', 'range', 'content-type']); |
| |
| function escapeString(str: string): string { |
| return '"' + |
| str.replace(/[`\$"]/g, '`$&').replace(/[^\x20-\x7E]/g, char => '$([char]' + char.charCodeAt(0) + ')') + '"'; |
| } |
| |
| command.push('-Uri ' + escapeString(request.url())); |
| |
| if (request.requestMethod !== 'GET') { |
| command.push('-Method ' + escapeString(request.requestMethod)); |
| } |
| |
| const requestHeaders = request.requestHeaders(); |
| const headerNameValuePairs = []; |
| for (const header of requestHeaders) { |
| const name = header.name.replace(/^:/, ''); // Translate h2 headers to HTTP headers. |
| if (ignoredHeaders.has(name.toLowerCase())) { |
| continue; |
| } |
| headerNameValuePairs.push(escapeString(name) + '=' + escapeString(header.value)); |
| } |
| if (headerNameValuePairs.length) { |
| command.push('-Headers @{\n' + headerNameValuePairs.join('\n ') + '\n}'); |
| } |
| |
| const contentTypeHeader = requestHeaders.find(({name}) => name.toLowerCase() === 'content-type'); |
| if (contentTypeHeader) { |
| command.push('-ContentType ' + escapeString(contentTypeHeader.value)); |
| } |
| |
| const formData = await request.requestFormData(); |
| if (formData) { |
| const body = escapeString(formData); |
| if (/[^\x20-\x7E]/.test(formData)) { |
| command.push('-Body ([System.Text.Encoding]::UTF8.GetBytes(' + body + '))'); |
| } else { |
| command.push('-Body ' + body); |
| } |
| } |
| |
| return 'Invoke-WebRequest ' + command.join(command.length >= 3 ? ' `\n' : ' '); |
| } |
| |
| async _generateAllPowerShellCommand(requests: SDK.NetworkRequest.NetworkRequest[]): Promise<string> { |
| const nonBlobRequests = this._filterOutBlobRequests(requests); |
| const commands = await Promise.all(nonBlobRequests.map(request => this._generatePowerShellCommand(request))); |
| return commands.join(';\r\n'); |
| } |
| |
| static getDCLEventColor(): string { |
| if (ThemeSupport.ThemeSupport.instance().themeName() === 'dark') { |
| return '#03A9F4'; |
| } |
| return '#0867CB'; |
| } |
| |
| static getLoadEventColor(): string { |
| return ThemeSupport.ThemeSupport.instance().patchColorText( |
| '#B31412', ThemeSupport.ThemeSupport.ColorUsage.Foreground); |
| } |
| } |
| |
| export function computeStackTraceText(stackTrace: Protocol.Runtime.StackTrace): string { |
| let stackTraceText = ''; |
| for (const frame of stackTrace.callFrames) { |
| const functionName = UI.UIUtils.beautifyFunctionName(frame.functionName); |
| stackTraceText += `${functionName} @ ${frame.url}:${frame.lineNumber + 1}\n`; |
| } |
| if (stackTrace.parent) { |
| stackTraceText += computeStackTraceText(stackTrace.parent); |
| } |
| return stackTraceText; |
| } |
| |
| const filteredNetworkRequests = new WeakSet<NetworkRequestNode>(); |
| const networkRequestToNode = new WeakMap<SDK.NetworkRequest.NetworkRequest, NetworkRequestNode>(); |
| |
| export function isRequestFilteredOut(request: NetworkRequestNode): boolean { |
| return filteredNetworkRequests.has(request); |
| } |
| |
| export const HTTPSchemas = { |
| 'http': true, |
| 'https': true, |
| 'ws': true, |
| 'wss': true, |
| }; |
| |
| // TODO(crbug.com/1167717): Make this a const enum again |
| // eslint-disable-next-line rulesdir/const_enum |
| export enum FilterType { |
| Domain = 'domain', |
| HasResponseHeader = 'has-response-header', |
| Is = 'is', |
| LargerThan = 'larger-than', |
| Method = 'method', |
| MimeType = 'mime-type', |
| MixedContent = 'mixed-content', |
| Priority = 'priority', |
| Scheme = 'scheme', |
| SetCookieDomain = 'set-cookie-domain', |
| SetCookieName = 'set-cookie-name', |
| SetCookieValue = 'set-cookie-value', |
| ResourceType = 'resource-type', |
| CookieDomain = 'cookie-domain', |
| CookieName = 'cookie-name', |
| CookiePath = 'cookie-path', |
| CookieValue = 'cookie-value', |
| StatusCode = 'status-code', |
| Url = 'url', |
| } |
| |
| // TODO(crbug.com/1167717): Make this a const enum again |
| // eslint-disable-next-line rulesdir/const_enum |
| export enum MixedContentFilterValues { |
| All = 'all', |
| Displayed = 'displayed', |
| Blocked = 'blocked', |
| BlockOverridden = 'block-overridden', |
| } |
| |
| // TODO(crbug.com/1167717): Make this a const enum again |
| // eslint-disable-next-line rulesdir/const_enum |
| export enum IsFilterType { |
| Running = 'running', |
| FromCache = 'from-cache', |
| ServiceWorkerIntercepted = 'service-worker-intercepted', |
| ServiceWorkerInitiated = 'service-worker-initiated', |
| } |
| |
| // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration) |
| // eslint-disable-next-line @typescript-eslint/naming-convention |
| export const _searchKeys: string[] = Object.values(FilterType); |
| |
| export interface GroupLookupInterface { |
| groupNodeForRequest(request: SDK.NetworkRequest.NetworkRequest): NetworkGroupNode|null; |
| reset(): void; |
| } |
| |
| export type Filter = (request: SDK.NetworkRequest.NetworkRequest) => boolean; |