docShell
      .QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIWebProgress);
    webProgress.removeProgressListener(this.#progressListener);
  }

  onLocationChange(webProgress, request, location, flags) {
    // We don't care about inner-frame navigations.
    if (!webProgress.isTopLevel) {
      return;
    }

    // If this is a new document then the DOMContentLoaded event will trigger
    // the new interaction instead.
    if (!(flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT)) {
      return;
    }

    this.#recordNewPage();
  }

  #recordNewPage() {
    if (!this.docShell.currentDocumentChannel) {
      // If there is no document channel, then it is something we're not
      // interested in, but we do need to know that the previous interaction
      // has ended.
      this.sendAsyncMessage("Interactions:PageHide");
      return;
    }

    let docInfo = this.#getDocumentInfo();

    // This may happen when the page calls replaceState or pushState with the
    // same URL. We'll just consider this to not be a new page.
    if (docInfo.url == this.#currentURL) {
      return;
    }

    this.#currentURL = docInfo.url;

    if (
      this.docShell.currentDocumentChannel instanceof Ci.nsIHttpChannel &&
      !this.docShell.currentDocumentChannel.requestSucceeded
    ) {
      return;
    }

    this.sendAsyncMessage("Interactions:PageLoaded", docInfo);
  }

  async handleEvent(event) {
    if (this.isContentWindowPrivate) {
      // No recording in private browsing mode.
      return;
    }
    switch (event.type) {
      case "DOMContentLoaded": {
        this.#recordNewPage();
        break;
      }
      case "pagehide": {
        // We generally expect this to be an nsIHttpChannel, if it isn't
        // then the if statement below will return early anyway.
        let currentDocumentChannel = /** @type {nsIHttpChannel} */ (
          this.docShell.currentDocumentChannel
        );
        if (!currentDocumentChannel) {
          return;
        }

        if (!currentDocumentChannel.requestSucceeded) {
          return;
        }

        this.sendAsyncMessage("Interactions:PageHide");
        break;
      }
    }
  }

  /**
   * Returns the current document information for sending to the parent process.
   *
   * @returns {{ isActive: boolean, url: string, referrer: * }?}
   */
  #getDocumentInfo() {
    let doc = this.document;

    let referrer;
    if (doc.referrer) {
      referrer = Services.io.newURI(doc.referrer);
    }
    return {
      isActive: this.manager.browsingContext.isActive,
      url: doc.documentURIObject.specIgnoringRef,
      referrer: referrer?.specIgnoringRef,
    };
  }
}
PK