// Copyright 2011 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as Common from '../../core/common/common.js';
import * as i18n from '../../core/i18n/i18n.js';
import * as Platform from '../../core/platform/platform.js';
import * as SDK from '../../core/sdk/sdk.js';
import * as Protocol from '../../generated/protocol.js';

const UIStrings = {
  /**
   * @description When DevTools doesn't know the URL that initiated a network request, we
   * show this phrase instead. 'unknown' would also work in this context.
   */
  anonymous: '<anonymous>',
} as const;
const str_ = i18n.i18n.registerUIStrings('models/logs/NetworkLog.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

let networkLogInstance: NetworkLog|undefined;

export class NetworkLog extends Common.ObjectWrapper.ObjectWrapper<EventTypes> implements
    SDK.TargetManager.SDKModelObserver<SDK.NetworkManager.NetworkManager> {
  #requests: SDK.NetworkRequest.NetworkRequest[] = [];
  #sentNetworkRequests: Protocol.Network.Request[] = [];
  #receivedNetworkResponses: Protocol.Network.Response[] = [];
  #requestsSet = new Set<SDK.NetworkRequest.NetworkRequest>();
  readonly #requestsMap = new Map<string, SDK.NetworkRequest.NetworkRequest[]>();
  readonly #pageLoadForManager = new Map<SDK.NetworkManager.NetworkManager, SDK.PageLoad.PageLoad>();
  readonly #unresolvedPreflightRequests = new Map<string, SDK.NetworkRequest.NetworkRequest>();
  readonly #modelListeners = new WeakMap<SDK.NetworkManager.NetworkManager, Common.EventTarget.EventDescriptor[]>();
  readonly #initiatorData = new WeakMap<SDK.NetworkRequest.NetworkRequest, InitiatorData>();
  #isRecording = true;

  constructor() {
    super();

    SDK.TargetManager.TargetManager.instance().observeModels(SDK.NetworkManager.NetworkManager, this);
    const recordLogSetting: Common.Settings.Setting<boolean> =
        Common.Settings.Settings.instance().moduleSetting('network-log.record-log');
    recordLogSetting.addChangeListener(() => {
      const preserveLogSetting = Common.Settings.Settings.instance().moduleSetting('network-log.preserve-log');
      if (!preserveLogSetting.get() && recordLogSetting.get()) {
        this.reset(true);
      }
      this.setIsRecording((recordLogSetting.get()));
    }, this);
  }

  static instance(): NetworkLog {
    if (!networkLogInstance) {
      networkLogInstance = new NetworkLog();
    }
    return networkLogInstance;
  }

  static removeInstance(): void {
    networkLogInstance = undefined;
  }

  modelAdded(networkManager: SDK.NetworkManager.NetworkManager): void {
    const eventListeners = [];
    eventListeners.push(
        networkManager.addEventListener(SDK.NetworkManager.Events.RequestStarted, this.onRequestStarted, this));
    eventListeners.push(
        networkManager.addEventListener(SDK.NetworkManager.Events.RequestUpdated, this.onRequestUpdated, this));
    eventListeners.push(
        networkManager.addEventListener(SDK.NetworkManager.Events.RequestRedirected, this.onRequestRedirect, this));
    eventListeners.push(
        networkManager.addEventListener(SDK.NetworkManager.Events.RequestFinished, this.onRequestUpdated, this));
    eventListeners.push(networkManager.addEventListener(
        SDK.NetworkManager.Events.MessageGenerated, this.networkMessageGenerated.bind(this, networkManager)));
    eventListeners.push(
        networkManager.addEventListener(SDK.NetworkManager.Events.ResponseReceived, this.onResponseReceived, this));

    const resourceTreeModel = networkManager.target().model(SDK.ResourceTreeModel.ResourceTreeModel);
    if (resourceTreeModel) {
      eventListeners.push(
          resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.WillReloadPage, this.willReloadPage, this));
      eventListeners.push(resourceTreeModel.addEventListener(
          SDK.ResourceTreeModel.Events.PrimaryPageChanged, this.onPrimaryPageChanged, this));
      eventListeners.push(resourceTreeModel.addEventListener(SDK.ResourceTreeModel.Events.Load, this.onLoad, this));
      eventListeners.push(resourceTreeModel.addEventListener(
          SDK.ResourceTreeModel.Events.DOMContentLoaded, this.onDOMContentLoaded.bind(this, resourceTreeModel)));
    }

    this.#modelListeners.set(networkManager, eventListeners);
  }

  modelRemoved(networkManager: SDK.NetworkManager.NetworkManager): void {
    this.removeNetworkManagerListeners(networkManager);
  }

  private removeNetworkManagerListeners(networkManager: SDK.NetworkManager.NetworkManager): void {
    Common.EventTarget.removeEventListeners(this.#modelListeners.get(networkManager) || []);
  }

  setIsRecording(enabled: boolean): void {
    if (this.#isRecording === enabled) {
      return;
    }
    this.#isRecording = enabled;
    if (enabled) {
      SDK.TargetManager.TargetManager.instance().observeModels(SDK.NetworkManager.NetworkManager, this);
    } else {
      SDK.TargetManager.TargetManager.instance().unobserveModels(SDK.NetworkManager.NetworkManager, this);
      SDK.TargetManager.TargetManager.instance()
          .models(SDK.NetworkManager.NetworkManager)
          .forEach(this.removeNetworkManagerListeners.bind(this));
    }
  }

  requestForURL(url: Platform.DevToolsPath.UrlString): SDK.NetworkRequest.NetworkRequest|null {
    return this.#requests.find(request => request.url() === url) || null;
  }

  originalRequestForURL(url: Platform.DevToolsPath.UrlString): Protocol.Network.Request|null {
    return this.#sentNetworkRequests.find(request => request.url === url) || null;
  }

  originalResponseForURL(url: Platform.DevToolsPath.UrlString): Protocol.Network.Response|null {
    return this.#receivedNetworkResponses.find(response => response.url === url) || null;
  }

  requests(): SDK.NetworkRequest.NetworkRequest[] {
    return this.#requests;
  }

  requestByManagerAndId(networkManager: SDK.NetworkManager.NetworkManager, requestId: string):
      SDK.NetworkRequest.NetworkRequest|null {
    // We iterate backwards because the last item will likely be the one needed for console network request lookups.
    for (let i = this.#requests.length - 1; i >= 0; i--) {
      const request = this.#requests[i];
      if (requestId === request.requestId() &&
          networkManager === SDK.NetworkManager.NetworkManager.forRequest(request)) {
        return request;
      }
    }
    return null;
  }

  private requestByManagerAndURL(
      networkManager: SDK.NetworkManager.NetworkManager,
      url: Platform.DevToolsPath.UrlString): SDK.NetworkRequest.NetworkRequest|null {
    for (const request of this.#requests) {
      if (url === request.url() && networkManager === SDK.NetworkManager.NetworkManager.forRequest(request)) {
        return request;
      }
    }
    return null;
  }

  private initializeInitiatorSymbolIfNeeded(request: SDK.NetworkRequest.NetworkRequest): InitiatorData {
    let initiatorInfo = this.#initiatorData.get(request);
    if (initiatorInfo) {
      return initiatorInfo;
    }
    initiatorInfo = {
      info: null,
      chain: null,
    };
    this.#initiatorData.set(request, initiatorInfo);
    return initiatorInfo;
  }

  static initiatorInfoForRequest(request: SDK.NetworkRequest.NetworkRequest, existingInitiatorData?: InitiatorData):
      InitiatorInfo {
    const initiatorInfo: InitiatorData = existingInitiatorData || {
      info: null,
      chain: null,
    };

    let type = SDK.NetworkRequest.InitiatorType.OTHER;
    let url = Platform.DevToolsPath.EmptyUrlString;
    let lineNumber: number|undefined = undefined;
    let columnNumber: number|undefined = undefined;
    let scriptId: Protocol.Runtime.ScriptId|null = null;
    let initiatorStack: Protocol.Runtime.StackTrace|null = null;
    let initiatorRequest: SDK.NetworkRequest.NetworkRequest|null = null;
    const initiator = request.initiator();

    const redirectSource = request.redirectSource();
    if (redirectSource) {
      type = SDK.NetworkRequest.InitiatorType.REDIRECT;
      url = redirectSource.url();
    } else if (initiator) {
      if (initiator.type === Protocol.Network.InitiatorType.Parser) {
        type = SDK.NetworkRequest.InitiatorType.PARSER;
        url = initiator.url ? initiator.url as Platform.DevToolsPath.UrlString : url;
        lineNumber = initiator.lineNumber;
        columnNumber = initiator.columnNumber;
      } else if (initiator.type === Protocol.Network.InitiatorType.Script) {
        for (let stack: (Protocol.Runtime.StackTrace|undefined) = initiator.stack; stack;) {
          const topFrame = stack.callFrames.length ? stack.callFrames[0] : null;
          if (!topFrame) {
            stack = stack.parent;
            continue;
          }
          type = SDK.NetworkRequest.InitiatorType.SCRIPT;
          url = (topFrame.url || i18nString(UIStrings.anonymous) as string) as Platform.DevToolsPath.UrlString;
          lineNumber = topFrame.lineNumber;
          columnNumber = topFrame.columnNumber;
          scriptId = topFrame.scriptId;
          break;
        }
        if (!initiator.stack && initiator.url) {
          type = SDK.NetworkRequest.InitiatorType.SCRIPT;
          url = initiator.url as Platform.DevToolsPath.UrlString;
          lineNumber = initiator.lineNumber;
        }
        if (initiator.stack?.callFrames?.length) {
          initiatorStack = initiator.stack;
        }
      } else if (initiator.type === Protocol.Network.InitiatorType.Preload) {
        type = SDK.NetworkRequest.InitiatorType.PRELOAD;
      } else if (initiator.type === Protocol.Network.InitiatorType.Preflight) {
        type = SDK.NetworkRequest.InitiatorType.PREFLIGHT;
        initiatorRequest = request.preflightInitiatorRequest();
      } else if (initiator.type === Protocol.Network.InitiatorType.SignedExchange) {
        type = SDK.NetworkRequest.InitiatorType.SIGNED_EXCHANGE;
        url = initiator.url as Platform.DevToolsPath.UrlString || Platform.DevToolsPath.EmptyUrlString;
      }
    }
    initiatorInfo.info = {type, url, lineNumber, columnNumber, scriptId, stack: initiatorStack, initiatorRequest};
    return initiatorInfo.info;
  }

  initiatorInfoForRequest(request: SDK.NetworkRequest.NetworkRequest): InitiatorInfo {
    const initiatorInfo = this.initializeInitiatorSymbolIfNeeded(request);
    if (initiatorInfo.info) {
      return initiatorInfo.info;
    }

    return NetworkLog.initiatorInfoForRequest(request, initiatorInfo);
  }

  initiatorGraphForRequest(request: SDK.NetworkRequest.NetworkRequest): InitiatorGraph {
    const initiated = new Map<SDK.NetworkRequest.NetworkRequest, SDK.NetworkRequest.NetworkRequest>();
    const networkManager = SDK.NetworkManager.NetworkManager.forRequest(request);
    for (const otherRequest of this.#requests) {
      const otherRequestManager = SDK.NetworkManager.NetworkManager.forRequest(otherRequest);
      if (networkManager === otherRequestManager && this.initiatorChain(otherRequest).has(request)) {
        // save parent request of otherRequst in order to build the initiator chain table later
        const initiatorRequest = this.initiatorRequest(otherRequest);
        if (initiatorRequest) {
          initiated.set(otherRequest, initiatorRequest);
        }
      }
    }
    return {initiators: this.initiatorChain(request), initiated};
  }

  private initiatorChain(request: SDK.NetworkRequest.NetworkRequest): Set<SDK.NetworkRequest.NetworkRequest> {
    const initiatorDataForRequest = this.initializeInitiatorSymbolIfNeeded(request);
    let initiatorChainCache = initiatorDataForRequest.chain;
    if (initiatorChainCache) {
      return initiatorChainCache;
    }

    initiatorChainCache = new Set();

    let checkRequest: SDK.NetworkRequest.NetworkRequest|null = request;
    while (checkRequest) {
      const initiatorData = this.initializeInitiatorSymbolIfNeeded(checkRequest);
      if (initiatorData.chain) {
        initiatorChainCache = initiatorChainCache.union(initiatorData.chain);
        break;
      }
      if (initiatorChainCache.has(checkRequest)) {
        break;
      }
      initiatorChainCache.add(checkRequest);
      checkRequest = this.initiatorRequest(checkRequest);
    }
    initiatorDataForRequest.chain = initiatorChainCache;
    return initiatorChainCache;
  }

  private initiatorRequest(request: SDK.NetworkRequest.NetworkRequest): SDK.NetworkRequest.NetworkRequest|null {
    const initiatorData = this.initializeInitiatorSymbolIfNeeded(request);
    if (initiatorData.request !== undefined) {
      return initiatorData.request;
    }
    const url = this.initiatorInfoForRequest(request).url;
    const networkManager = SDK.NetworkManager.NetworkManager.forRequest(request);
    initiatorData.request = networkManager ? this.requestByManagerAndURL(networkManager, url) : null;
    return initiatorData.request;
  }

  private willReloadPage(): void {
    if (!Common.Settings.Settings.instance().moduleSetting('network-log.preserve-log').get()) {
      this.reset(true);
    }
  }

  private onPrimaryPageChanged(
      event: Common.EventTarget.EventTargetEvent<
          {frame: SDK.ResourceTreeModel.ResourceTreeFrame, type: SDK.ResourceTreeModel.PrimaryPageChangeType}>): void {
    const mainFrame = event.data.frame;
    const manager = mainFrame.resourceTreeModel().target().model(SDK.NetworkManager.NetworkManager);
    if (!manager || mainFrame.resourceTreeModel().target().parentTarget()?.type() === SDK.Target.Type.FRAME) {
      return;
    }

    // If a page resulted in an error, the browser will navigate to an internal error page
    // hosted at 'chrome-error://...'. In this case, skip the frame navigated event to preserve
    // the network log.
    if (mainFrame.url !== mainFrame.unreachableUrl() && Common.ParsedURL.schemeIs(mainFrame.url, 'chrome-error:')) {
      return;
    }

    const preserveLog = Common.Settings.Settings.instance().moduleSetting('network-log.preserve-log').get();

    const oldRequests = this.#requests;
    const oldManagerRequests =
        this.#requests.filter(request => SDK.NetworkManager.NetworkManager.forRequest(request) === manager);
    const oldRequestsSet = this.#requestsSet;
    this.#requests = [];
    this.#sentNetworkRequests = [];
    this.#receivedNetworkResponses = [];
    this.#requestsSet = new Set();
    this.#requestsMap.clear();
    this.#unresolvedPreflightRequests.clear();
    this.dispatchEventToListeners(Events.Reset, {clearIfPreserved: !preserveLog});

    // Preserve requests from the new session.
    let currentPageLoad: SDK.PageLoad.PageLoad|null = null;
    const requestsToAdd = [];
    for (const request of oldManagerRequests) {
      if (event.data.type !== SDK.ResourceTreeModel.PrimaryPageChangeType.ACTIVATION &&
          request.loaderId !== mainFrame.loaderId) {
        continue;
      }
      if (!currentPageLoad) {
        currentPageLoad = new SDK.PageLoad.PageLoad(request);
        let redirectSource = request.redirectSource();
        while (redirectSource) {
          requestsToAdd.push(redirectSource);
          redirectSource = redirectSource.redirectSource();
        }
      }
      requestsToAdd.push(request);
    }

    // Preserve service worker requests from the new session.
    const serviceWorkerRequestsToAdd = [];
    for (const swRequest of oldRequests) {
      if (!swRequest.initiatedByServiceWorker()) {
        continue;
      }

      // If there is a matching request that came before this one, keep it.
      const keepRequest = requestsToAdd.some(
          request => request.url() === swRequest.url() && request.issueTime() <= swRequest.issueTime());
      if (keepRequest) {
        serviceWorkerRequestsToAdd.push(swRequest);
      }
    }
    requestsToAdd.push(...serviceWorkerRequestsToAdd);

    for (const request of requestsToAdd) {
      currentPageLoad?.bindRequest(request);
      oldRequestsSet.delete(request);
      this.addRequest(request);
    }

    if (preserveLog) {
      for (const request of oldRequestsSet) {
        this.addRequest(request, true);
        request.preserved = true;
      }
    }

    if (currentPageLoad) {
      this.#pageLoadForManager.set(manager, currentPageLoad);
    }
  }

  private addRequest(request: SDK.NetworkRequest.NetworkRequest, preserveLog?: boolean): void {
    this.#requests.push(request);
    this.#requestsSet.add(request);
    const requestList = this.#requestsMap.get(request.requestId());
    if (!requestList) {
      this.#requestsMap.set(request.requestId(), [request]);
    } else {
      requestList.push(request);
    }
    this.tryResolvePreflightRequests(request);
    this.dispatchEventToListeners(Events.RequestAdded, {request, preserveLog});
  }

  private removeRequest(request: SDK.NetworkRequest.NetworkRequest): void {
    const index = this.#requests.indexOf(request);
    if (index > -1) {
      this.#requests.splice(index, 1);
    }
    this.#requestsSet.delete(request);
    this.#requestsMap.delete(request.requestId());
    this.dispatchEventToListeners(Events.RequestRemoved, {request});
  }

  private tryResolvePreflightRequests(request: SDK.NetworkRequest.NetworkRequest): void {
    if (request.isPreflightRequest()) {
      const initiator = request.initiator();
      if (initiator?.requestId) {
        const [initiatorRequest] = this.requestsForId(initiator.requestId);
        if (initiatorRequest) {
          request.setPreflightInitiatorRequest(initiatorRequest);
          initiatorRequest.setPreflightRequest(request);
        } else {
          this.#unresolvedPreflightRequests.set(initiator.requestId, request);
        }
      }
    } else {
      const preflightRequest = this.#unresolvedPreflightRequests.get(request.requestId());
      if (preflightRequest) {
        this.#unresolvedPreflightRequests.delete(request.requestId());
        request.setPreflightRequest(preflightRequest);
        preflightRequest.setPreflightInitiatorRequest(request);
        // Force recomputation of initiator info, if it already exists.
        const data = this.#initiatorData.get(preflightRequest);
        if (data) {
          data.info = null;
        }
        this.dispatchEventToListeners(Events.RequestUpdated, {request: preflightRequest});
      }
    }
  }

  importRequests(requests: SDK.NetworkRequest.NetworkRequest[]): void {
    this.reset(true);
    this.#requests = [];
    this.#sentNetworkRequests = [];
    this.#receivedNetworkResponses = [];
    this.#requestsSet.clear();
    this.#requestsMap.clear();
    this.#unresolvedPreflightRequests.clear();
    for (const request of requests) {
      this.addRequest(request);
    }
  }

  private onRequestStarted(event: Common.EventTarget.EventTargetEvent<SDK.NetworkManager.RequestStartedEvent>): void {
    const {request, originalRequest} = event.data;
    if (originalRequest) {
      this.#sentNetworkRequests.push(originalRequest);
    }
    this.#requestsSet.add(request);
    const manager = SDK.NetworkManager.NetworkManager.forRequest(request);
    const pageLoad = manager ? this.#pageLoadForManager.get(manager) : null;
    if (pageLoad) {
      pageLoad.bindRequest(request);
    }
    this.addRequest(request);
  }

  private onResponseReceived(event: Common.EventTarget.EventTargetEvent<SDK.NetworkManager.ResponseReceivedEvent>):
      void {
    const response = event.data.response;
    this.#receivedNetworkResponses.push(response);
  }

  private onRequestUpdated(event: Common.EventTarget.EventTargetEvent<SDK.NetworkRequest.NetworkRequest>): void {
    const request = event.data;
    if (!this.#requestsSet.has(request)) {
      return;
    }

    this.dispatchEventToListeners(Events.RequestUpdated, {request});
  }

  private onRequestRedirect(event: Common.EventTarget.EventTargetEvent<SDK.NetworkRequest.NetworkRequest>): void {
    this.#initiatorData.delete(event.data);
  }

  private onDOMContentLoaded(
      resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel,
      event: Common.EventTarget.EventTargetEvent<number>): void {
    const networkManager = resourceTreeModel.target().model(SDK.NetworkManager.NetworkManager);
    const pageLoad = networkManager ? this.#pageLoadForManager.get(networkManager) : null;
    if (pageLoad) {
      pageLoad.contentLoadTime = event.data;
    }
  }

  private onLoad(
      event: Common.EventTarget
          .EventTargetEvent<{resourceTreeModel: SDK.ResourceTreeModel.ResourceTreeModel, loadTime: number}>): void {
    const networkManager = event.data.resourceTreeModel.target().model(SDK.NetworkManager.NetworkManager);
    const pageLoad = networkManager ? this.#pageLoadForManager.get(networkManager) : null;
    if (pageLoad) {
      pageLoad.loadTime = event.data.loadTime;
    }
  }

  reset(clearIfPreserved: boolean): void {
    this.#requests = [];
    this.#sentNetworkRequests = [];
    this.#receivedNetworkResponses = [];
    this.#requestsSet.clear();
    this.#requestsMap.clear();
    this.#unresolvedPreflightRequests.clear();
    const managers = new Set<SDK.NetworkManager.NetworkManager>(
        SDK.TargetManager.TargetManager.instance().models(SDK.NetworkManager.NetworkManager));
    for (const manager of this.#pageLoadForManager.keys()) {
      if (!managers.has(manager)) {
        this.#pageLoadForManager.delete(manager);
      }
    }

    this.dispatchEventToListeners(Events.Reset, {clearIfPreserved});
  }

  private networkMessageGenerated(
      networkManager: SDK.NetworkManager.NetworkManager,
      event: Common.EventTarget.EventTargetEvent<SDK.NetworkManager.MessageGeneratedEvent>): void {
    const {message, warning, requestId} = event.data;
    const consoleMessage = new SDK.ConsoleModel.ConsoleMessage(
        networkManager.target().model(SDK.RuntimeModel.RuntimeModel), Protocol.Log.LogEntrySource.Network,
        warning ? Protocol.Log.LogEntryLevel.Warning : Protocol.Log.LogEntryLevel.Info, message);
    this.associateConsoleMessageWithRequest(consoleMessage, requestId);
    networkManager.target().model(SDK.ConsoleModel.ConsoleModel)?.addMessage(consoleMessage);
  }

  associateConsoleMessageWithRequest(consoleMessage: SDK.ConsoleModel.ConsoleMessage, requestId: string): void {
    const target = consoleMessage.target();
    const networkManager = target ? target.model(SDK.NetworkManager.NetworkManager) : null;
    if (!networkManager) {
      return;
    }
    const request = this.requestByManagerAndId(networkManager, requestId);
    if (!request) {
      return;
    }
    consoleMessageToRequest.set(consoleMessage, request);
    const initiator = request.initiator();
    if (initiator) {
      consoleMessage.stackTrace = initiator.stack || undefined;
      if (initiator.url) {
        consoleMessage.url = initiator.url as Platform.DevToolsPath.UrlString;
        consoleMessage.line = initiator.lineNumber || 0;
      }
    }
  }

  static requestForConsoleMessage(consoleMessage: SDK.ConsoleModel.ConsoleMessage): SDK.NetworkRequest.NetworkRequest
      |null {
    return consoleMessageToRequest.get(consoleMessage) || null;
  }

  requestsForId(requestId: string): SDK.NetworkRequest.NetworkRequest[] {
    return this.#requestsMap.get(requestId) || [];
  }
}

const consoleMessageToRequest = new WeakMap<SDK.ConsoleModel.ConsoleMessage, SDK.NetworkRequest.NetworkRequest>();

export enum Events {
  /* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */
  Reset = 'Reset',
  RequestAdded = 'RequestAdded',
  RequestUpdated = 'RequestUpdated',
  RequestRemoved = 'RequestRemoved',
  /* eslint-enable @typescript-eslint/naming-convention */
}

export interface ResetEvent {
  clearIfPreserved: boolean;
}

export interface EventTypes {
  [Events.Reset]: ResetEvent;
  [Events.RequestAdded]: {request: SDK.NetworkRequest.NetworkRequest, preserveLog?: boolean};
  [Events.RequestUpdated]: {request: SDK.NetworkRequest.NetworkRequest};
  [Events.RequestRemoved]: {request: SDK.NetworkRequest.NetworkRequest};
}

export interface InitiatorData {
  info: InitiatorInfo|null;
  chain: Set<SDK.NetworkRequest.NetworkRequest>|null;
  request?: SDK.NetworkRequest.NetworkRequest|null;
}

export interface InitiatorGraph {
  initiators: Set<SDK.NetworkRequest.NetworkRequest>;
  initiated: Map<SDK.NetworkRequest.NetworkRequest, SDK.NetworkRequest.NetworkRequest>;
}

export interface InitiatorInfo {
  type: SDK.NetworkRequest.InitiatorType;
  // generally this is a url but can also contain "<anonymous>"
  url: Platform.DevToolsPath.UrlString;
  lineNumber: number|undefined;
  columnNumber: number|undefined;
  scriptId: Protocol.Runtime.ScriptId|null;
  stack: Protocol.Runtime.StackTrace|null;
  initiatorRequest: SDK.NetworkRequest.NetworkRequest|null;
}
