UNPKG

chrome-devtools-frontend

Version:
305 lines (267 loc) • 11.1 kB
// Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import * as SDK from '../../core/sdk/sdk.js'; import * as AiAssistanceModel from '../../models/ai_assistance/ai_assistance.js'; import * as Bindings from '../../models/bindings/bindings.js'; import * as Formatter from '../../models/formatter/formatter.js'; import * as Logs from '../../models/logs/logs.js'; import * as Components from '../../ui/legacy/components/utils/utils.js'; import type * as Console from '../console/console.js'; const MAX_MESSAGE_SIZE = 1000; const MAX_STACK_TRACE_SIZE = 1000; const MAX_CODE_SIZE = 1000; export enum SourceType { MESSAGE = 'message', STACKTRACE = 'stacktrace', NETWORK_REQUEST = 'networkRequest', RELATED_CODE = 'relatedCode', } export interface Source { type: SourceType; value: string; } export class PromptBuilder { #consoleMessage: Console.ConsoleViewMessage.ConsoleViewMessage; constructor(consoleMessage: Console.ConsoleViewMessage.ConsoleViewMessage) { this.#consoleMessage = consoleMessage; } async getNetworkRequest(): Promise<SDK.NetworkRequest.NetworkRequest|undefined> { const requestId = this.#consoleMessage.consoleMessage().getAffectedResources()?.requestId; if (!requestId) { return; } const log = Logs.NetworkLog.NetworkLog.instance(); // TODO: we might try handling redirect requests too later. return log.requestsForId(requestId)[0]; } /** * Gets the source file associated with the top of the message's stacktrace. * Returns an empty string if the source is not available for any reasons. */ async getMessageSourceCode(): Promise<{text: string, columnNumber: number, lineNumber: number}> { const callframe = this.#consoleMessage.consoleMessage().stackTrace?.callFrames[0]; const runtimeModel = this.#consoleMessage.consoleMessage().runtimeModel(); const debuggerModel = runtimeModel?.debuggerModel(); if (!debuggerModel || !runtimeModel || !callframe) { return {text: '', columnNumber: 0, lineNumber: 0}; } const rawLocation = new SDK.DebuggerModel.Location(debuggerModel, callframe.scriptId, callframe.lineNumber, callframe.columnNumber); const mappedLocation = await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().rawLocationToUILocation( rawLocation); const content = await mappedLocation?.uiSourceCode.requestContent(); const text = !content?.isEncoded && content?.content ? content.content : ''; const firstNewline = text.indexOf('\n'); if (text.length > MAX_CODE_SIZE && (firstNewline < 0 || firstNewline > MAX_CODE_SIZE)) { // Use formatter const {formattedContent, formattedMapping} = await Formatter.ScriptFormatter.formatScriptContent( mappedLocation?.uiSourceCode.mimeType() ?? 'text/javascript', text); const [lineNumber, columnNumber] = formattedMapping.originalToFormatted(mappedLocation?.lineNumber ?? 0, mappedLocation?.columnNumber ?? 0); return {text: formattedContent, columnNumber, lineNumber}; } return {text, columnNumber: mappedLocation?.columnNumber ?? 0, lineNumber: mappedLocation?.lineNumber ?? 0}; } async buildPrompt(sourcesTypes: SourceType[] = Object.values(SourceType)): Promise<{prompt: string, sources: Source[], isPageReloadRecommended: boolean}> { const [sourceCode, request] = await Promise.all([ sourcesTypes.includes(SourceType.RELATED_CODE) ? this.getMessageSourceCode() : undefined, sourcesTypes.includes(SourceType.NETWORK_REQUEST) ? this.getNetworkRequest() : undefined, ]); const relatedCode = sourceCode?.text ? formatRelatedCode(sourceCode) : ''; const relatedRequest = request ? formatNetworkRequest(request) : ''; const stacktrace = sourcesTypes.includes(SourceType.STACKTRACE) ? formatStackTrace(this.#consoleMessage) : ''; const message = formatConsoleMessage(this.#consoleMessage); const prompt = this.formatPrompt({ message: [message, stacktrace].join('\n').trim(), relatedCode, relatedRequest, }); const sources = [ { type: SourceType.MESSAGE, value: message, }, ]; if (stacktrace) { sources.push({ type: SourceType.STACKTRACE, value: stacktrace, }); } if (relatedCode) { sources.push({ type: SourceType.RELATED_CODE, value: relatedCode, }); } if (relatedRequest) { sources.push({ type: SourceType.NETWORK_REQUEST, value: relatedRequest, }); } return { prompt, sources, isPageReloadRecommended: sourcesTypes.includes(SourceType.NETWORK_REQUEST) && Boolean(this.#consoleMessage.consoleMessage().getAffectedResources()?.requestId) && !relatedRequest, }; } formatPrompt({message, relatedCode, relatedRequest}: {message: string, relatedCode: string, relatedRequest: string}): string { let prompt = `Please explain the following console error or warning: \`\`\` ${message} \`\`\``; if (relatedCode) { prompt += ` For the following code: \`\`\` ${relatedCode} \`\`\``; } if (relatedRequest) { prompt += ` For the following network request: \`\`\` ${relatedRequest} \`\`\``; } return prompt; } getSearchQuery(): string { let message = this.#consoleMessage.toMessageTextString(); if (message) { // If there are multiple lines, it is likely the rest of the message // is a stack trace, which we don't want. message = message.split('\n')[0]; } return message; } } export function allowHeader(header: SDK.NetworkRequest.NameValue): boolean { const normalizedName = header.name.toLowerCase().trim(); // Skip custom headers. if (normalizedName.startsWith('x-')) { return false; } // Skip cookies as they might contain auth. if (normalizedName === 'cookie' || normalizedName === 'set-cookie') { return false; } if (normalizedName === 'authorization') { return false; } return true; } export function lineWhitespace(line: string): string|null { const matches = /^\s*/.exec(line); if (!matches?.length) { // This should not happen return null; } const whitespace = matches[0]; if (whitespace === line) { return null; } return whitespace; } export function formatRelatedCode( {text, columnNumber, lineNumber}: {text: string, columnNumber: number, lineNumber: number}, maxCodeSize = MAX_CODE_SIZE): string { const lines = text.split('\n'); if (lines[lineNumber].length >= maxCodeSize / 2) { const start = Math.max(columnNumber - maxCodeSize / 2, 0); const end = Math.min(columnNumber + maxCodeSize / 2, lines[lineNumber].length); return lines[lineNumber].substring(start, end); } let relatedCodeSize = 0; let currentLineNumber = lineNumber; let currentWhitespace = lineWhitespace(lines[lineNumber]); const startByPrefix = new Map<string, number>(); while (lines[currentLineNumber] !== undefined && (relatedCodeSize + lines[currentLineNumber].length <= maxCodeSize / 2)) { const whitespace = lineWhitespace(lines[currentLineNumber]); if (whitespace !== null && currentWhitespace !== null && (whitespace === currentWhitespace || !whitespace.startsWith(currentWhitespace))) { // Don't start on a line that begins with a closing character if (!/^\s*[\}\)\]]/.exec(lines[currentLineNumber])) { // Update map of where code should start based on its indentation startByPrefix.set(whitespace, currentLineNumber); } currentWhitespace = whitespace; } relatedCodeSize += lines[currentLineNumber].length + 1; currentLineNumber--; } currentLineNumber = lineNumber + 1; let startLine = lineNumber; let endLine = lineNumber; currentWhitespace = lineWhitespace(lines[lineNumber]); while (lines[currentLineNumber] !== undefined && (relatedCodeSize + lines[currentLineNumber].length <= maxCodeSize)) { relatedCodeSize += lines[currentLineNumber].length; const whitespace = lineWhitespace(lines[currentLineNumber]); if (whitespace !== null && currentWhitespace !== null && (whitespace === currentWhitespace || !whitespace.startsWith(currentWhitespace))) { // We shouldn't end on a line if it is followed by an indented line const nextLine = lines[currentLineNumber + 1]; const nextWhitespace = nextLine ? lineWhitespace(nextLine) : null; if (!nextWhitespace || nextWhitespace === whitespace || !nextWhitespace.startsWith(whitespace)) { // Look up where code should start based on its indentation if (startByPrefix.has(whitespace)) { startLine = startByPrefix.get(whitespace) ?? 0; endLine = currentLineNumber; } } currentWhitespace = whitespace; } currentLineNumber++; } return lines.slice(startLine, endLine + 1).join('\n'); } function formatLines(title: string, lines: string[], maxLength: number): string { let result = ''; for (const line of lines) { if (result.length + line.length > maxLength) { break; } result += line; } result = result.trim(); return result && title ? title + '\n' + result : result; } export function formatNetworkRequest( request: Pick<SDK.NetworkRequest.NetworkRequest, 'url'|'requestHeaders'|'responseHeaders'|'statusCode'|'statusText'>): string { return `Request: ${request.url()} ${AiAssistanceModel.NetworkRequestFormatter.formatHeaders('Request headers:', request.requestHeaders())} ${AiAssistanceModel.NetworkRequestFormatter.formatHeaders('Response headers:', request.responseHeaders)} Response status: ${request.statusCode} ${request.statusText}`; } export function formatConsoleMessage(message: Console.ConsoleViewMessage.ConsoleViewMessage): string { return message.toMessageTextString().substr(0, MAX_MESSAGE_SIZE); } /** * This formats the stacktrace from the console message which might or might not * match the content of stacktrace(s) in the console message arguments. */ export function formatStackTrace(message: Console.ConsoleViewMessage.ConsoleViewMessage): string { const previewContainer = message.contentElement().querySelector('.stack-preview-container'); if (!previewContainer) { return ''; } const preview = previewContainer.shadowRoot?.querySelector('.stack-preview-container') as HTMLElement; const nodes = preview.childTextNodes(); // Gets application-level source mapped stack trace taking the ignore list // into account. const messageContent = nodes .filter(n => { return !n.parentElement?.closest('.show-all-link,.show-less-link,.hidden-row'); }) .map(Components.Linkifier.Linkifier.untruncatedNodeText); return formatLines('', messageContent, MAX_STACK_TRACE_SIZE); }