Bug 1637363 - [remote] Use frameId of the related browsing context for Network events. r=remote-protocol-reviewers,maja_zf
authorHenrik Skupin <mail@hskupin.info>
Fri, 29 May 2020 12:36:26 +0000
changeset 532956 bf7719c227fb46d7bd76d1bd3dc09c1600b0578c
parent 532955 cb93af886469eb4e622a0be1ab73434de0b958d8
child 532957 1b82d9e32bf555f889b0c727859544f282cf8f18
push id37461
push userccoroiu@mozilla.com
push dateFri, 29 May 2020 21:46:31 +0000
treeherdermozilla-central@a58cc68b0c51 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersremote-protocol-reviewers, maja_zf
bugs1637363
milestone78.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1637363 - [remote] Use frameId of the related browsing context for Network events. r=remote-protocol-reviewers,maja_zf Differential Revision: https://phabricator.services.mozilla.com/D76835
remote/domains/content/Page.jsm
remote/domains/parent/Network.jsm
remote/domains/parent/Page.jsm
remote/observers/NetworkObserver.jsm
remote/test/browser/head.js
remote/test/browser/network/browser.ini
remote/test/browser/network/browser_navigationEvents.js
remote/test/browser/network/browser_requestWillBeSent.js
remote/test/browser/network/browser_responseReceived.js
remote/test/browser/network/doc_frameset.html
remote/test/browser/network/doc_networkEvents.html
remote/test/browser/network/file_framesetEvents.js
remote/test/browser/network/head.js
remote/test/browser/page/browser_createIsolatedWorld.js
remote/test/browser/runtime/browser_executionContextEvents.js
--- a/remote/domains/content/Page.jsm
+++ b/remote/domains/content/Page.jsm
@@ -32,16 +32,23 @@ class Page extends ContentProcessDomain 
     super(session);
 
     this.enabled = false;
     this.lifecycleEnabled = false;
     // script id => { source, worldName }
     this.scriptsToEvaluateOnLoad = new Map();
     this.worldsToEvaluateOnLoad = new Set();
 
+    // This map is used to keep a reference to the loader id for
+    // those Page events, which do not directly rely on
+    // Network events. This might be a temporary solution until
+    // the Network observer could be queried for that. But right
+    // now this lives in the parent process.
+    this.frameIdToLoaderId = new Map();
+
     this._onFrameAttached = this._onFrameAttached.bind(this);
     this._onFrameDetached = this._onFrameDetached.bind(this);
     this._onFrameNavigated = this._onFrameNavigated.bind(this);
     this._onScriptLoaded = this._onScriptLoaded.bind(this);
 
     this.session.contextObserver.on("script-loaded", this._onScriptLoaded);
   }
 
