repo_description: Chrome DevTools for coding agents
repo_topics: browser chrome chrome-devtools debugging devtools mcp mcp-server puppeteer
code: /**
   * @license
   * Copyright 2026 Google LLC
   * SPDX-License-Identifier: Apache-2.0
   */
  
  import {
    createStackTraceForConsoleMessage,
    type TargetUniverse,
    SymbolizedError,
  } from '../DevtoolsUtils.js';
  import {UncaughtError} from '../PageCollector.js';
  import * as DevTools from '../third_party/index.js';
  import type {ConsoleMessage} from '../third_party/index.js';
  
  export interface ConsoleFormatterOptions {
    fetchDetailedData?: boolean;
    id: number;
    devTools?: TargetUniverse;
    resolvedArgsForTesting?: unknown[];
    resolvedStackTraceForTesting?: DevTools.DevTools.StackTrace.StackTrace.StackTrace;
    resolvedCauseForTesting?: SymbolizedError;
    isIgnoredForTesting?: IgnoreCheck;
  }
  
  export type IgnoreCheck = (
    frame: DevTools.DevTools.StackTrace.StackTrace.Frame,
  ) => boolean;
  
  interface ConsoleMessageConcise {
    type: string;
    text: string;
    argsCount: number;
    id: number;
  }
  
  interface ConsoleMessageDetailed extends ConsoleMessageConcise {
    // pre-formatted args.
    args: string[];
    // pre-formatted stacktrace.
    stackTrace?: string;
  }
  
  export class ConsoleFormatter {
    readonly #id: number;
    readonly #type: string;
    readonly #text: string;
  
    readonly #argCount: number;
    readonly #resolvedArgs: unknown[];
  
    readonly #stack?: DevTools.DevTools.StackTrace.StackTrace.StackTrace;
    readonly #cause?: SymbolizedError;
  
    readonly isIgnored: IgnoreCheck;
  
    private constructor(params: {
      id: number;
      type: string;
      text: string;
      argCount?: number;
      resolvedArgs?: unknown[];
      stack?: DevTools.DevTools.StackTrace.StackTrace.StackTrace;
      cause?: SymbolizedError;
      isIgnored: IgnoreCheck;
    }) {
      this.#id = params.id;
      this.#type = params.type;
      this.#text = params.text;
      this.#argCount = params.argCount ?? 0;
      this.#resolvedArgs = params.resolvedArgs ?? [];
      this.#stack = params.stack;
      this.#cause = params.cause;
      this.isIgnored = params.isIgnored;
    }
  
    static async from(
      msg: ConsoleMessage | UncaughtError,
      options: ConsoleFormatterOptions,
    ): Promise<ConsoleFormatter> {
      const ignoreListManager = options?.devTools?.universe.context.get(
        DevTools.DevTools.IgnoreListManager,
      );
      const isIgnored: IgnoreCheck =
        options.isIgnoredForTesting ||
        (frame => {
          if (!ignoreListManager) {
            return false;
          }
          if (frame.uiSourceCode) {
            return ignoreListManager.isUserOrSourceMapIgnoreListedUISourceCode(
              frame.uiSourceCode,
            );
          }
          if (frame.url) {
            return ignoreListManager.isUserIgnoreListedURL(
              frame.url as Parameters<
                DevTools.DevTools.IgnoreListManager['isUserIgnoreListedURL']
              >[0],
            );
          }
          return false;
        });
  
      if (msg instanceof UncaughtError) {
        const error = await SymbolizedError.fromDetails({
          devTools: options?.devTools,
          details: msg.details,
          targetId: msg.targetId,
          includeStackAndCause: options?.fetchDetailedData,
          resolvedStackTraceForTesting: options?.resolvedStackTraceForTesting,
          resolvedCauseForTesting: options?.resolvedCauseForTesting,
        });
        return new ConsoleFormatter({
          id: options.id,
          type: 'error',
          text: error.message,
          stack: error.stackTrace,
          cause: error.cause,
          isIgnored,
        });
      }
  
      let resolvedArgs: unknown[] = [];
      if (options.resolvedArgsForTesting) {
        resolvedArgs = options.resolvedArgsForTesting;
      } else if (options.fetchDetailedData) {
        resolvedArgs = await Promise.all(
          msg.args().map(async (arg, i) => {
            try {
              const remoteObject = arg.remoteObject();
              if (
                remoteObject.type === 'object' &&
                remoteObject.subtype === 'error'
              ) {
                return await SymbolizedError.fromError({
                  devTools: options.devTools,
                  error: remoteObject,
                  // @ts-expect-error Internal ConsoleMessage API
                  targetId: msg._targetId(),
                });
              }
              return await arg.jsonValue();
            } catch {
              return `<error: Argument ${i} is no longer available>`;
            }
          }),
        );
      }
  
      let stack: DevTools.DevTools.StackTrace.StackTrace.StackTrace | undefined;
      if (options.resolvedStackTraceForTesting) {
        stack = options.resolvedStackTraceForTesting;
      } else if (options.fetchDetailedData && options.devTools) {
        try {
          stack = await createStackTraceForConsoleMessage(options.devTools, msg);
        } catch {
          // ignore
        }
      }
  
      return new ConsoleFormatter({
        id: options.id,
        type: msg.type(),
        text: msg.text(),
        argCount: resolvedArgs.length || msg.args().length,
        resolvedArgs,
        stack,
        isIgnored,
      });
    }
  
    // The short format for a console message.
    toString(): string {
      return convertConsoleMessageConciseToString(this.toJSON());
    }
  
    // The verbose format for a console message, including all details.
    toStringDetailed(): string {
      return convertConsoleMessageConciseDetailedToString(this.toJSONDetailed());
    }
  
    #getArgs(): unknown[] {
      if (this.#resolvedArgs.length > 0) {
        const args = [...this.#resolvedArgs];
        // If there is no text, the first argument serves as text (see formatMessage).
        if (!this.#text) {
          args.shift();
        }
        return args;
      }
      return [];
    }
  
    toJSON(): ConsoleMessageConcise {
      return {
        type: this.#type,
        text: this.#text,
        argsCount: this.#argCount,
        id: this.#id,
      };
    }
  
    toJSONDetailed(): ConsoleMessageDetailed {
      return {
        id: this.#id,
        type: this.#type,
        text: this.#text,
        argsCount: this.#argCount,
        args: this.#getArgs().map(arg => formatArg(arg, this)),
        stackTrace: this.#stack
          ? formatStackTrace(this.#stack, this.#cause, this)
          : undefined,
      };
    }
  }
  
  function convertConsoleMessageConciseToString(msg: ConsoleMessageConcise) {
    return `msgid=${msg.id} [${msg.type}] ${msg.text} (${msg.argsCount} args)`;
  }
  
  function convertConsoleMessageConciseDetailedToString(
    msg: ConsoleMessageDetailed,
  ) {
    const result = [
      `ID: ${msg.id}`,
      `Message: ${msg.type}> ${msg.text}`,
      formatArgs(msg),
      ...(msg.stackTrace ? ['### Stack trace', msg.stackTrace] : []),
    ].filter(line => !!line);
    return result.join('\n');
  }
  
  function formatArgs(msg: ConsoleMessageDetailed): string {
    const args = msg.args;
  
    if (!args.length) {
      return '';
    }
  
    const result = ['### Arguments'];
  
    for (const [key, arg] of args.entries()) {
      result.push(`Arg #${key}: ${arg}`);
    }
  
    return result.join('\n');
  }
  
  function formatArg(arg: unknown, formatter: {isIgnored: IgnoreCheck}) {
    if (arg instanceof SymbolizedError) {
      return [
        arg.message,
        arg.stackTrace
          ? formatStackTrace(arg.stackTrace, arg.cause, formatter)
          : undefined,
      ]
        .filter(line => !!line)
        .join('\n');
    }
    return typeof arg === 'object' ? JSON.stringify(arg) : String(arg);
  }
  
  const STACK_TRACE_MAX_LINES = 50;
  
  function formatStackTrace(
    stackTrace: DevTools.DevTools.StackTrace.StackTrace.StackTrace,
    cause: SymbolizedError | undefined,
    formatter: {isIgnored: IgnoreCheck},
  ): string {
    const lines = formatStackTraceInner(stackTrace, cause, formatter);
    const includedLines = lines.slice(0, STACK_TRACE_MAX_LINES);
    const reminderCount = lines.length - includedLines.length;
  
    return [
      ...includedLines,
      reminderCount > 0 ? `... and ${reminderCount} more frames` : '',
      'Note: line and column numbers use 1-based indexing',
    ]
      .filter(line => !!line)
      .join('\n');
  }
  
  function formatStackTraceInner(
    stackTrace: DevTools.DevTools.StackTrace.StackTrace.StackTrace | undefined,
    cause: SymbolizedError | undefined,
    formatter: {isIgnored: IgnoreCheck},
  ): string[] {
    if (!stackTrace) {
      return [];
    }
  
    return [
      ...formatFragment(stackTrace.syncFragment, formatter),
      ...stackTrace.asyncFragments
        .map(item => formatAsyncFragment(item, formatter))
        .flat(),
      ...formatCause(cause, formatter),
    ];
  }
  
  function formatFragment(
    fragment: DevTools.DevTools.StackTrace.StackTrace.Fragment,
    formatter: {isIgnored: IgnoreCheck},
  ): string[] {
    const frames = fragment.frames.filter(frame => !formatter.isIgnored(frame));
    return frames.map(formatFrame);
  }
  
  function formatAsyncFragment(
    fragment: DevTools.DevTools.StackTrace.StackTrace.AsyncFragment,
    formatter: {isIgnored: IgnoreCheck},
  ): string[] {
    const formattedFrames = formatFragment(fragment, formatter);
    if (formattedFrames.length === 0) {
      return [];
    }
  
    const separatorLineLength = 40;
    const prefix = `--- ${fragment.description || 'async'} `;
    const separator = prefix + '-'.repeat(separatorLineLength - prefix.length);
    return [separator, ...formattedFrames];
  }
  
  function formatFrame(
    frame: DevTools.DevTools.StackTrace.StackTrace.Frame,
  ): string {
    let result = `at ${frame.name ?? '<anonymous>'}`;
    if (frame.uiSourceCode) {
      const location = frame.uiSourceCode.uiLocation(frame.line, frame.column);
      result += ` (${location.linkText(/* skipTrim */ false, /* showColumnNumber */ true)})`;
    } else if (frame.url) {
      result += ` (${frame.url}:${frame.line}:${frame.column})`;
    }
    return result;
  }
  
  function formatCause(
    cause: SymbolizedError | undefined,
    formatter: {isIgnored: IgnoreCheck},
  ): string[] {
    if (!cause) {
      return [];
    }
  
    return [
      `Caused by: ${cause.message}`,
      ...formatStackTraceInner(cause.stackTrace, cause.cause, formatter),
    ];
  }
  