static async compute_(data, context) {
    const networkRecords = await NetworkRecords.request(data.devtoolsLog, context);
    /** @type {EntityCache} */
    const madeUpEntityCache = new Map();
    /** @type {Map<string, LH.Artifacts.Entity>} */
    const entityByUrl = new Map();
    /** @type {Map<LH.Artifacts.Entity, Set<string>>} */
    const urlsByEntity = new Map();

    EntityClassification._preloadChromeExtensionsToCache(madeUpEntityCache, data.devtoolsLog);

    for (const record of networkRecords) {
      const {url} = record;
      if (entityByUrl.has(url)) continue;

      const entity = thirdPartyWeb.getEntity(url) ||
        EntityClassification._makeUpAnEntity(madeUpEntityCache, url);
      if (!entity) continue;

      const entityURLs = urlsByEntity.get(entity) || new Set();
      entityURLs.add(url);
      urlsByEntity.set(entity, entityURLs);
      entityByUrl.set(url, entity);
    }

    // When available, first party identification will be done via
    // `mainDocumentUrl` (for navigations), and falls back to `finalDisplayedUrl` (for timespan/snapshot).
    // See https://github.com/GoogleChrome/lighthouse/issues/13706
    const firstPartyUrl = data.URL.mainDocumentUrl || data.URL.finalDisplayedUrl;
    const firstParty = thirdPartyWeb.getEntity(firstPartyUrl) ||
      EntityClassification._makeUpAnEntity(madeUpEntityCache, firstPartyUrl);

    /**
     * Convenience function to check if a URL belongs to first party.
     * @param {string} url
     * @return {boolean}
     */
    function isFirstParty(url) {
      const entityUrl = entityByUrl.get(url);
      return entityUrl === firstParty;
    }

    return {
      entityByUrl,
      urlsByEntity,
      firstParty,
      isFirstParty,
    };
  }
@param {{URL: LH.Artifacts['URL'], devtoolsLog: LH.DevtoolsLog}} data
@param {LH.Artifacts.ComputedContext} context
@return {Promise<LH.Artifacts.EntityClassification>}