UNPKG

chrome-devtools-frontend

Version:
841 lines (750 loc) • 33.4 kB
/* * Copyright (C) 2011 Google Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Google Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ import * as Protocol from '../../generated/protocol.js'; import * as Common from '../common/common.js'; import * as Host from '../host/host.js'; import * as i18n from '../i18n/i18n.js'; import * as Platform from '../platform/platform.js'; import {FrontendMessageSource, FrontendMessageType} from './ConsoleModelTypes.js'; export {FrontendMessageSource, FrontendMessageType} from './ConsoleModelTypes.js'; import {CPUProfilerModel, Events as CPUProfilerModelEvents, type EventData} from './CPUProfilerModel.js'; import {Events as DebuggerModelEvents, type Location, BreakpointType} from './DebuggerModel.js'; import {LogModel} from './LogModel.js'; import {RemoteObject} from './RemoteObject.js'; import { Events as ResourceTreeModelEvents, ResourceTreeModel, type ResourceTreeFrame, type PrimaryPageChangeType, } from './ResourceTreeModel.js'; import { Events as RuntimeModelEvents, RuntimeModel, type ConsoleAPICall, type ExceptionWithTimestamp, type ExecutionContext, type QueryObjectRequestedEvent, } from './RuntimeModel.js'; import {Capability, type Target, Type} from './Target.js'; import {TargetManager} from './TargetManager.js'; import {SDKModel} from './SDKModel.js'; import {COND_BREAKPOINT_SOURCE_URL, LOGPOINT_SOURCE_URL} from './DebuggerModel.js'; const UIStrings = { /** *@description Text shown when the main frame (page) of the website was navigated to a different URL. *@example {https://example.com} PH1 */ navigatedToS: 'Navigated to {PH1}', /** *@description Text shown when the main frame (page) of the website was navigated to a different URL * and the page was restored from back/forward cache (https://web.dev/bfcache/). *@example {https://example.com} PH1 */ bfcacheNavigation: 'Navigation to {PH1} was restored from back/forward cache (see https://web.dev/bfcache/)', /** *@description Text shown in the console when a performance profile (with the given name) was started. *@example {title} PH1 */ profileSStarted: 'Profile \'\'{PH1}\'\' started.', /** *@description Text shown in the console when a performance profile (with the given name) was stopped. *@example {name} PH1 */ profileSFinished: 'Profile \'\'{PH1}\'\' finished.', /** *@description Error message shown in the console after the user tries to save a JavaScript value to a temporary variable. */ failedToSaveToTempVariable: 'Failed to save to temp variable.', }; const str_ = i18n.i18n.registerUIStrings('core/sdk/ConsoleModel.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export class ConsoleModel extends SDKModel<EventTypes> { #messagesInternal: ConsoleMessage[]; readonly #messagesByTimestamp: Platform.MapUtilities.Multimap<number, ConsoleMessage>; readonly #messageByExceptionId: Map<RuntimeModel, Map<number, ConsoleMessage>>; #warningsInternal: number; #errorsInternal: number; #violationsInternal: number; #pageLoadSequenceNumber: number; readonly #targetListeners: WeakMap<Target, Common.EventTarget.EventDescriptor[]>; constructor(target: Target) { super(target); this.#messagesInternal = []; this.#messagesByTimestamp = new Platform.MapUtilities.Multimap(); this.#messageByExceptionId = new Map(); this.#warningsInternal = 0; this.#errorsInternal = 0; this.#violationsInternal = 0; this.#pageLoadSequenceNumber = 0; this.#targetListeners = new WeakMap(); const resourceTreeModel = target.model(ResourceTreeModel); if (!resourceTreeModel || resourceTreeModel.cachedResourcesLoaded()) { this.initTarget(target); return; } const eventListener = resourceTreeModel.addEventListener(ResourceTreeModelEvents.CachedResourcesLoaded, () => { Common.EventTarget.removeEventListeners([eventListener]); this.initTarget(target); }); } private initTarget(target: Target): void { const eventListeners = []; const cpuProfilerModel = target.model(CPUProfilerModel); if (cpuProfilerModel) { eventListeners.push(cpuProfilerModel.addEventListener( CPUProfilerModelEvents.ConsoleProfileStarted, this.consoleProfileStarted.bind(this, cpuProfilerModel))); eventListeners.push(cpuProfilerModel.addEventListener( CPUProfilerModelEvents.ConsoleProfileFinished, this.consoleProfileFinished.bind(this, cpuProfilerModel))); } const resourceTreeModel = target.model(ResourceTreeModel); if (resourceTreeModel && target.parentTarget()?.type() !== Type.Frame) { eventListeners.push(resourceTreeModel.addEventListener( ResourceTreeModelEvents.PrimaryPageChanged, this.primaryPageChanged, this)); } const runtimeModel = target.model(RuntimeModel); if (runtimeModel) { eventListeners.push(runtimeModel.addEventListener( RuntimeModelEvents.ExceptionThrown, this.exceptionThrown.bind(this, runtimeModel))); eventListeners.push(runtimeModel.addEventListener( RuntimeModelEvents.ExceptionRevoked, this.exceptionRevoked.bind(this, runtimeModel))); eventListeners.push(runtimeModel.addEventListener( RuntimeModelEvents.ConsoleAPICalled, this.consoleAPICalled.bind(this, runtimeModel))); if (target.parentTarget()?.type() !== Type.Frame) { eventListeners.push(runtimeModel.debuggerModel().addEventListener( DebuggerModelEvents.GlobalObjectCleared, this.clearIfNecessary, this)); } eventListeners.push(runtimeModel.addEventListener( RuntimeModelEvents.QueryObjectRequested, this.queryObjectRequested.bind(this, runtimeModel))); } this.#targetListeners.set(target, eventListeners); } targetRemoved(target: Target): void { const runtimeModel = target.model(RuntimeModel); if (runtimeModel) { this.#messageByExceptionId.delete(runtimeModel); } Common.EventTarget.removeEventListeners(this.#targetListeners.get(target) || []); } async evaluateCommandInConsole( executionContext: ExecutionContext, originatingMessage: ConsoleMessage, expression: string, useCommandLineAPI: boolean): Promise<void> { const result = await executionContext.evaluate( { expression, objectGroup: 'console', includeCommandLineAPI: useCommandLineAPI, silent: false, returnByValue: false, generatePreview: true, replMode: true, allowUnsafeEvalBlockedByCSP: false, }, Common.Settings.Settings.instance().moduleSetting('consoleUserActivationEval').get(), /* awaitPromise */ false); Host.userMetrics.actionTaken(Host.UserMetrics.Action.ConsoleEvaluated); if ('error' in result) { return; } await Common.Console.Console.instance().showPromise(); this.dispatchEventToListeners( Events.CommandEvaluated, {result: result.object, commandMessage: originatingMessage, exceptionDetails: result.exceptionDetails}); } addCommandMessage(executionContext: ExecutionContext, text: string): ConsoleMessage { const commandMessage = new ConsoleMessage( executionContext.runtimeModel, Protocol.Log.LogEntrySource.Javascript, null, text, {type: FrontendMessageType.Command}); commandMessage.setExecutionContextId(executionContext.id); this.addMessage(commandMessage); return commandMessage; } addMessage(msg: ConsoleMessage): void { msg.setPageLoadSequenceNumber(this.#pageLoadSequenceNumber); if (msg.source === FrontendMessageSource.ConsoleAPI && msg.type === Protocol.Runtime.ConsoleAPICalledEventType.Clear) { this.clearIfNecessary(); } this.#messagesInternal.push(msg); this.#messagesByTimestamp.set(msg.timestamp, msg); const runtimeModel = msg.runtimeModel(); const exceptionId = msg.getExceptionId(); if (exceptionId && runtimeModel) { let modelMap = this.#messageByExceptionId.get(runtimeModel); if (!modelMap) { modelMap = new Map(); this.#messageByExceptionId.set(runtimeModel, modelMap); } modelMap.set(exceptionId, msg); } this.incrementErrorWarningCount(msg); this.dispatchEventToListeners(Events.MessageAdded, msg); } private exceptionThrown( runtimeModel: RuntimeModel, event: Common.EventTarget.EventTargetEvent<ExceptionWithTimestamp>): void { const exceptionWithTimestamp = event.data; const affectedResources = extractExceptionMetaData(exceptionWithTimestamp.details.exceptionMetaData); const consoleMessage = ConsoleMessage.fromException( runtimeModel, exceptionWithTimestamp.details, undefined, exceptionWithTimestamp.timestamp, undefined, affectedResources); consoleMessage.setExceptionId(exceptionWithTimestamp.details.exceptionId); this.addMessage(consoleMessage); } private exceptionRevoked(runtimeModel: RuntimeModel, event: Common.EventTarget.EventTargetEvent<number>): void { const exceptionId = event.data; const modelMap = this.#messageByExceptionId.get(runtimeModel); const exceptionMessage = modelMap ? modelMap.get(exceptionId) : null; if (!exceptionMessage) { return; } this.#errorsInternal--; exceptionMessage.level = Protocol.Log.LogEntryLevel.Verbose; this.dispatchEventToListeners(Events.MessageUpdated, exceptionMessage); } private consoleAPICalled(runtimeModel: RuntimeModel, event: Common.EventTarget.EventTargetEvent<ConsoleAPICall>): void { const call = event.data; let level: Protocol.Log.LogEntryLevel = Protocol.Log.LogEntryLevel.Info; if (call.type === Protocol.Runtime.ConsoleAPICalledEventType.Debug) { level = Protocol.Log.LogEntryLevel.Verbose; } else if ( call.type === Protocol.Runtime.ConsoleAPICalledEventType.Error || call.type === Protocol.Runtime.ConsoleAPICalledEventType.Assert) { level = Protocol.Log.LogEntryLevel.Error; } else if (call.type === Protocol.Runtime.ConsoleAPICalledEventType.Warning) { level = Protocol.Log.LogEntryLevel.Warning; } else if ( call.type === Protocol.Runtime.ConsoleAPICalledEventType.Info || call.type === Protocol.Runtime.ConsoleAPICalledEventType.Log) { level = Protocol.Log.LogEntryLevel.Info; } let message = ''; if (call.args.length && call.args[0].unserializableValue) { message = call.args[0].unserializableValue; } else if (call.args.length && (typeof call.args[0].value !== 'object' || call.args[0].value === null)) { message = String(call.args[0].value); } else if (call.args.length && call.args[0].description) { message = call.args[0].description; } const callFrame = call.stackTrace && call.stackTrace.callFrames.length ? call.stackTrace.callFrames[0] : null; const details = { type: call.type, url: callFrame?.url as Platform.DevToolsPath.UrlString | undefined, line: callFrame?.lineNumber, column: callFrame?.columnNumber, parameters: call.args, stackTrace: call.stackTrace, timestamp: call.timestamp, executionContextId: call.executionContextId, context: call.context, }; const consoleMessage = new ConsoleMessage(runtimeModel, FrontendMessageSource.ConsoleAPI, level, (message as string), details); for (const msg of this.#messagesByTimestamp.get(consoleMessage.timestamp).values()) { if (consoleMessage.isEqual(msg)) { return; } } this.addMessage(consoleMessage); } private queryObjectRequested( runtimeModel: RuntimeModel, event: Common.EventTarget.EventTargetEvent<QueryObjectRequestedEvent>): void { const {objects, executionContextId} = event.data; const details = { type: FrontendMessageType.QueryObjectResult, parameters: [objects], executionContextId, }; const consoleMessage = new ConsoleMessage( runtimeModel, FrontendMessageSource.ConsoleAPI, Protocol.Log.LogEntryLevel.Info, '', details); this.addMessage(consoleMessage); } private clearIfNecessary(): void { if (!Common.Settings.Settings.instance().moduleSetting('preserveConsoleLog').get()) { this.clear(); } ++this.#pageLoadSequenceNumber; } private primaryPageChanged( event: Common.EventTarget.EventTargetEvent<{frame: ResourceTreeFrame, type: PrimaryPageChangeType}>): void { if (Common.Settings.Settings.instance().moduleSetting('preserveConsoleLog').get()) { const {frame} = event.data; if (frame.backForwardCacheDetails.restoredFromCache) { Common.Console.Console.instance().log(i18nString(UIStrings.bfcacheNavigation, {PH1: frame.url})); } else { Common.Console.Console.instance().log(i18nString(UIStrings.navigatedToS, {PH1: frame.url})); } } } private consoleProfileStarted( cpuProfilerModel: CPUProfilerModel, event: Common.EventTarget.EventTargetEvent<EventData>): void { const {data} = event; this.addConsoleProfileMessage( cpuProfilerModel, Protocol.Runtime.ConsoleAPICalledEventType.Profile, data.scriptLocation, i18nString(UIStrings.profileSStarted, {PH1: data.title})); } private consoleProfileFinished( cpuProfilerModel: CPUProfilerModel, event: Common.EventTarget.EventTargetEvent<EventData>): void { const {data} = event; this.addConsoleProfileMessage( cpuProfilerModel, Protocol.Runtime.ConsoleAPICalledEventType.ProfileEnd, data.scriptLocation, i18nString(UIStrings.profileSFinished, {PH1: data.title})); } private addConsoleProfileMessage( cpuProfilerModel: CPUProfilerModel, type: MessageType, scriptLocation: Location, messageText: string): void { const script = scriptLocation.script(); const callFrames = [{ functionName: '', scriptId: scriptLocation.scriptId, url: script ? script.contentURL() : '', lineNumber: scriptLocation.lineNumber, columnNumber: scriptLocation.columnNumber || 0, }]; this.addMessage(new ConsoleMessage( cpuProfilerModel.runtimeModel(), FrontendMessageSource.ConsoleAPI, Protocol.Log.LogEntryLevel.Info, messageText, {type, stackTrace: {callFrames}})); } private incrementErrorWarningCount(msg: ConsoleMessage): void { if (msg.source === Protocol.Log.LogEntrySource.Violation) { this.#violationsInternal++; return; } switch (msg.level) { case Protocol.Log.LogEntryLevel.Warning: this.#warningsInternal++; break; case Protocol.Log.LogEntryLevel.Error: this.#errorsInternal++; break; } } messages(): ConsoleMessage[] { return this.#messagesInternal; } // messages[] are not ordered by timestamp. static allMessagesUnordered(): ConsoleMessage[] { const messages = []; for (const target of TargetManager.instance().targets()) { const targetMessages = target.model(ConsoleModel)?.messages() || []; messages.push(...targetMessages); } return messages; } static requestClearMessages(): void { for (const logModel of TargetManager.instance().models(LogModel)) { logModel.requestClear(); } for (const runtimeModel of TargetManager.instance().models(RuntimeModel)) { runtimeModel.discardConsoleEntries(); } for (const target of TargetManager.instance().targets()) { target.model(ConsoleModel)?.clear(); } } private clear(): void { this.#messagesInternal = []; this.#messagesByTimestamp.clear(); this.#messageByExceptionId.clear(); this.#errorsInternal = 0; this.#warningsInternal = 0; this.#violationsInternal = 0; this.dispatchEventToListeners(Events.ConsoleCleared); } errors(): number { return this.#errorsInternal; } static allErrors(): number { let errors = 0; for (const target of TargetManager.instance().targets()) { errors += target.model(ConsoleModel)?.errors() || 0; } return errors; } warnings(): number { return this.#warningsInternal; } static allWarnings(): number { let warnings = 0; for (const target of TargetManager.instance().targets()) { warnings += target.model(ConsoleModel)?.warnings() || 0; } return warnings; } violations(): number { return this.#violationsInternal; } static allViolations(): number { let violations = 0; for (const target of TargetManager.instance().targets()) { violations += target.model(ConsoleModel)?.violations() || 0; } return violations; } async saveToTempVariable(currentExecutionContext: ExecutionContext|null, remoteObject: RemoteObject|null): Promise<void> { if (!remoteObject || !currentExecutionContext) { failedToSave(null); return; } const executionContext = (currentExecutionContext as ExecutionContext); const result = await executionContext.globalObject(/* objectGroup */ '', /* generatePreview */ false); if ('error' in result || Boolean(result.exceptionDetails) || !result.object) { failedToSave('object' in result && result.object || null); return; } const globalObject = result.object; const callFunctionResult = // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration // @ts-expect-error await globalObject.callFunction(saveVariable, [RemoteObject.toCallArgument(remoteObject)]); globalObject.release(); if (callFunctionResult.wasThrown || !callFunctionResult.object || callFunctionResult.object.type !== 'string') { failedToSave(callFunctionResult.object || null); } else { const text = (callFunctionResult.object.value as string); const message = this.addCommandMessage(executionContext, text); void this.evaluateCommandInConsole(executionContext, message, text, /* useCommandLineAPI */ false); } if (callFunctionResult.object) { callFunctionResult.object.release(); } function saveVariable(this: Window, value: Protocol.Runtime.CallArgument): string { const prefix = 'temp'; let index = 1; while ((prefix + index) in this) { ++index; } const name = prefix + index; // @ts-ignore Assignment to global object this[name] = value; return name; } function failedToSave(result: RemoteObject|null): void { let message = i18nString(UIStrings.failedToSaveToTempVariable); if (result) { message = (message + ' ' + result.description as Common.UIString.LocalizedString); } Common.Console.Console.instance().error(message); } } } // TODO(crbug.com/1167717): Make this a const enum again // eslint-disable-next-line rulesdir/const_enum export enum Events { ConsoleCleared = 'ConsoleCleared', MessageAdded = 'MessageAdded', MessageUpdated = 'MessageUpdated', CommandEvaluated = 'CommandEvaluated', } export interface CommandEvaluatedEvent { result: RemoteObject; commandMessage: ConsoleMessage; exceptionDetails?: Protocol.Runtime.ExceptionDetails|undefined; } export type EventTypes = { [Events.ConsoleCleared]: void, [Events.MessageAdded]: ConsoleMessage, [Events.MessageUpdated]: ConsoleMessage, [Events.CommandEvaluated]: CommandEvaluatedEvent, }; export interface AffectedResources { requestId?: Protocol.Network.RequestId; issueId?: Protocol.Audits.IssueId; } // eslint-disable-next-line @typescript-eslint/no-explicit-any function extractExceptionMetaData(metaData: any|undefined): AffectedResources|undefined { if (!metaData) { return undefined; } return {requestId: metaData.requestId || undefined, issueId: metaData.issueId || undefined}; } function areAffectedResourcesEquivalent(a?: AffectedResources, b?: AffectedResources): boolean { // Not considering issueId, as that would prevent de-duplication of console #messages. return a?.requestId === b?.requestId; } function areStackTracesEquivalent( stackTrace1?: Protocol.Runtime.StackTrace, stackTrace2?: Protocol.Runtime.StackTrace): boolean { if (!stackTrace1 !== !stackTrace2) { return false; } if (!stackTrace1 || !stackTrace2) { return true; } const callFrames1 = stackTrace1.callFrames; const callFrames2 = stackTrace2.callFrames; if (callFrames1.length !== callFrames2.length) { return false; } for (let i = 0, n = callFrames1.length; i < n; ++i) { if (callFrames1[i].scriptId !== callFrames2[i].scriptId || callFrames1[i].functionName !== callFrames2[i].functionName || callFrames1[i].lineNumber !== callFrames2[i].lineNumber || callFrames1[i].columnNumber !== callFrames2[i].columnNumber) { return false; } } return areStackTracesEquivalent(stackTrace1.parent, stackTrace2.parent); } export interface ConsoleMessageDetails { type?: MessageType; url?: Platform.DevToolsPath.UrlString; line?: number; column?: number; parameters?: (string|RemoteObject|Protocol.Runtime.RemoteObject)[]; stackTrace?: Protocol.Runtime.StackTrace; timestamp?: number; executionContextId?: number; scriptId?: Protocol.Runtime.ScriptId; workerId?: string; context?: string; affectedResources?: AffectedResources; category?: Protocol.Log.LogEntryCategory; } export class ConsoleMessage { readonly #runtimeModelInternal: RuntimeModel|null; source: MessageSource; level: Protocol.Log.LogEntryLevel|null; messageText: string; readonly type: MessageType; url: Platform.DevToolsPath.UrlString|undefined; line: number; column: number; parameters: (string|RemoteObject|Protocol.Runtime.RemoteObject)[]|undefined; stackTrace: Protocol.Runtime.StackTrace|undefined; timestamp: number; #executionContextId: number; scriptId?: Protocol.Runtime.ScriptId; workerId?: string; context?: string; #originatingConsoleMessage: ConsoleMessage|null = null; #pageLoadSequenceNumber?: number = undefined; #exceptionId?: number = undefined; #affectedResources?: AffectedResources; category?: Protocol.Log.LogEntryCategory; /** * The parent frame of the `console.log` call of logpoints or conditional breakpoints * if they called `console.*` explicitly. The parent frame is where V8 paused * and consequently where the logpoint is set. * * Is `null` for page console.logs, commands, command results, etc. */ readonly stackFrameWithBreakpoint: Protocol.Runtime.CallFrame|null = null; readonly #originatingBreakpointType: BreakpointType|null = null; constructor( runtimeModel: RuntimeModel|null, source: MessageSource, level: Protocol.Log.LogEntryLevel|null, messageText: string, details?: ConsoleMessageDetails) { this.#runtimeModelInternal = runtimeModel; this.source = source; this.level = (level as Protocol.Log.LogEntryLevel | null); this.messageText = messageText; this.type = details?.type || Protocol.Runtime.ConsoleAPICalledEventType.Log; this.url = details?.url; this.line = details?.line || 0; this.column = details?.column || 0; this.parameters = details?.parameters; this.stackTrace = details?.stackTrace; this.timestamp = details?.timestamp || Date.now(); this.#executionContextId = details?.executionContextId || 0; this.scriptId = details?.scriptId; this.workerId = details?.workerId; this.#affectedResources = details?.affectedResources; this.category = details?.category; if (!this.#executionContextId && this.#runtimeModelInternal) { if (this.scriptId) { this.#executionContextId = this.#runtimeModelInternal.executionContextIdForScriptId(this.scriptId); } else if (this.stackTrace) { this.#executionContextId = this.#runtimeModelInternal.executionContextForStackTrace(this.stackTrace); } } if (details?.context) { const match = details?.context.match(/[^#]*/); this.context = match?.[0]; } if (this.stackTrace) { const {callFrame, type} = ConsoleMessage.#stackFrameWithBreakpoint(this.stackTrace); this.stackFrameWithBreakpoint = callFrame; this.#originatingBreakpointType = type; } } getAffectedResources(): AffectedResources|undefined { return this.#affectedResources; } setPageLoadSequenceNumber(pageLoadSequenceNumber: number): void { this.#pageLoadSequenceNumber = pageLoadSequenceNumber; } static fromException( runtimeModel: RuntimeModel, exceptionDetails: Protocol.Runtime.ExceptionDetails, messageType?: Protocol.Runtime.ConsoleAPICalledEventType|FrontendMessageType, timestamp?: number, forceUrl?: Platform.DevToolsPath.UrlString, affectedResources?: AffectedResources): ConsoleMessage { const details = { type: messageType, url: forceUrl || exceptionDetails.url as Platform.DevToolsPath.UrlString, line: exceptionDetails.lineNumber, column: exceptionDetails.columnNumber, parameters: exceptionDetails.exception ? [RemoteObject.fromLocalObject(exceptionDetails.text), exceptionDetails.exception] : undefined, stackTrace: exceptionDetails.stackTrace, timestamp, executionContextId: exceptionDetails.executionContextId, scriptId: exceptionDetails.scriptId, affectedResources, }; return new ConsoleMessage( runtimeModel, Protocol.Log.LogEntrySource.Javascript, Protocol.Log.LogEntryLevel.Error, RuntimeModel.simpleTextFromException(exceptionDetails), details); } runtimeModel(): RuntimeModel|null { return this.#runtimeModelInternal; } target(): Target|null { return this.#runtimeModelInternal ? this.#runtimeModelInternal.target() : null; } setOriginatingMessage(originatingMessage: ConsoleMessage): void { this.#originatingConsoleMessage = originatingMessage; this.#executionContextId = originatingMessage.#executionContextId; } originatingMessage(): ConsoleMessage|null { return this.#originatingConsoleMessage; } setExecutionContextId(executionContextId: number): void { this.#executionContextId = executionContextId; } getExecutionContextId(): number { return this.#executionContextId; } getExceptionId(): number|undefined { return this.#exceptionId; } setExceptionId(exceptionId: number): void { this.#exceptionId = exceptionId; } isGroupMessage(): boolean { return this.type === Protocol.Runtime.ConsoleAPICalledEventType.StartGroup || this.type === Protocol.Runtime.ConsoleAPICalledEventType.StartGroupCollapsed || this.type === Protocol.Runtime.ConsoleAPICalledEventType.EndGroup; } isGroupStartMessage(): boolean { return this.type === Protocol.Runtime.ConsoleAPICalledEventType.StartGroup || this.type === Protocol.Runtime.ConsoleAPICalledEventType.StartGroupCollapsed; } isErrorOrWarning(): boolean { return (this.level === Protocol.Log.LogEntryLevel.Warning || this.level === Protocol.Log.LogEntryLevel.Error); } isGroupable(): boolean { const isUngroupableError = this.level === Protocol.Log.LogEntryLevel.Error && (this.source === Protocol.Log.LogEntrySource.Javascript || this.source === Protocol.Log.LogEntrySource.Network); return ( this.source !== FrontendMessageSource.ConsoleAPI && this.type !== FrontendMessageType.Command && this.type !== FrontendMessageType.Result && this.type !== FrontendMessageType.System && !isUngroupableError); } groupCategoryKey(): string { return [this.source, this.level, this.type, this.#pageLoadSequenceNumber].join(':'); } isEqual(msg: ConsoleMessage|null): boolean { if (!msg) { return false; } if (this.parameters) { if (!msg.parameters || this.parameters.length !== msg.parameters.length) { return false; } for (let i = 0; i < msg.parameters.length; ++i) { const msgParam = msg.parameters[i]; const param = this.parameters[i]; if (typeof msgParam === 'string' || typeof param === 'string') { // TODO(chromium:1136435): Remove this case. return false; } // Never treat objects as equal - their properties might change over time. Errors can be treated as equal // since they are always formatted as strings. if (msgParam.type === 'object' && msgParam.subtype !== 'error') { return false; } if (param.type !== msgParam.type || param.value !== msgParam.value || param.description !== msgParam.description) { return false; } } } return (this.runtimeModel() === msg.runtimeModel()) && (this.source === msg.source) && (this.type === msg.type) && (this.level === msg.level) && (this.line === msg.line) && (this.url === msg.url) && (this.scriptId === msg.scriptId) && (this.messageText === msg.messageText) && (this.#executionContextId === msg.#executionContextId) && areAffectedResourcesEquivalent(this.#affectedResources, msg.#affectedResources) && areStackTracesEquivalent(this.stackTrace, msg.stackTrace); } get originatesFromLogpoint(): boolean { return this.#originatingBreakpointType === BreakpointType.LOGPOINT; } /** @returns true, iff this was a console.* call in a conditional breakpoint */ get originatesFromConditionalBreakpoint(): boolean { return this.#originatingBreakpointType === BreakpointType.CONDITIONAL_BREAKPOINT; } static #stackFrameWithBreakpoint({callFrames}: Protocol.Runtime.StackTrace): {callFrame: Protocol.Runtime.CallFrame|null, type: BreakpointType|null} { // Note that breakpoint condition code could in theory call into user JS and back into // "condition-defined" functions. This means that the top-most // stack frame is not necessarily the `console.log` call, but there could be other things // on top. We want the LAST marker frame in the stack. // We search FROM THE TOP for the last marked stack frame and // return it's parent (successor). const markerSourceUrls = [COND_BREAKPOINT_SOURCE_URL, LOGPOINT_SOURCE_URL]; const lastBreakpointFrameIndex = callFrames.findLastIndex(({url}) => markerSourceUrls.includes(url)); if (lastBreakpointFrameIndex === -1 || lastBreakpointFrameIndex === callFrames.length - 1) { // We either didn't find any breakpoint or we didn't capture enough stack // frames and the breakpoint condition is the bottom-most frame. return {callFrame: null, type: null}; } const type = callFrames[lastBreakpointFrameIndex].url === LOGPOINT_SOURCE_URL ? BreakpointType.LOGPOINT : BreakpointType.CONDITIONAL_BREAKPOINT; return {callFrame: callFrames[lastBreakpointFrameIndex + 1], type}; } } SDKModel.register(ConsoleModel, {capabilities: Capability.JS, autostart: true}); export type MessageSource = Protocol.Log.LogEntrySource|FrontendMessageSource; export type MessageLevel = Protocol.Log.LogEntryLevel; export type MessageType = Protocol.Runtime.ConsoleAPICalledEventType|FrontendMessageType; export const MessageSourceDisplayName = new Map<MessageSource, string>(([ [Protocol.Log.LogEntrySource.XML, 'xml'], [Protocol.Log.LogEntrySource.Javascript, 'javascript'], [Protocol.Log.LogEntrySource.Network, 'network'], [FrontendMessageSource.ConsoleAPI, 'console-api'], [Protocol.Log.LogEntrySource.Storage, 'storage'], [Protocol.Log.LogEntrySource.Appcache, 'appcache'], [Protocol.Log.LogEntrySource.Rendering, 'rendering'], [FrontendMessageSource.CSS, 'css'], [Protocol.Log.LogEntrySource.Security, 'security'], [Protocol.Log.LogEntrySource.Deprecation, 'deprecation'], [Protocol.Log.LogEntrySource.Worker, 'worker'], [Protocol.Log.LogEntrySource.Violation, 'violation'], [Protocol.Log.LogEntrySource.Intervention, 'intervention'], [Protocol.Log.LogEntrySource.Recommendation, 'recommendation'], [Protocol.Log.LogEntrySource.Other, 'other'], ]));