@@ -304,23 +311,20 @@ class Page extends ContentProcessDomain 
   }
 
   handleEvent({ type, target }) {
     if (target.defaultView != this.content) {
       // Ignore iframes for now
       return;
     }
 
+    const frameId = target.defaultView.docShell.browsingContext.id.toString();
+    const loaderId = this.frameIdToLoaderId.get(frameId);
     const timestamp = Date.now() / 1000;
-    const frameId = target.defaultView.docShell.browsingContext.id.toString();
     const url = target.location.href;
-    const loaderId =
-      this._lastRequest?.frameId == frameId
-        ? this._lastRequest?.loaderId
-        : null;
 
     switch (type) {
       case "DOMContentLoaded":
         this.emit("Page.domContentEventFired", { timestamp });
         this.emitLifecycleEvent(
           frameId,
           loaderId,
           "DOMContentLoaded",
@@ -346,17 +350,19 @@ class Page extends ContentProcessDomain 
       case "readystatechange":
         if (this.content.document.readState === "loading") {
           this.emitLifecycleEvent(frameId, loaderId, "init", timestamp);
         }
     }
   }
 
   _updateLoaderId(data) {
-    this._lastRequest = data;
+    const { frameId, loaderId } = data;
+
+    this.frameIdToLoaderId.set(frameId, loaderId);
   }
 
   _contentRect() {
     const docEl = this.content.document.documentElement;
 
     return {
       x: 0,
       y: 0,
--- a/remote/domains/parent/Network.jsm
+++ b/remote/domains/parent/Network.jsm
@@ -373,17 +373,17 @@ class Network extends Domain {
    */
   setUserAgentOverride(options = {}) {
     const { id } = this.session;
     this.session.execute(id, "Emulation", "setUserAgentOverride", options);
   }
 
   _onRequest(eventName, httpChannel, data) {
     const wrappedChannel = ChannelWrapper.get(httpChannel);
-    const topFrame = getLoadContext(httpChannel).topFrameElement;
+
     const request = {
       url: httpChannel.URI.spec,
       urlFragment: undefined,
       method: httpChannel.requestMethod,
       headers: headersAsObject(data.headers),
       postData: undefined,
       hasPostData: false,
       mixedContentType: undefined,
@@ -396,26 +396,25 @@ class Network extends Domain {
       loaderId: data.loaderId,
       documentURL: wrappedChannel.documentURL || httpChannel.URI.spec,
       request,
       timestamp: Date.now() / 1000,
       wallTime: undefined,
       initiator: undefined,
       redirectResponse: undefined,
       type: LOAD_CAUSE_STRINGS[data.cause] || "unknown",
-      // Bug 1637363 - Add subframe support
-      frameId: topFrame.browsingContext?.id.toString(),
+      frameId: data.frameId,
       hasUserGesture: undefined,
     });
   }
 
   _onResponse(eventName, httpChannel, data) {
     const wrappedChannel = ChannelWrapper.get(httpChannel);
-    const topFrame = getLoadContext(httpChannel).topFrameElement;
     const headers = headersAsObject(data.headers);
+
     this.emit("Network.responseReceived", {
       requestId: data.requestId,
       loaderId: data.loaderId,
       timestamp: Date.now() / 1000,
       type: LOAD_CAUSE_STRINGS[data.cause] || "unknown",
       response: {
         url: httpChannel.URI.spec,
         status: data.status,
@@ -429,41 +428,21 @@ class Network extends Domain {
         remotePort: data.remotePort,
         fromDiskCache: data.fromCache,
         encodedDataLength: undefined,
         protocol: httpChannel.protocolVersion,
         securityDetails: data.securityDetails,
         // unknown, neutral, insecure, secure, info, insecure-broken
         securityState: "unknown",
       },
-      // Bug 1637363 - Add subframe support
-      frameId: topFrame.browsingContext?.id.toString(),
+      frameId: data.frameId,
     });
   }
 }
 
-function getLoadContext(httpChannel) {
-  let loadContext = null;
-  try {
-    if (httpChannel.notificationCallbacks) {
-      loadContext = httpChannel.notificationCallbacks.getInterface(
-        Ci.nsILoadContext
-      );
-    }
-  } catch (e) {}
-  try {
-    if (!loadContext && httpChannel.loadGroup) {
-      loadContext = httpChannel.loadGroup.notificationCallbacks.getInterface(
-        Ci.nsILoadContext
-      );
-    }
-  } catch (e) {}
-  return loadContext;
-}
-
 /**
  * Creates a CDP Network.Cookie from our internal cookie values
  *
  * @param {nsICookie} cookie
  *
  * @returns {Network.Cookie}
  *      A CDP Cookie
  */
--- a/remote/domains/parent/Page.jsm
+++ b/remote/domains/parent/Page.jsm
@@ -742,17 +742,17 @@ class Page extends Domain {
    * content process
    */
   _onRequest(_type, _ch, data) {
     if (!data.loaderId) {
       return;
     }
     this.executeInChild("_updateLoaderId", {
       loaderId: data.loaderId,
-      frameId: this.session.browsingContext.id.toString(),
+      frameId: data.frameId,
     });
   }
 }
 
 function transitionToLoadFlag(transitionType) {
   switch (transitionType) {
     case "reload":
       return Ci.nsIWebNavigation.LOAD_FLAGS_IS_REFRESH;
--- a/remote/observers/NetworkObserver.jsm
+++ b/remote/observers/NetworkObserver.jsm
@@ -49,16 +49,17 @@ const MAX_RESPONSE_STORAGE_SIZE = 100 * 
 class NetworkObserver {
   constructor() {
     EventEmitter.decorate(this);
     this._browserSessionCount = new Map();
     gActivityDistributor.addObserver(this);
     ChannelEventSinkFactory.getService().registerCollector(this);
 
     this._redirectMap = new Map();
+    this._windowIdToFrameIdMap = new Map();
 
     // Request interception state.
     this._browserSuspendedChannels = new Map();
     this._extraHTTPHeaders = new Map();
     this._browserResponseStorages = new Map();
 
     this._onRequest = this._onRequest.bind(this);
     this._onExamineResponse = this._onResponse.bind(
@@ -186,69 +187,69 @@ class NetworkObserver {
   }
 
   _onRequest(channel, topic) {
     try {
       channel.QueryInterface(Ci.nsIHttpChannel);
     } catch (ex) {
       return;
     }
+
     const httpChannel = channel.QueryInterface(Ci.nsIHttpChannel);
     const loadContext = getLoadContext(httpChannel);
-    if (
-      !loadContext ||
-      !this._browserSessionCount.has(loadContext.topFrameElement) ||
-      !loadContext.topFrameElement.currentURI
-    ) {
+    const browser = loadContext?.topFrameElement;
+    if (!loadContext || !this._browserSessionCount.has(browser)) {
       return;
     }
-    const extraHeaders = this._extraHTTPHeaders.get(
-      loadContext.topFrameElement
-    );
+
+    const extraHeaders = this._extraHTTPHeaders.get(browser);
     if (extraHeaders) {
       for (const header of extraHeaders) {
         httpChannel.setRequestHeader(
           header.name,
           header.value,
           false /* merge */
         );
       }
     }
     const causeType = httpChannel.loadInfo
       ? httpChannel.loadInfo.externalContentPolicyType
       : Ci.nsIContentPolicy.TYPE_OTHER;
-    const suspendedChannels = this._browserSuspendedChannels.get(
-      loadContext.topFrameElement
-    );
+
+    const suspendedChannels = this._browserSuspendedChannels.get(browser);
     if (suspendedChannels) {
       httpChannel.suspend();
       suspendedChannels.set(requestId(httpChannel), httpChannel);
     }
+
     const oldChannel = this._redirectMap.get(httpChannel);
     this._redirectMap.delete(httpChannel);
 
     // Install response body hooks.
-    new ResponseBodyListener(this, loadContext.topFrameElement, httpChannel);
+    new ResponseBodyListener(this, browser, httpChannel);
 
     this.emit("request", httpChannel, {
       url: httpChannel.URI.spec,
       suspended: suspendedChannels ? true : undefined,
       requestId: requestId(httpChannel),
       redirectedFrom: oldChannel ? requestId(oldChannel) : undefined,
       postData: readRequestPostData(httpChannel),
       headers: requestHeaders(httpChannel),
       method: httpChannel.requestMethod,
       isNavigationRequest: httpChannel.isMainDocumentChannel,
       cause: causeType,
       causeString: causeTypeToString(causeType),
+      frameId: this.frameId(httpChannel, causeType),
       // clients expect loaderId == requestId for document navigation
-      loaderId:
-        causeType == Ci.nsIContentPolicy.TYPE_DOCUMENT
-          ? requestId(httpChannel)
-          : undefined,
+      loaderId: [
+        Ci.nsIContentPolicy.TYPE_DOCUMENT,
+        Ci.nsIContentPolicy.TYPE_SUBDOCUMENT,
+      ].includes(causeType)
+        ? requestId(httpChannel)
+        : undefined,
     });
   }
 
   _onResponse(fromCache, httpChannel, topic) {
     const loadContext = getLoadContext(httpChannel);
     if (
       !loadContext ||
       !this._browserSessionCount.has(loadContext.topFrameElement)
@@ -274,21 +275,24 @@ class NetworkObserver {
       headers: responseHeaders(httpChannel),
       requestHeaders: requestHeaders(httpChannel),
       remoteIPAddress,
       remotePort,
       status: httpChannel.responseStatus,
       statusText: httpChannel.responseStatusText,
       cause: causeType,
       causeString: causeTypeToString(causeType),
+      frameId: this.frameId(httpChannel, causeType),
       // clients expect loaderId == requestId for document navigation
-      loaderId:
-        causeType == Ci.nsIContentPolicy.TYPE_DOCUMENT
-          ? requestId(httpChannel)
-          : undefined,
+      loaderId: [
+        Ci.nsIContentPolicy.TYPE_DOCUMENT,
+        Ci.nsIContentPolicy.TYPE_SUBDOCUMENT,
+      ].includes(causeType)
+        ? requestId(httpChannel)
+        : undefined,
     });
   }
 
   _onResponseFinished(browser, httpChannel, body) {
     const responseStorage = this._browserResponseStorages.get(browser);
     if (!responseStorage) {
       return;
     }
@@ -336,16 +340,46 @@ class NetworkObserver {
     if (value) {
       this._browserSessionCount.set(browser, value - 1);
     } else {
       this._browserSessionCount.delete(browser);
       this._browserResponseStorages.delete(browser);
       this.dispose();
     }
   }
+
+  /**
+   * Returns the frameId of the current httpChannel.
+   *
+   * Only Document and Subdocument requests contain a reference
+   * to the browsing context, and as such the id, which is used
+   * as frameId. To be able to send a frameId for resource requests
+   * a map has to be kept up-to-date to retrieve that id from the
+   * window id. Hereby window ids are unique for all requests
+   * belonging to the same frame, but differ between frames.
+   */
+  frameId(httpChannel, causeType) {
+    const loadContext = getLoadContext(httpChannel);
+    const wrappedChannel = ChannelWrapper.get(httpChannel);
+
+    // Document requests indicate a fresh navigation cycle.
+    // As such cleanup all the old window id references.
+    if (causeType == Ci.nsIContentPolicy.TYPE_DOCUMENT) {
+      this._windowIdToFrameIdMap.clear();
+    }
+
+    let frameId = loadContext.id?.toString();
+    if (frameId) {
+      this._windowIdToFrameIdMap.set(wrappedChannel.windowId, frameId);
+    } else {
+      frameId = this._windowIdToFrameIdMap.get(wrappedChannel.windowId);
+    }
+
+    return frameId;
+  }
 }
 
 const protocolVersionNames = {
   [Ci.nsITransportSecurityInfo.TLS_VERSION_1]: "TLS 1",
   [Ci.nsITransportSecurityInfo.TLS_VERSION_1_1]: "TLS 1.1",
   [Ci.nsITransportSecurityInfo.TLS_VERSION_1_2]: "TLS 1.2",
   [Ci.nsITransportSecurityInfo.TLS_VERSION_1_3]: "TLS 1.3",
 };
--- a/remote/test/browser/head.js
+++ b/remote/test/browser/head.js
@@ -546,53 +546,63 @@ class RecordEvents {
   }
 
   /**
    * Record events until we hit the timeout or the expected total is exceeded.
    *
    * @param {number=} timeout
    *     Timeout in milliseconds. Defaults to 1000.
    *
-   * @return {Array<{ eventName, payload }>} Recorded events
+   * @return {Array<{ eventName, payload, index }>} Recorded events
    */
   async record(timeout = TIMEOUT_EVENTS) {
     await Promise.race([Promise.all(this.promises), timeoutPromise(timeout)]);
     for (const unsubscribe of this.subscriptions) {
       unsubscribe();
     }
     return this.events;
   }
 
   /**
+   * Filter events based on predicate
+   *
+   * @param {Function} predicate
+   *
+   * @return {Array<{ eventName, payload, index }>}
+   *     The list of events matching the filter.
+   */
+  filter(predicate) {
+    return this.events.filter(predicate);
+  }
+
+  /**
    * Find first occurrence of the given event.
    *
    * @param {string} eventName
    *
-   * @return {object} The event payload, if any.
+   * @return {{ eventName, payload, index }} The event, if any.
    */
   findEvent(eventName) {
     const event = this.events.find(el => el.eventName == eventName);
     if (event) {
-      return event.payload;
+      return event;
     }
     return {};
   }
 
   /**
    * Find given events.
    *
    * @param {string} eventName
    *
-   * @return {Array<object>}
-   *     The events payload, if any.
+   * @return {Array<{ eventName, payload, index }>}
+   *     The events, if any.
    */
   findEvents(eventName) {
-    return this.events
-      .filter(event => event.eventName == eventName)
-      .map(event => event.payload);
+    return this.events.filter(event => event.eventName == eventName);
   }
 
   /**
    * Find index of first occurrence of the given event.
    *
    * @param {string} eventName
    *
    * @return {number} The event index, -1 if not found.
--- a/remote/test/browser/network/browser.ini
+++ b/remote/test/browser/network/browser.ini
@@ -4,18 +4,20 @@ subsuite = remote
 prefs =
   remote.enabled=true
   remote.frames.enabled=true
 support-files =
   !/remote/test/browser/chrome-remote-interface.js
   !/remote/test/browser/head.js
   head.js
   doc_empty.html
+  doc_frameset.html
   doc_networkEvents.html
   file_networkEvents.js
+  file_framesetEvents.js
   sjs-cookies.sjs
 
 [browser_deleteCookies.js]
 [browser_emulateNetworkConditions.js]
 [browser_getAllCookies.js]
 [browser_getCookies.js]
 skip-if = (os == 'win' && os_version == '10.0' && ccov) # Bug 1605650
 [browser_navigationEvents.js]
--- a/remote/test/browser/network/browser_navigationEvents.js
+++ b/remote/test/browser/network/browser_navigationEvents.js
@@ -1,174 +1,202 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test order and consistency of Network/Page events as a whole.
 // Details of specific events are checked in event-specific test files.
 
-const PAGE_URL =
-  "http://example.com/browser/remote/test/browser/network/doc_networkEvents.html";
-const JS_URL =
-  "http://example.com/browser/remote/test/browser/network/file_networkEvents.js";
-
-add_task(async function documentNavigationWithScriptResource({ client }) {
-  const { Page, Network } = client;
-  await Network.enable();
-  await Page.enable();
-  await Page.setLifecycleEventsEnabled({ enabled: true });
-  const { history, urlToEvents } = configureHistory(client);
-  const navigateDone = history.addPromise("Page.navigate");
-
-  const { frameId } = await Page.navigate({ url: PAGE_URL }).then(navigateDone);
-  ok(frameId, "Page.navigate returned a frameId");
+const BASE_PATH = "http://example.com/browser/remote/test/browser/network";
+const FRAMESET_URL = `${BASE_PATH}/doc_frameset.html`;
+const FRAMESET_JS_URL = `${BASE_PATH}/file_framesetEvents.js`;
+const PAGE_URL = `${BASE_PATH}/doc_networkEvents.html`;
+const PAGE_JS_URL = `${BASE_PATH}/file_networkEvents.js`;
 
-  info("Wait for events");
-  const events = await history.record();
-  is(events.length, 8, "Expected number of events");
-  const eventNames = events.map(
-    item => `${item.eventName}(${item.payload.type || item.payload.name})`
-  );
-  info(`Received events: ${eventNames}`);
-  const documentEvents = urlToEvents.get(PAGE_URL);
-  const resourceEvents = urlToEvents.get(JS_URL);
-  is(
-    2,
-    documentEvents.length,
-    "Expected number of Network events for document"
-  );
-  is(
-    2,
-    resourceEvents.length,
-    "Expected number of Network events for resource"
+add_task(async function eventsForTopFrameNavigation({ client }) {
+  const { history, frameId: frameIdNav } = await prepareTest(
+    client,
+    FRAMESET_URL,
+    10
   );
 
-  const docRequest = documentEvents[0].event;
-  is(docRequest.request.url, PAGE_URL, "Got the doc request");
-  is(docRequest.documentURL, PAGE_URL, "documenURL matches request url");
-  const lifeCycleEvents = history.findEvents("Page.lifecycleEvent");
-  const firstLifecycle = history.indexOf("Page.lifecycleEvent");
-  ok(
-    firstLifecycle > documentEvents[1].index,
-    "First lifecycle event is after document response"
-  );
-  for (const e of lifeCycleEvents) {
-    is(
-      e.loaderId,
-      docRequest.loaderId,
-      `${e.name} lifecycle event has same loaderId as document request`
-    );
-  }
+  const documentEvents = filterEventsByType(history, "Document");
+  const scriptEvents = filterEventsByType(history, "Script");
+  const subdocumentEvents = filterEventsByType(history, "Subdocument");
+
+  is(documentEvents.length, 2, "Expected number of Document events");
+  is(subdocumentEvents.length, 2, "Expected number of Subdocument events");
+  is(scriptEvents.length, 4, "Expected number of Script events");
+
+  const navigatedEvents = history.findEvents("Page.navigate");
+  is(navigatedEvents.length, 1, "Expected number of navigate done events");
+
+  const frameAttachedEvents = history.findEvents("Page.frameAttached");
+  is(frameAttachedEvents.length, 1, "Expected number of frame attached events");
 
-  const resourceRequest = resourceEvents[0].event;
-  is(resourceRequest.documentURL, PAGE_URL, "documentURL is trigger document");
-  is(resourceRequest.request.url, JS_URL, "Got the JS request");
-  ok(
-    documentEvents[0].index < resourceEvents[0].index,
-    "Document request received before resource request"
-  );
-  const navigateStep = history.indexOf("Page.navigate");
-  ok(
-    navigateStep > documentEvents[1].index,
-    "Page.navigate returns after document response"
-  );
-  ok(
-    navigateStep < resourceEvents[0].index,
-    "Page.navigate returns before resource request"
-  );
-  ok(
-    navigateStep < firstLifecycle,
-    "Page.navigate returns before first lifecycle event"
-  );
+  // network events for document and script
+  assertEventOrder(documentEvents[0], documentEvents[1]);
+  assertEventOrder(documentEvents[1], navigatedEvents[0], {
+    ignoreTimestamps: true,
+  });
+  assertEventOrder(navigatedEvents[0], scriptEvents[0], {
+    ignoreTimestamps: true,
+  });
+  assertEventOrder(scriptEvents[0], scriptEvents[1]);
 
-  const docResponse = documentEvents[1].event;
-  is(docResponse.response.url, PAGE_URL, "Got the doc response");
-  is(
-    docRequest.frameId,
-    docResponse.frameId,
-    "Doc response frame id matches that of doc request"
-  );
-  ok(!!docResponse.response.headers.server, "Doc response has headers");
+  const docRequest = documentEvents[0].payload;
+  is(docRequest.documentURL, FRAMESET_URL, "documenURL matches target url");
+  is(docRequest.frameId, frameIdNav, "Got the expected frame id");
+  is(docRequest.request.url, FRAMESET_URL, "Got the Document request");
+
+  const docResponse = documentEvents[1].payload;
+  is(docResponse.frameId, frameIdNav, "Got the expected frame id");
+  is(docResponse.response.url, FRAMESET_URL, "Got the Document response");
+  ok(!!docResponse.response.headers.server, "Document response has headers");
   // TODO? response reports extra request header "upgrade-insecure-requests":"1"
   // Assert.deepEqual(
   //   docResponse.response.requestHeaders,
   //   docRequest.request.headers,
   //   "Response event reports same request headers as request event"
   // );
 
-  ok(
-    docRequest.timestamp <= docResponse.timestamp,
-    "Document request happens before document response"
+  const scriptRequest = scriptEvents[0].payload;
+  is(
+    scriptRequest.documentURL,
+    FRAMESET_URL,
+    "documentURL is trigger document"
   );
-  const resourceResponse = resourceEvents[1].event;
-  is(resourceResponse.response.url, JS_URL, "Got the resource response");
+  is(scriptRequest.frameId, frameIdNav, "Got the expected frame id");
+  is(scriptRequest.request.url, FRAMESET_JS_URL, "Got the Script request");
+
+  const scriptResponse = scriptEvents[1].payload;
+  is(scriptResponse.frameId, frameIdNav, "Got the expected frame id");
   todo(
-    resourceResponse.loaderId === docRequest.loaderId,
+    scriptResponse.loaderId === docRequest.loaderId,
     "The same loaderId is used for dependent responses (Bug 1637838)"
   );
-  ok(!!resourceResponse.frameId, "Resource response has a frame id");
-  is(
-    docRequest.frameId,
-    resourceResponse.frameId,
-    "Resource response frame id matches that of doc request"
-  );
+  is(scriptResponse.response.url, FRAMESET_JS_URL, "Got the Script response");
   Assert.deepEqual(
-    resourceResponse.response.requestHeaders,
-    resourceRequest.request.headers,
+    scriptResponse.response.requestHeaders,
+    scriptRequest.request.headers,
     "Response event reports same request headers as request event"
   );
-  ok(
-    resourceRequest.timestamp <= resourceResponse.timestamp,
-    "Document request happens before document response"
+
+  // frame is attached after all resources of the document have been loaded
+  // and before sub document starts loading
+  assertEventOrder(scriptEvents[1], frameAttachedEvents[0], {
+    ignoreTimestamps: true,
+  });
+  assertEventOrder(frameAttachedEvents[0], subdocumentEvents[0], {
+    ignoreTimestamps: true,
+  });
+
+  const {
+    frameId: frameIdSubFrame,
+    parentFrameId,
+  } = frameAttachedEvents[0].payload;
+  is(parentFrameId, frameIdNav, "Got expected parent frame id");
+
+  // network events for subdocument and script
+  assertEventOrder(subdocumentEvents[0], subdocumentEvents[1]);
+  assertEventOrder(subdocumentEvents[1], scriptEvents[2]);
+  assertEventOrder(scriptEvents[2], scriptEvents[3]);
+
+  const subdocRequest = subdocumentEvents[0].payload;
+  is(
+    subdocRequest.documentURL,
+    FRAMESET_URL,
+    "documentURL is trigger document"
   );
+  is(subdocRequest.frameId, frameIdSubFrame, "Got the expected frame id");
+  is(subdocRequest.request.url, PAGE_URL, "Got the Subdocument request");
+
+  const subdocResponse = subdocumentEvents[1].payload;
+  is(subdocResponse.frameId, frameIdSubFrame, "Got the expected frame id");
+  is(subdocResponse.response.url, PAGE_URL, "Got the Subdocument response");
+
+  const subscriptRequest = scriptEvents[2].payload;
+  is(subscriptRequest.documentURL, PAGE_URL, "documentURL is trigger document");
+  is(subscriptRequest.frameId, frameIdSubFrame, "Got the expected frame id");
+  is(subscriptRequest.request.url, PAGE_JS_URL, "Got the Script request");
+
+  const subscriptResponse = scriptEvents[3].payload;
+  is(subscriptResponse.frameId, frameIdSubFrame, "Got the expected frame id");
+  is(subscriptResponse.response.url, PAGE_JS_URL, "Got the Script response");
+  todo(
+    subscriptResponse.loaderId === subdocRequest.loaderId,
+    "The same loaderId is used for dependent responses (Bug 1637838)"
+  );
+  Assert.deepEqual(
+    subscriptResponse.response.requestHeaders,
+    subscriptRequest.request.headers,
+    "Response event reports same request headers as request event"
+  );
+
+  const lifeCycleEvents = history
+    .findEvents("Page.lifecycleEvent")
+    .map(event => event.payload);
+  for (const { name, loaderId } of lifeCycleEvents) {
+    is(
+      loaderId,
+      docRequest.loaderId,
+      `${name} lifecycle event has same loaderId as Document request`
+    );
+  }
 });
 
-function configureHistory(client) {
+async function prepareTest(client, url, totalCount) {
   const REQUEST = "Network.requestWillBeSent";
   const RESPONSE = "Network.responseReceived";
-  const LIFECYCLE = "Page.lifecycleEvent";
+  const FRAMEATTACHED = "Page.frameAttached";
+  const LIFECYCLE = "Page.livecycleEvent";
 
   const { Network, Page } = client;
-  const history = new RecordEvents(8);
-  const urlToEvents = new Map();
-  function updateUrlToEvents(kind) {
-    return ({ payload, index, eventName }) => {
-      const url = payload[kind]?.url;
-      if (!url) {
-        return;
-      }
-      if (!urlToEvents.get(url)) {
-        urlToEvents.set(url, [{ index, event: payload, eventName }]);
-      } else {
-        urlToEvents.get(url).push({ index, event: payload, eventName });
-      }
-    };
-  }
+  const history = new RecordEvents(totalCount);
 
   history.addRecorder({
     event: Network.requestWillBeSent,
     eventName: REQUEST,
     messageFn: payload => {
       return `Received ${REQUEST} for ${payload.request?.url}`;
     },
-    callback: updateUrlToEvents("request"),
   });
 
   history.addRecorder({
     event: Network.responseReceived,
     eventName: RESPONSE,
     messageFn: payload => {
       return `Received ${RESPONSE} for ${payload.response?.url}`;
     },
-    callback: updateUrlToEvents("response"),
+  });
+
+  history.addRecorder({
+    event: Page.frameAttached,
+    eventName: FRAMEATTACHED,
+    messageFn: ({ frameId, parentFrameId: parentId }) => {
+      return `Received ${FRAMEATTACHED} frame=${frameId} parent=${parentId}`;
+    },
   });
 
   history.addRecorder({
     event: Page.lifecycleEvent,
     eventName: LIFECYCLE,
     messageFn: payload => {
       return `Received ${LIFECYCLE} ${payload.name}`;
     },
   });
 
-  return { history, urlToEvents };
+  await Network.enable();
+  await Page.enable();
+
+  const navigateDone = history.addPromise("Page.navigate");
+  const { frameId } = await Page.navigate({ url }).then(navigateDone);
+  ok(frameId, "Page.navigate returned a frameId");
+
+  info("Wait for events");
+  const events = await history.record();
+
+  info(`Received events: ${events.map(getDescriptionForEvent)}`);
+  is(events.length, totalCount, "Received expected number of events");
+
+  return { history, frameId };
 }
--- a/remote/test/browser/network/browser_requestWillBeSent.js
+++ b/remote/test/browser/network/browser_requestWillBeSent.js
@@ -1,17 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-const PAGE_URL =
-  "http://example.com/browser/remote/test/browser/network/doc_networkEvents.html";
-const JS_URL =
-  "http://example.com/browser/remote/test/browser/network/file_networkEvents.js";
+const BASE_PATH = "http://example.com/browser/remote/test/browser/network";
+const FRAMESET_URL = `${BASE_PATH}/doc_frameset.html`;
+const FRAMESET_JS_URL = `${BASE_PATH}/file_framesetEvents.js`;
+const PAGE_URL = `${BASE_PATH}/doc_networkEvents.html`;
+const PAGE_JS_URL = `${BASE_PATH}/file_networkEvents.js`;
 
 add_task(async function noEventsWhenNetworkDomainDisabled({ client }) {
   const history = configureHistory(client, 0);
   await loadURL(PAGE_URL);
 
   const events = await history.record();
   is(events.length, 0, "Expected no Network.responseReceived events");
 });
@@ -25,76 +26,129 @@ add_task(async function noEventsAfterNet
   await loadURL(PAGE_URL);
 
   const events = await history.record();
   is(events.length, 0, "Expected no Network.responseReceived events");
 });
 
 add_task(async function documentNavigationWithResource({ client }) {
   const { Page, Network } = client;
+
   await Network.enable();
-  const history = configureHistory(client, 2);
+  await Page.enable();
+
+  const history = configureHistory(client, 4);
 
-  const { frameId: frameIdNav } = await Page.navigate({ url: PAGE_URL });
+  const frameAttached = Page.frameAttached();
+  const { frameId: frameIdNav } = await Page.navigate({ url: FRAMESET_URL });
+  const { frameId: frameIdSubFrame } = await frameAttached;
   ok(frameIdNav, "Page.navigate returned a frameId");
 
   info("Wait for Network events");
   const events = await history.record();
-  is(events.length, 2, "Expected number of Network.requestWillBeSent events");
+  is(events.length, 4, "Expected number of Network.requestWillBeSent events");
 
+  // Check top-level document request
   const docRequest = events[0].payload;
-  is(docRequest.request.url, PAGE_URL, "Got the doc request");
-  is(docRequest.documentURL, PAGE_URL, "documenURL matches request url");
-  is(docRequest.type, "Document", "The doc request has 'Document' type");
-  is(docRequest.request.method, "GET", "The doc request has 'GET' method");
+  is(docRequest.type, "Document", "Document request has the expected type");
+  is(docRequest.documentURL, FRAMESET_URL, "documentURL matches requested url");
+  is(docRequest.frameId, frameIdNav, "Got the expected frame id");
+  is(docRequest.request.url, FRAMESET_URL, "Got the Document request");
+  is(docRequest.request.method, "GET", "Has the expected request method");
   is(
     docRequest.requestId,
     docRequest.loaderId,
-    "The doc request has requestId = loaderId"
+    "The request id is equal to the loader id"
   );
   is(
-    docRequest.frameId,
-    frameIdNav,
-    "Doc request returns same frameId as Page.navigate"
+    docRequest.request.headers.host,
+    "example.com",
+    "Document request has headers"
   );
-  is(docRequest.request.headers.host, "example.com", "Doc request has headers");
 
-  const resourceRequest = events[1].payload;
-  is(resourceRequest.documentURL, PAGE_URL, "documentURL is trigger document");
-  is(resourceRequest.request.url, JS_URL, "Got the JS request");
+  // Check top-level script request
+  const scriptRequest = events[1].payload;
+  is(scriptRequest.type, "Script", "Script request has the expected type");
   is(
-    resourceRequest.request.headers.host,
-    "example.com",
-    "Doc request has headers"
+    scriptRequest.documentURL,
+    FRAMESET_URL,
+    "documentURL is trigger document for the script request"
   );
-  is(resourceRequest.type, "Script", "The page request has 'Script' type");
-  is(resourceRequest.request.method, "GET", "The doc request has 'GET' method");
+  is(scriptRequest.frameId, frameIdNav, "Got the expected frame id");
+  is(scriptRequest.request.url, FRAMESET_JS_URL, "Got the Script request");
+  is(scriptRequest.request.method, "GET", "Has the expected request method");
   is(
-    docRequest.frameId,
-    frameIdNav,
-    "Resource request returns same frameId as Page.navigate"
+    scriptRequest.request.headers.host,
+    "example.com",
+    "Script request has headers"
   );
   todo(
-    resourceRequest.loaderId === docRequest.loaderId,
+    scriptRequest.loaderId === docRequest.loaderId,
     "The same loaderId is used for dependent requests (Bug 1637838)"
   );
-  ok(
-    docRequest.timestamp <= resourceRequest.timestamp,
-    "Document request happens before resource request"
+  assertEventOrder(events[0], events[1]);
+
+  // Check subdocument request
+  const subdocRequest = events[2].payload;
+  is(
+    subdocRequest.type,
+    "Subdocument",
+    "Subdocument request has the expected type"
+  );
+  is(subdocRequest.documentURL, FRAMESET_URL, "documenURL matches request url");
+  is(subdocRequest.frameId, frameIdSubFrame, "Got the expected frame id");
+  is(
+    subdocRequest.requestId,
+    subdocRequest.loaderId,
+    "The request id is equal to the loader id"
+  );
+  is(subdocRequest.request.url, PAGE_URL, "Got the Subdocument request");
+  is(subdocRequest.request.method, "GET", "Has the expected request method");
+  is(
+    subdocRequest.request.headers.host,
+    "example.com",
+    "Subdocument request has headers"
   );
+  assertEventOrder(events[1], events[2]);
+
+  // Check script request (frame)
+  const subscriptRequest = events[3].payload;
+  is(subscriptRequest.type, "Script", "Script request has the expected type");
+  is(
+    subscriptRequest.documentURL,
+    PAGE_URL,
+    "documentURL is trigger document for the script request"
+  );
+  is(subscriptRequest.frameId, frameIdSubFrame, "Got the expected frame id");
+  todo(
+    subscriptRequest.loaderId === docRequest.loaderId,
+    "The same loaderId is used for dependent requests (Bug 1637838)"
+  );
+  is(subscriptRequest.request.url, PAGE_JS_URL, "Got the Script request");
+  is(
+    subscriptRequest.request.method,
+    "GET",
+    "Script request has the expected method"
+  );
+  is(
+    subscriptRequest.request.headers.host,
+    "example.com",
+    "Script request has headers"
+  );
+  assertEventOrder(events[2], events[3]);
 });
 
 function configureHistory(client, total) {
   const REQUEST = "Network.requestWillBeSent";
 
   const { Network } = client;
   const history = new RecordEvents(total);
 
   history.addRecorder({
     event: Network.requestWillBeSent,
     eventName: REQUEST,
     messageFn: payload => {
-      return `Received ${REQUEST} for ${payload.request?.url}`;
+      return `Received ${REQUEST} for ${payload.request.url}`;
     },
   });
 
   return history;
 }
--- a/remote/test/browser/network/browser_responseReceived.js
+++ b/remote/test/browser/network/browser_responseReceived.js
@@ -1,17 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-const PAGE_URL =
-  "http://example.com/browser/remote/test/browser/network/doc_networkEvents.html";
-const JS_URL =
-  "http://example.com/browser/remote/test/browser/network/file_networkEvents.js";
+const BASE_PATH = "http://example.com/browser/remote/test/browser/network";
+const FRAMESET_URL = `${BASE_PATH}/doc_frameset.html`;
+const FRAMESET_JS_URL = `${BASE_PATH}/file_framesetEvents.js`;
+const PAGE_URL = `${BASE_PATH}/doc_networkEvents.html`;
+const PAGE_JS_URL = `${BASE_PATH}/file_networkEvents.js`;
 
 add_task(async function noEventsWhenNetworkDomainDisabled({ client }) {
   const history = configureHistory(client, 0);
   await loadURL(PAGE_URL);
 
   const events = await history.record();
   is(events.length, 0, "Expected no Network.responseReceived events");
 });
@@ -25,108 +26,211 @@ add_task(async function noEventsAfterNet
   await loadURL(PAGE_URL);
 
   const events = await history.record();
   is(events.length, 0, "Expected no Network.responseReceived events");
 });
 
 add_task(async function documentNavigationWithResource({ client }) {
   const { Page, Network } = client;
+
   await Network.enable();
-  const history = configureHistory(client, 2);
+  await Page.enable();
+
+  const history = configureHistory(client, 4);
 
-  const { frameId: frameIdNav } = await Page.navigate({ url: PAGE_URL });
+  const frameAttached = Page.frameAttached();
+  const { frameId: frameIdNav } = await Page.navigate({ url: FRAMESET_URL });
+  const { frameId: frameIdSubframe } = await frameAttached;
   ok(frameIdNav, "Page.navigate returned a frameId");
 
   info("Wait for Network events");
   const events = await history.record();
-  is(events.length, 2, "Expected number of Network.responseReceived events");
+  is(events.length, 4, "Expected number of Network.responseReceived events");
 
+  // Check top-level document response
   const docResponse = events[0].payload;
-  is(docResponse.response.url, PAGE_URL, "Got the doc response");
+  is(docResponse.type, "Document", "Document response has expected type");
+  is(docResponse.frameId, frameIdNav, "Got the expected frame id");
+  is(
+    docResponse.requestId,
+    docResponse.loaderId,
+    "The response id is equal to the loader id"
+  );
+  is(docResponse.response.url, FRAMESET_URL, "Got the Document response");
   is(
     docResponse.response.mimeType,
     "text/html",
-    "Doc response has expected mimeType"
-  );
-  is(docResponse.type, "Document", "The doc response has 'Document' type");
-  is(
-    docResponse.requestId,
-    docResponse.loaderId,
-    "The doc request has requestId = loaderId"
+    "Document response has expected mimeType"
   );
+  ok(!!docResponse.response.headers.server, "Document response has headers");
+  is(docResponse.response.status, 200, "Document response has expected status");
   is(
-    docResponse.frameId,
-    frameIdNav,
-    "Doc response returns same frameId as Page.navigate"
+    docResponse.response.statusText,
+    "OK",
+    "Document response has expected status text"
   );
-  ok(!!docResponse.response.headers.server, "Doc response has headers");
-  is(docResponse.response.status, 200, "Doc response status is 200");
-  is(docResponse.response.statusText, "OK", "Doc response status is OK");
   if (docResponse.response.fromDiskCache === false) {
     is(
       docResponse.response.remoteIPAddress,
       "127.0.0.1",
-      "Doc response has an IP address"
+      "Document response has the expected IP address"
     );
     ok(
       typeof docResponse.response.remotePort == "number",
-      "Doc response has a remotePort"
+      "Document response has a remotePort"
     );
   }
   is(
     docResponse.response.protocol,
     "http/1.1",
-    "Doc response has expected protocol"
+    "Document response has expected protocol"
   );
 
-  const resourceResponse = events[1].payload;
-  is(resourceResponse.response.url, JS_URL, "Got the resource response");
+  // Check top-level script response
+  const scriptResponse = events[1].payload;
+  is(scriptResponse.type, "Script", "Script response has expected type");
+  is(scriptResponse.frameId, frameIdNav, "Got the expected frame id");
+  is(scriptResponse.response.url, FRAMESET_JS_URL, "Got the Script response");
   is(
-    resourceResponse.response.mimeType,
+    scriptResponse.response.mimeType,
     "application/x-javascript",
-    "Resource response has expected mimeType"
+    "Script response has expected mimeType"
+  );
+  ok(!!scriptResponse.response.headers.server, "Script response has headers");
+  is(
+    scriptResponse.response.status,
+    200,
+    "Script response has the expected status"
   );
   is(
-    resourceResponse.type,
-    "Script",
-    "The resource response has 'Script' type"
+    scriptResponse.response.statusText,
+    "OK",
+    "Script response has the expected status text"
   );
-  ok(!!resourceResponse.frameId, "Resource response has a frame id");
-  ok(
-    !!resourceResponse.response.headers.server,
-    "Resource response has headers"
-  );
-  is(resourceResponse.response.status, 200, "Resource response status is 200");
-  is(resourceResponse.response.statusText, "OK", "Response status is OK");
-  if (resourceResponse.response.fromDiskCache === false) {
+  if (scriptResponse.response.fromDiskCache === false) {
     is(
-      resourceResponse.response.remoteIPAddress,
+      scriptResponse.response.remoteIPAddress,
       docResponse.response.remoteIPAddress,
-      "Resource response has same IP address and doc response"
+      "Script response has same IP address as document response"
     );
     ok(
-      typeof resourceResponse.response.remotePort == "number",
-      "Resource response has a remotePort"
+      typeof scriptResponse.response.remotePort == "number",
+      "Script response has a remotePort"
     );
   }
   is(
-    resourceResponse.response.protocol,
+    scriptResponse.response.protocol,
     "http/1.1",
-    "Resource response has expected protocol"
+    "Script response has the expected protocol"
+  );
+
+  // Check subdocument response
+  const frameDocResponse = events[2].payload;
+  is(
+    frameDocResponse.type,
+    "Subdocument",
+    "Subdocument response has expected type"
+  );
+  is(frameDocResponse.frameId, frameIdSubframe, "Got the expected frame id");
+  is(
+    frameDocResponse.requestId,
+    frameDocResponse.loaderId,
+    "The response id is equal to the loader id"
+  );
+  is(
+    frameDocResponse.response.url,
+    PAGE_URL,
+    "Got the expected Document response"
+  );
+  is(
+    frameDocResponse.response.mimeType,
+    "text/html",
+    "Document response has expected mimeType"
+  );
+  ok(
+    !!frameDocResponse.response.headers.server,
+    "Subdocument response has headers"
+  );
+  is(
+    frameDocResponse.response.status,
+    200,
+    "Subdocument response has expected status"
+  );
+  is(
+    frameDocResponse.response.statusText,
+    "OK",
+    "Subdocument response has expected status text"
+  );
+  if (frameDocResponse.response.fromDiskCache === false) {
+    is(
+      frameDocResponse.response.remoteIPAddress,
+      "127.0.0.1",
+      "Subdocument response has the expected IP address"
+    );
+    ok(
+      typeof frameDocResponse.response.remotePort == "number",
+      "Subdocument response has a remotePort"
+    );
+  }
+  is(
+    frameDocResponse.response.protocol,
+    "http/1.1",
+    "Subdocument response has expected protocol"
+  );
+
+  // Check frame script response
+  const frameScriptResponse = events[3].payload;
+  is(frameScriptResponse.type, "Script", "Script response has expected type");
+  is(frameScriptResponse.frameId, frameIdSubframe, "Got the expected frame id");
+  is(frameScriptResponse.response.url, PAGE_JS_URL, "Got the Script response");
+  is(
+    frameScriptResponse.response.mimeType,
+    "application/x-javascript",
+    "Script response has expected mimeType"
+  );
+  ok(
+    !!frameScriptResponse.response.headers.server,
+    "Script response has headers"
+  );
+  is(
+    frameScriptResponse.response.status,
+    200,
+    "Script response has the expected status"
+  );
+  is(
+    frameScriptResponse.response.statusText,
+    "OK",
+    "Script response has the expected status text"
+  );
+  if (frameScriptResponse.response.fromDiskCache === false) {
+    is(
+      frameScriptResponse.response.remoteIPAddress,
+      docResponse.response.remoteIPAddress,
+      "Script response has same IP address as document response"
+    );
+    ok(
+      typeof frameScriptResponse.response.remotePort == "number",
+      "Script response has a remotePort"
+    );
+  }
+  is(
+    frameScriptResponse.response.protocol,
+    "http/1.1",
+    "Script response has the expected protocol"
   );
 });
 
 function configureHistory(client, total) {
   const RESPONSE = "Network.responseReceived";
 
   const { Network } = client;
   const history = new RecordEvents(total);
 
   history.addRecorder({
     event: Network.responseReceived,
     eventName: RESPONSE,
     messageFn: payload => {
-      return `Received ${RESPONSE} for ${payload.response?.url}`;
+      return `Received ${RESPONSE} for ${payload.response.url}`;
     },
   });
   return history;
 }
new file mode 100644
--- /dev/null
+++ b/remote/test/browser/network/doc_frameset.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Frameset for Network events</title>
+  <script type="text/javascript" src="file_framesetEvents.js"></script>
+</head>
+<body>
+  <iframe src="doc_networkEvents.html"></iframe>
+</body>
+</html>
--- a/remote/test/browser/network/doc_networkEvents.html
+++ b/remote/test/browser/network/doc_networkEvents.html
@@ -1,9 +1,9 @@
 <!DOCTYPE html>
 <html>
 <head>
   <title>Test page for Network events</title>
+  <script type="text/javascript" src="file_networkEvents.js"></script>
 </head>
 <body>
-  <script type="text/javascript" src="file_networkEvents.js"></script>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/remote/test/browser/network/file_framesetEvents.js
@@ -0,0 +1,2 @@
+// Test file to emit Network events.
+var foo = true;
--- a/remote/test/browser/network/head.js
+++ b/remote/test/browser/network/head.js
@@ -38,16 +38,39 @@ function assertCookie(cookie, expected =
 
   if (sameSite) {
     expectedCookie.sameSite = sameSite;
   }
 
   Assert.deepEqual(cookie, expectedCookie);
 }
 
+function assertEventOrder(first, second, options = {}) {
+  const { ignoreTimestamps = false } = options;
+
+  const firstDescription = getDescriptionForEvent(first);
+  const secondDescription = getDescriptionForEvent(second);
+
+  ok(
+    first.index < second.index,
+    `${firstDescription} received before ${secondDescription})`
+  );
+
+  if (!ignoreTimestamps) {
+    ok(
+      first.payload.timestamp <= second.payload.timestamp,
+      `Timestamp of ${firstDescription}) is earlier than ${secondDescription})`
+    );
+  }
+}
+
+function filterEventsByType(history, type) {
+  return history.filter(event => event.payload.type == type);
+}
+
 function getCookies() {
   return Services.cookies.cookies.map(cookie => {
     const data = {
       name: cookie.name,
       value: cookie.value,
       domain: cookie.host,
       path: cookie.path,
       expires: cookie.isSession ? -1 : cookie.expiry,
@@ -65,8 +88,14 @@ function getCookies() {
       ]);
 
       data.sameSite = sameSiteMap.get(cookie.sameSite);
     }
 
     return data;
   });
 }
+
+function getDescriptionForEvent(event) {
+  const { eventName, payload } = event;
+
+  return `${eventName}(${payload.type || payload.name || payload.frameId})`;
+}
--- a/remote/test/browser/page/browser_createIsolatedWorld.js
+++ b/remote/test/browser/page/browser_createIsolatedWorld.js
@@ -152,17 +152,19 @@ add_task(async function contextCreatedAf
     history,
     expectedEvents: [
       DESTROYED, // default, about:blank
       CREATED, // default, DOC
       CREATED, // isolated, DOC
     ],
   });
 
-  const contexts = history.findEvents(CREATED).map(event => event.context);
+  const contexts = history
+    .findEvents(CREATED)
+    .map(event => event.payload.context);
   const defaultContext = contexts[0];
   const isolatedContext = contexts[1];
   is(defaultContext.auxData.isDefault, true, "Default context is default");
   is(
     defaultContext.auxData.type,
     "default",
     "Default context has type 'default'"
   );
@@ -191,21 +193,21 @@ add_task(async function contextDestroyed
       DESTROYED, // isolated, about:blank
       CLEARED,
       CREATED, // default, DOC
     ],
   });
 
   const destroyed = history
     .findEvents(DESTROYED)
-    .map(event => event.executionContextId);
+    .map(event => event.payload.executionContextId);
   ok(destroyed.includes(isolatedContext.id), "Isolated context destroyed");
   ok(destroyed.includes(defaultContext.id), "Default context destroyed");
 
-  const { context: newContext } = history.findEvent(CREATED);
+  const { context: newContext } = history.findEvent(CREATED).payload;
   is(newContext.auxData.isDefault, true, "The new context is a default one");
   ok(!!newContext.id, "The new context has an id");
   ok(
     ![defaultContext.id, isolatedContext.id].includes(newContext.id),
     "The new context has a new id"
   );
 });
 
@@ -249,17 +251,17 @@ add_task(async function contextsForFrame
       CREATED, // default, DOC
       CREATED, // isolated, DOC_IFRAME
       CREATED, // isolated, DOC
     ],
   });
 
   const contextsCreated = historyTo
     .findEvents(CREATED)
-    .map(event => event.context);
+    .map(event => event.payload.context);
   const parentDefaultContextCreated = contextsCreated[0];
   const frameDefaultContextCreated = contextsCreated[1];
   const parentIsolatedContextCreated = contextsCreated[2];
   const frameIsolatedContextCreated = contextsCreated[3];
 
   checkIsolated(
     parentIsolatedContextCreated,
     contextIdParent,
@@ -290,25 +292,25 @@ add_task(async function contextsForFrame
       DESTROYED, // default, DOC_IFRAME
       DESTROYED, // isolated, DOC_IFRAME
       CREATED, // default, DOC
     ],
   });
 
   const contextsDestroyed = historyFrom
     .findEvents(DESTROYED)
-    .map(event => event.executionContextId);
+    .map(event => event.payload.executionContextId);
   contextsCreated.forEach(context => {
     ok(
       contextsDestroyed.includes(context.id),
       `Context with id ${context.id} destroyed`
     );
   });
 
-  const { context: newContext } = historyFrom.findEvent(CREATED);
+  const { context: newContext } = historyFrom.findEvent(CREATED).payload;
   is(newContext.auxData.isDefault, true, "The new context is a default one");
   ok(!!newContext.id, "The new context has an id");
   ok(
     ![parentDefaultContextCreated.id, frameDefaultContextCreated.id].includes(
       newContext.id
     ),
     "The new context has a new id"
   );
--- a/remote/test/browser/runtime/browser_executionContextEvents.js
+++ b/remote/test/browser/runtime/browser_executionContextEvents.js
@@ -35,24 +35,26 @@ add_task(async function eventsWhenNaviga
   const { Page, Runtime } = client;
 
   const previousContext = await enableRuntime(client);
   const history = recordContextEvents(Runtime, 3);
 
   const { frameId } = await Page.navigate({ url: DOC });
   await assertEventOrder({ history });
 
-  const { executionContextId: destroyedId } = history.findEvent(DESTROYED);
+  const { executionContextId: destroyedId } = history.findEvent(
+    DESTROYED
+  ).payload;
   is(
     destroyedId,
     previousContext.id,
     "The destroyed event reports the previous context id"
   );
 
-  const { context: contextCreated } = history.findEvent(CREATED);
+  const { context: contextCreated } = history.findEvent(CREATED).payload;
   checkDefaultContext(contextCreated);
   isnot(
     contextCreated.id,
     previousContext.id,
     "The new execution context has a different id"
   );
   is(
     contextCreated.auxData.frameId,
@@ -70,26 +72,28 @@ add_task(async function eventsWhenNaviga
   // Check navigation to a frameset
   const historyTo = recordContextEvents(Runtime, 4);
   await loadURL(DOC_IFRAME);
   await assertEventOrder({
     history: historyTo,
     expectedEvents: [DESTROYED, CLEARED, CREATED, CREATED],
   });
 
-  const { executionContextId: destroyedId } = historyTo.findEvent(DESTROYED);
+  const { executionContextId: destroyedId } = historyTo.findEvent(
+    DESTROYED
+  ).payload;
   is(
     destroyedId,
     previousContext.id,
     "The destroyed event reports the previous context id"
   );
 
   const contexts = historyTo.findEvents(CREATED);
-  const createdTopContext = contexts[0].context;
-  const createdFrameContext = contexts[1].context;
+  const createdTopContext = contexts[0].payload.context;
+  const createdFrameContext = contexts[1].payload.context;
 
   checkDefaultContext(createdTopContext);
   isnot(
     createdTopContext.id,
     previousContext.id,
     "The new execution context has a different id"
   );
   is(
@@ -115,27 +119,27 @@ add_task(async function eventsWhenNaviga
   await loadURL(DOC);
   await assertEventOrder({
     history: historyFrom,
     expectedEvents: [DESTROYED, CLEARED, DESTROYED, CLEARED, CREATED],
   });
 
   const destroyedContextIds = historyFrom.findEvents(DESTROYED);
   is(
-    destroyedContextIds[0].executionContextId,
+    destroyedContextIds[0].payload.executionContextId,
     createdTopContext.id,
     "The destroyed event reports the previous context id"
   );
   is(
-    destroyedContextIds[1].executionContextId,
+    destroyedContextIds[1].payload.executionContextId,
     createdFrameContext.id,
     "The destroyed event reports the previous frame's context id"
   );
 
-  const { context: contextCreated } = historyFrom.findEvent(CREATED);
+  const { context: contextCreated } = historyFrom.findEvent(CREATED).payload;
   checkDefaultContext(contextCreated);
   isnot(
     contextCreated.id,
     createdTopContext.id,
     "The new execution context has a different id"
   );
   is(
     contextCreated.origin,
@@ -154,24 +158,26 @@ add_task(async function eventsWhenNaviga
   const executionContextCreated = Runtime.executionContextCreated();
   await loadURL(toDataURL("other-test-page"));
   const { context: createdContext } = await executionContextCreated;
 
   const history = recordContextEvents(Runtime, 3);
   gBrowser.selectedBrowser.goBack();
   await assertEventOrder({ history });
 
-  const { executionContextId: destroyedId } = history.findEvent(DESTROYED);
+  const { executionContextId: destroyedId } = history.findEvent(
+    DESTROYED
+  ).payload;
   is(
     destroyedId,
     createdContext.id,
     "The destroyed event reports the current context id"
   );
 
-  const { context } = history.findEvent(CREATED);
+  const { context } = history.findEvent(CREATED).payload;
   checkDefaultContext(context);
   is(
     context.origin,
     previousContext.origin,
     "The new execution context has the same origin as the previous one."
   );
   isnot(
     context.id,
@@ -209,24 +215,24 @@ add_task(async function eventsWhenReload
 
   const history = recordContextEvents(Runtime, 3);
   const frameNavigated = Page.frameNavigated();
   gBrowser.selectedBrowser.reload();
   await frameNavigated;
 
   await assertEventOrder({ history });
 
-  const { executionContextId } = history.findEvent(DESTROYED);
+  const { executionContextId } = history.findEvent(DESTROYED).payload;
   is(
     executionContextId,
     previousContext.id,
     "The destroyed event reports the previous context id"
   );
 
-  const { context } = history.findEvent(CREATED);
+  const { context } = history.findEvent(CREATED).payload;
   checkDefaultContext(context);
   is(
     context.auxData.frameId,
     previousContext.auxData.frameId,
     "The execution context frame id is the same as before reloading"
   );
 
   isnot(
@@ -243,24 +249,26 @@ add_task(async function eventsWhenNaviga
   const history = recordContextEvents(Runtime, 3);
 
   await Runtime.evaluate({
     contextId: previousContext.id,
     expression: `window.location = '${DOC}';`,
   });
   await assertEventOrder({ history });
 
-  const { executionContextId: destroyedId } = history.findEvent(DESTROYED);
+  const { executionContextId: destroyedId } = history.findEvent(
+    DESTROYED
+  ).payload;
   is(
     destroyedId,
     previousContext.id,
     "The destroyed event reports the previous context id"
   );
 
-  const { context: createdContext } = history.findEvent(CREATED);
+  const { context: createdContext } = history.findEvent(CREATED).payload;
   checkDefaultContext(createdContext);
   is(
     createdContext.auxData.frameId,
     previousContext.auxData.frameId,
     "The execution context frame id is identical " +
       "to the one from before before setting the window's location"
   );
   isnot(
@@ -278,22 +286,22 @@ function recordContextEvents(Runtime, to
     eventName: DESTROYED,
     messageFn: payload => {
       return `Received ${DESTROYED} for id ${payload.executionContextId}`;
     },
   });
   history.addRecorder({
     event: Runtime.executionContextCreated,
     eventName: CREATED,
-    messageFn: payload => {
+    messageFn: ({ context }) => {
       return (
-        `Received ${CREATED} for id ${payload.context.id}` +
-        ` type: ${payload.context.auxData.type}` +
-        ` name: ${payload.context.name}` +
-        ` origin: ${payload.context.origin}`
+        `Received ${CREATED} for id ${context.id}` +
+        ` type: ${context.auxData.type}` +
+        ` name: ${context.name}` +
+        ` origin: ${context.origin}`
       );
     },
   });
   history.addRecorder({
     event: Runtime.executionContextsCleared,
     eventName: CLEARED,
   });