UNPKG

lighthouse

Version:

Automated auditing, performance metrics, and best practices for the web.

353 lines (311 loc) • 12.8 kB
/** * @license * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import {EventEmitter} from 'events'; import log from 'lighthouse-logger'; import * as LH from '../../types/lh.js'; import * as Lantern from './lantern/lantern.js'; import {NetworkRequest} from './network-request.js'; /** * @typedef {{ * requeststarted: [NetworkRequest], * requestfinished: [NetworkRequest], * }} NetworkRecorderEventMap */ /** @typedef {LH.Protocol.StrictEventEmitterClass<NetworkRecorderEventMap>} RequestEmitter */ const RequestEventEmitter = /** @type {RequestEmitter} */ (EventEmitter); class NetworkRecorder extends RequestEventEmitter { /** * Creates an instance of NetworkRecorder. */ constructor() { super(); /** @type {NetworkRequest[]} */ this._records = []; /** @type {Map<string, NetworkRequest>} */ this._recordsById = new Map(); } /** * Returns the array of raw network request data without finalizing the initiator and * redirect chain. * @return {Array<NetworkRequest>} */ getRawRecords() { return Array.from(this._records); } /** * Listener for the DevTools SDK NetworkManager's RequestStarted event, which includes both * web socket and normal request creation. * @param {NetworkRequest} request * @private */ onRequestStarted(request) { this._records.push(request); this._recordsById.set(request.requestId, request); this.emit('requeststarted', request); } /** * Listener for the DevTools SDK NetworkManager's RequestFinished event, which includes * request finish, failure, and redirect, as well as the closing of web sockets. * @param {NetworkRequest} request * @private */ onRequestFinished(request) { this.emit('requestfinished', request); } // The below methods proxy network data into the NetworkRequest object which mimics the // DevTools SDK network layer. /** * @param {{params: LH.Crdp.Network.RequestWillBeSentEvent, targetType: LH.Protocol.TargetType, sessionId?: string}} event */ onRequestWillBeSent(event) { const data = event.params; const originalRequest = this._findRealRequestAndSetSession( data.requestId, event.targetType, event.sessionId); // This is a simple new request, create the NetworkRequest object and finish. if (!originalRequest) { const request = new NetworkRequest(); request.onRequestWillBeSent(data); request.sessionId = event.sessionId; request.sessionTargetType = event.targetType; this.onRequestStarted(request); log.verbose('network', `request will be sent to ${request.url}`); return; } // TODO: beacon to Sentry, https://github.com/GoogleChrome/lighthouse/issues/7041 if (!data.redirectResponse) { return; } // On redirect, another requestWillBeSent message is fired for the same requestId. // Update/finish the previous network request and create a new one for the redirect. const modifiedData = { ...data, // Copy over the initiator as well to match DevTools behavior initiator: originalRequest.initiator, requestId: `${originalRequest.requestId}:redirect`, }; const redirectedRequest = new NetworkRequest(); redirectedRequest.onRequestWillBeSent(modifiedData); originalRequest.onRedirectResponse(data); log.verbose('network', `${originalRequest.url} redirected to ${redirectedRequest.url}`); originalRequest.redirectDestination = redirectedRequest; redirectedRequest.redirectSource = originalRequest; // Start the redirect request before finishing the original so we don't get erroneous quiet periods this.onRequestStarted(redirectedRequest); this.onRequestFinished(originalRequest); } /** * @param {{params: LH.Crdp.Network.RequestServedFromCacheEvent, targetType: LH.Protocol.TargetType, sessionId?: string}} event */ onRequestServedFromCache(event) { const data = event.params; const request = this._findRealRequestAndSetSession( data.requestId, event.targetType, event.sessionId); if (!request) return; log.verbose('network', `${request.url} served from cache`); request.onRequestServedFromCache(); } /** * @param {{params: LH.Crdp.Network.ResponseReceivedEvent, targetType: LH.Protocol.TargetType, sessionId?: string}} event */ onResponseReceived(event) { const data = event.params; const request = this._findRealRequestAndSetSession( data.requestId, event.targetType, event.sessionId); if (!request) return; log.verbose('network', `${request.url} response received`); request.onResponseReceived(data); } /** * @param {{params: LH.Crdp.Network.ResponseReceivedExtraInfoEvent, targetType: LH.Protocol.TargetType, sessionId?: string}} event */ onResponseReceivedExtraInfo(event) { const data = event.params; const request = this._findRealRequestAndSetSession( data.requestId, event.targetType, event.sessionId); if (!request) return; log.verbose('network', `${request.url} response received extra info`); request.onResponseReceivedExtraInfo(data); } /** * @param {{params: LH.Crdp.Network.DataReceivedEvent, targetType: LH.Protocol.TargetType, sessionId?: string}} event */ onDataReceived(event) { const data = event.params; const request = this._findRealRequestAndSetSession( data.requestId, event.targetType, event.sessionId); if (!request) return; log.verbose('network', `${request.url} data received`); request.onDataReceived(data); } /** * @param {{params: LH.Crdp.Network.LoadingFinishedEvent, targetType: LH.Protocol.TargetType, sessionId?: string}} event */ onLoadingFinished(event) { const data = event.params; const request = this._findRealRequestAndSetSession( data.requestId, event.targetType, event.sessionId); if (!request) return; log.verbose('network', `${request.url} loading finished`); request.onLoadingFinished(data); this.onRequestFinished(request); } /** * @param {{params: LH.Crdp.Network.LoadingFailedEvent, targetType: LH.Protocol.TargetType, sessionId?: string}} event */ onLoadingFailed(event) { const data = event.params; const request = this._findRealRequestAndSetSession( data.requestId, event.targetType, event.sessionId); if (!request) return; log.verbose('network', `${request.url} loading failed`); request.onLoadingFailed(data); this.onRequestFinished(request); } /** * @param {{params: LH.Crdp.Network.ResourceChangedPriorityEvent, targetType: LH.Protocol.TargetType, sessionId?: string}} event */ onResourceChangedPriority(event) { const data = event.params; const request = this._findRealRequestAndSetSession( data.requestId, event.targetType, event.sessionId); if (!request) return; request.onResourceChangedPriority(data); } /** * Routes network events to their handlers, so we can construct networkRecords * @param {LH.Protocol.RawEventMessage} event */ dispatch(event) { switch (event.method) { case 'Network.requestWillBeSent': return this.onRequestWillBeSent(event); case 'Network.requestServedFromCache': return this.onRequestServedFromCache(event); case 'Network.responseReceived': return this.onResponseReceived(event); case 'Network.responseReceivedExtraInfo': return this.onResponseReceivedExtraInfo(event); case 'Network.dataReceived': return this.onDataReceived(event); case 'Network.loadingFinished': return this.onLoadingFinished(event); case 'Network.loadingFailed': return this.onLoadingFailed(event); case 'Network.resourceChangedPriority': return this.onResourceChangedPriority(event); default: return; } } /** * Redirected requests all have identical requestIds over the protocol. Once a request has been * redirected all future messages referrencing that requestId are about the new destination, not * the original. This method is a helper for finding the real request object to which the current * message is referring. * * @param {string} requestId * @param {LH.Protocol.TargetType} targetType * @param {string|undefined} sessionId * @return {NetworkRequest|undefined} */ _findRealRequestAndSetSession(requestId, targetType, sessionId) { let request = this._recordsById.get(requestId); if (!request || !request.isValid) return undefined; while (request.redirectDestination) { request = request.redirectDestination; } request.setSession(sessionId); request.sessionTargetType = targetType; return request; } /** * @param {NetworkRequest} record The record to find the initiator of * @param {Map<string, NetworkRequest[]>} recordsByURL * @return {NetworkRequest|null} * @private */ static _chooseInitiatorRequest(record, recordsByURL) { if (record.redirectSource) { return record.redirectSource; } const initiatorURL = Lantern.Graph.PageDependencyGraph.getNetworkInitiators(record)[0]; let candidates = recordsByURL.get(initiatorURL) || []; // The (valid) initiator must come before the initiated request. candidates = candidates.filter(c => { return c.responseHeadersEndTime <= record.rendererStartTime && c.finished && !c.failed; }); if (candidates.length > 1) { // Disambiguate based on prefetch. Prefetch requests have type 'Other' and cannot // initiate requests, so we drop them here. const nonPrefetchCandidates = candidates.filter( cand => cand.resourceType !== NetworkRequest.TYPES.Other); if (nonPrefetchCandidates.length) { candidates = nonPrefetchCandidates; } } if (candidates.length > 1) { // Disambiguate based on frame. It's likely that the initiator comes from the same frame. const sameFrameCandidates = candidates.filter(cand => cand.frameId === record.frameId); if (sameFrameCandidates.length) { candidates = sameFrameCandidates; } } if (candidates.length > 1 && record.initiator.type === 'parser') { // Filter to just Documents when initiator type is parser. const documentCandidates = candidates.filter(cand => cand.resourceType === NetworkRequest.TYPES.Document); if (documentCandidates.length) { candidates = documentCandidates; } } if (candidates.length > 1) { // If all real loads came from successful preloads (url preloaded and // loads came from the cache), filter to link rel=preload request(s). const linkPreloadCandidates = candidates.filter(c => c.isLinkPreload); if (linkPreloadCandidates.length) { const nonPreloadCandidates = candidates.filter(c => !c.isLinkPreload); const allPreloaded = nonPreloadCandidates.every(c => c.fromDiskCache || c.fromMemoryCache); if (nonPreloadCandidates.length && allPreloaded) { candidates = linkPreloadCandidates; } } } // Only return an initiator if the result is unambiguous. return candidates.length === 1 ? candidates[0] : null; } /** * Construct network records from a log of devtools protocol messages. * @param {LH.DevtoolsLog} devtoolsLog * @return {Array<LH.Artifacts.NetworkRequest>} */ static recordsFromLogs(devtoolsLog) { const networkRecorder = new NetworkRecorder(); // playback all the devtools messages to recreate network records devtoolsLog.forEach(message => networkRecorder.dispatch(message)); // get out the list of records & filter out invalid records const records = networkRecorder.getRawRecords().filter(record => record.isValid); /** @type {Map<string, NetworkRequest[]>} */ const recordsByURL = new Map(); for (const record of records) { const records = recordsByURL.get(record.url) || []; records.push(record); recordsByURL.set(record.url, records); } // set the initiatorRequest and redirects array for (const record of records) { const initiatorRequest = NetworkRecorder._chooseInitiatorRequest(record, recordsByURL); if (initiatorRequest) { record.setInitiatorRequest(initiatorRequest); } let finalRecord = record; while (finalRecord.redirectDestination) finalRecord = finalRecord.redirectDestination; if (finalRecord === record || finalRecord.redirects) continue; const redirects = []; for ( let redirect = finalRecord.redirectSource; redirect; redirect = redirect.redirectSource ) { redirects.unshift(redirect); } finalRecord.redirects = redirects; } return records; } } export {NetworkRecorder};