Bug 1886451: Add missing ifdef Nightly guards. r=dminor
[gecko.git] / remote / shared / listeners / NetworkEventRecord.sys.mjs
blob72b43e3de1785dbd5b1f4bdd769f591f9ace7ac6
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 const lazy = {};
6 ChromeUtils.defineESModuleGetters(lazy, {
7   NetworkUtils:
8     "resource://devtools/shared/network-observer/NetworkUtils.sys.mjs",
10   TabManager: "chrome://remote/content/shared/TabManager.sys.mjs",
11 });
13 /**
14  * The NetworkEventRecord implements the interface expected from network event
15  * owners for consumers of the DevTools NetworkObserver.
16  *
17  * The NetworkEventRecord emits the before-request-sent event on behalf of the
18  * NetworkListener instance which created it.
19  */
20 export class NetworkEventRecord {
21   #contextId;
22   #fromCache;
23   #isMainDocumentChannel;
24   #networkListener;
25   #redirectCount;
26   #requestChannel;
27   #requestData;
28   #requestId;
29   #responseChannel;
30   #responseData;
31   #wrappedChannel;
33   /**
34    *
35    * @param {object} networkEvent
36    *     The initial network event information (see createNetworkEvent() in
37    *     NetworkUtils.sys.mjs).
38    * @param {nsIChannel} channel
39    *     The nsIChannel behind this network event.
40    * @param {NetworkListener} networkListener
41    *     The NetworkListener which created this NetworkEventRecord.
42    */
43   constructor(networkEvent, channel, networkListener) {
44     this.#requestChannel = channel;
45     this.#responseChannel = null;
47     this.#fromCache = networkEvent.fromCache;
48     this.#isMainDocumentChannel = channel.isMainDocumentChannel;
50     this.#wrappedChannel = ChannelWrapper.get(channel);
52     this.#networkListener = networkListener;
54     // The context ids computed by TabManager have the lifecycle of a navigable
55     // and can be reused for all the events emitted from this record.
56     this.#contextId = this.#getContextId();
58     // The wrappedChannel id remains identical across redirects, whereas
59     // nsIChannel.channelId is different for each and every request.
60     this.#requestId = this.#wrappedChannel.id.toString();
62     const { cookies, headers } =
63       lazy.NetworkUtils.fetchRequestHeadersAndCookies(channel);
65     // See the RequestData type definition for the full list of properties that
66     // should be set on this object.
67     this.#requestData = {
68       bodySize: null,
69       cookies,
70       headers,
71       headersSize: networkEvent.rawHeaders ? networkEvent.rawHeaders.length : 0,
72       method: channel.requestMethod,
73       request: this.#requestId,
74       timings: {},
75       url: channel.URI.spec,
76     };
78     // See the ResponseData type definition for the full list of properties that
79     // should be set on this object.
80     this.#responseData = {
81       // encoded size (body)
82       bodySize: null,
83       content: {
84         // decoded size
85         size: null,
86       },
87       // encoded size (headers)
88       headersSize: null,
89       url: channel.URI.spec,
90     };
92     // NetworkObserver creates a network event when request headers have been
93     // parsed.
94     // According to the BiDi spec, we should emit beforeRequestSent when adding
95     // request headers, see https://whatpr.org/fetch/1540.html#http-network-or-cache-fetch
96     // step 8.17
97     // Bug 1802181: switch the NetworkObserver to an event-based API.
98     this.#emitBeforeRequestSent();
100     // If the request is already blocked, we will not receive further updates,
101     // emit a network.fetchError event immediately.
102     if (networkEvent.blockedReason) {
103       this.#emitFetchError();
104     }
105   }
107   /**
108    * Add network request POST data.
109    *
110    * Required API for a NetworkObserver event owner.
111    *
112    * @param {object} postData
113    *     The request POST data.
114    */
115   addRequestPostData(postData) {
116     // Only the postData size is needed for RemoteAgent consumers.
117     this.#requestData.bodySize = postData.size;
118   }
120   /**
121    * Add the initial network response information.
122    *
123    * Required API for a NetworkObserver event owner.
124    *
125    *
126    * @param {object} options
127    * @param {nsIChannel} options.channel
128    *     The channel.
129    * @param {boolean} options.fromCache
130    * @param {string} options.rawHeaders
131    */
132   addResponseStart(options) {
133     const { channel, fromCache, rawHeaders = "" } = options;
134     this.#responseChannel = channel;
136     const { headers } =
137       lazy.NetworkUtils.fetchResponseHeadersAndCookies(channel);
139     const headersSize = rawHeaders.length;
140     this.#responseData = {
141       ...this.#responseData,
142       bodySize: 0,
143       bytesReceived: headersSize,
144       fromCache: this.#fromCache || !!fromCache,
145       headers,
146       headersSize,
147       mimeType: this.#getMimeType(),
148       protocol: lazy.NetworkUtils.getProtocol(channel),
149       status: channel.responseStatus,
150       statusText: channel.responseStatusText,
151     };
153     // This should be triggered when all headers have been received, matching
154     // the WebDriverBiDi response started trigger in `4.6. HTTP-network fetch`
155     // from the fetch specification, based on the PR visible at
156     // https://github.com/whatwg/fetch/pull/1540
157     this.#emitResponseStarted();
158   }
160   /**
161    * Add connection security information.
162    *
163    * Required API for a NetworkObserver event owner.
164    *
165    * Not used for RemoteAgent.
166    */
167   addSecurityInfo() {}
169   /**
170    * Add network event timings.
171    *
172    * Required API for a NetworkObserver event owner.
173    *
174    * Not used for RemoteAgent.
175    */
176   addEventTimings() {}
178   /**
179    * Add response cache entry.
180    *
181    * Required API for a NetworkObserver event owner.
182    *
183    * Not used for RemoteAgent.
184    */
185   addResponseCache() {}
187   /**
188    * Add response content.
189    *
190    * Required API for a NetworkObserver event owner.
191    *
192    * @param {object} response
193    *     An object which represents the response content.
194    * @param {object} responseInfo
195    *     Additional meta data about the response.
196    */
197   addResponseContent(response, responseInfo) {
198     // Update content-related sizes with the latest data from addResponseContent.
199     this.#responseData = {
200       ...this.#responseData,
201       bodySize: response.bodySize,
202       bytesReceived: response.transferredSize,
203       content: {
204         size: response.decodedBodySize,
205       },
206     };
208     if (responseInfo.blockedReason) {
209       this.#emitFetchError();
210     } else {
211       this.#emitResponseCompleted();
212     }
213   }
215   /**
216    * Add server timings.
217    *
218    * Required API for a NetworkObserver event owner.
219    *
220    * Not used for RemoteAgent.
221    */
222   addServerTimings() {}
224   /**
225    * Add service worker timings.
226    *
227    * Required API for a NetworkObserver event owner.
228    *
229    * Not used for RemoteAgent.
230    */
231   addServiceWorkerTimings() {}
233   onAuthPrompt(authDetails, authCallbacks) {
234     this.#emitAuthRequired(authCallbacks);
235   }
237   /**
238    * Convert the provided request timing to a timing relative to the beginning
239    * of the request. All timings are numbers representing high definition
240    * timestamps.
241    *
242    * @param {number} timing
243    *     High definition timestamp for a request timing relative from the time
244    *     origin.
245    * @param {number} requestTime
246    *     High definition timestamp for the request start time relative from the
247    *     time origin.
248    * @returns {number}
249    *     High definition timestamp for the request timing relative to the start
250    *     time of the request, or 0 if the provided timing was 0.
251    */
252   #convertTimestamp(timing, requestTime) {
253     if (timing == 0) {
254       return 0;
255     }
257     return timing - requestTime;
258   }
260   #emitAuthRequired(authCallbacks) {
261     this.#updateDataFromTimedChannel();
263     this.#networkListener.emit("auth-required", {
264       authCallbacks,
265       contextId: this.#contextId,
266       isNavigationRequest: this.#isMainDocumentChannel,
267       redirectCount: this.#redirectCount,
268       requestChannel: this.#requestChannel,
269       requestData: this.#requestData,
270       responseChannel: this.#responseChannel,
271       responseData: this.#responseData,
272       timestamp: Date.now(),
273     });
274   }
276   #emitBeforeRequestSent() {
277     this.#updateDataFromTimedChannel();
279     this.#networkListener.emit("before-request-sent", {
280       contextId: this.#contextId,
281       isNavigationRequest: this.#isMainDocumentChannel,
282       redirectCount: this.#redirectCount,
283       requestChannel: this.#requestChannel,
284       requestData: this.#requestData,
285       timestamp: Date.now(),
286     });
287   }
289   #emitFetchError() {
290     this.#updateDataFromTimedChannel();
292     this.#networkListener.emit("fetch-error", {
293       contextId: this.#contextId,
294       // TODO: Update with a proper error text. Bug 1873037.
295       errorText: ChromeUtils.getXPCOMErrorName(this.#requestChannel.status),
296       isNavigationRequest: this.#isMainDocumentChannel,
297       redirectCount: this.#redirectCount,
298       requestChannel: this.#requestChannel,
299       requestData: this.#requestData,
300       timestamp: Date.now(),
301     });
302   }
304   #emitResponseCompleted() {
305     this.#updateDataFromTimedChannel();
307     this.#networkListener.emit("response-completed", {
308       contextId: this.#contextId,
309       isNavigationRequest: this.#isMainDocumentChannel,
310       redirectCount: this.#redirectCount,
311       requestChannel: this.#requestChannel,
312       requestData: this.#requestData,
313       responseChannel: this.#responseChannel,
314       responseData: this.#responseData,
315       timestamp: Date.now(),
316     });
317   }
319   #emitResponseStarted() {
320     this.#updateDataFromTimedChannel();
322     this.#networkListener.emit("response-started", {
323       contextId: this.#contextId,
324       isNavigationRequest: this.#isMainDocumentChannel,
325       redirectCount: this.#redirectCount,
326       requestChannel: this.#requestChannel,
327       requestData: this.#requestData,
328       responseChannel: this.#responseChannel,
329       responseData: this.#responseData,
330       timestamp: Date.now(),
331     });
332   }
334   #getBrowsingContext() {
335     const id = lazy.NetworkUtils.getChannelBrowsingContextID(
336       this.#requestChannel
337     );
338     return BrowsingContext.get(id);
339   }
341   /**
342    * Retrieve the navigable id for the current browsing context associated to
343    * the requests' channel. Network events are recorded in the parent process
344    * so we always expect to be able to use TabManager.getIdForBrowsingContext.
345    *
346    * @returns {string}
347    *     The navigable id corresponding to the given browsing context.
348    */
349   #getContextId() {
350     return lazy.TabManager.getIdForBrowsingContext(this.#getBrowsingContext());
351   }
353   #getMimeType() {
354     // TODO: DevTools NetworkObserver is computing a similar value in
355     // addResponseContent, but uses an inconsistent implementation in
356     // addResponseStart. This approach can only be used as early as in
357     // addResponseHeaders. We should move this logic to the NetworkObserver and
358     // expose mimeType in addResponseStart. Bug 1809670.
359     let mimeType = "";
361     try {
362       mimeType = this.#wrappedChannel.contentType;
363       const contentCharset = this.#requestChannel.contentCharset;
364       if (contentCharset) {
365         mimeType += `;charset=${contentCharset}`;
366       }
367     } catch (e) {
368       // Ignore exceptions when reading contentType/contentCharset
369     }
371     return mimeType;
372   }
374   #getTimingsFromTimedChannel(timedChannel) {
375     const {
376       channelCreationTime,
377       redirectStartTime,
378       redirectEndTime,
379       dispatchFetchEventStartTime,
380       cacheReadStartTime,
381       domainLookupStartTime,
382       domainLookupEndTime,
383       connectStartTime,
384       connectEndTime,
385       secureConnectionStartTime,
386       requestStartTime,
387       responseStartTime,
388       responseEndTime,
389     } = timedChannel;
391     // fetchStart should be the post-redirect start time, which should be the
392     // first non-zero timing from: dispatchFetchEventStart, cacheReadStart and
393     // domainLookupStart. See https://www.w3.org/TR/navigation-timing-2/#processing-model
394     const fetchStartTime =
395       dispatchFetchEventStartTime ||
396       cacheReadStartTime ||
397       domainLookupStartTime;
399     // Bug 1805478: Per spec, the origin time should match Performance API's
400     // timeOrigin for the global which initiated the request. This is not
401     // available in the parent process, so for now we will use 0.
402     const timeOrigin = 0;
404     return {
405       timeOrigin,
406       requestTime: this.#convertTimestamp(channelCreationTime, timeOrigin),
407       redirectStart: this.#convertTimestamp(redirectStartTime, timeOrigin),
408       redirectEnd: this.#convertTimestamp(redirectEndTime, timeOrigin),
409       fetchStart: this.#convertTimestamp(fetchStartTime, timeOrigin),
410       dnsStart: this.#convertTimestamp(domainLookupStartTime, timeOrigin),
411       dnsEnd: this.#convertTimestamp(domainLookupEndTime, timeOrigin),
412       connectStart: this.#convertTimestamp(connectStartTime, timeOrigin),
413       connectEnd: this.#convertTimestamp(connectEndTime, timeOrigin),
414       tlsStart: this.#convertTimestamp(secureConnectionStartTime, timeOrigin),
415       tlsEnd: this.#convertTimestamp(connectEndTime, timeOrigin),
416       requestStart: this.#convertTimestamp(requestStartTime, timeOrigin),
417       responseStart: this.#convertTimestamp(responseStartTime, timeOrigin),
418       responseEnd: this.#convertTimestamp(responseEndTime, timeOrigin),
419     };
420   }
422   /**
423    * Update the timings and the redirect count from the nsITimedChannel
424    * corresponding to the current channel. This should be called before emitting
425    * any event from this class.
426    */
427   #updateDataFromTimedChannel() {
428     const timedChannel = this.#requestChannel.QueryInterface(
429       Ci.nsITimedChannel
430     );
431     this.#redirectCount = timedChannel.redirectCount;
432     this.#requestData.timings = this.#getTimingsFromTimedChannel(timedChannel);
433   }