diff --git a/cypress/e2e/laboratory-preflight-script.cy.ts b/cypress/e2e/laboratory-preflight-script.cy.ts index df14cae82..6529499c0 100644 --- a/cypress/e2e/laboratory-preflight-script.cy.ts +++ b/cypress/e2e/laboratory-preflight-script.cy.ts @@ -76,7 +76,7 @@ describe('Preflight Script Modal', () => { it('logs show console/error information', () => { setEditorScript(script); cy.dataCy('run-preflight-script').click(); - cy.dataCy('console-output').should('contain', 'Log: Hello_world (Line: 1, Column: 1)'); + cy.dataCy('console-output').should('contain', 'log: Hello_world (1:1)'); setEditorScript( `console.info(1) @@ -87,16 +87,11 @@ throw new TypeError('Test')`, cy.dataCy('run-preflight-script').click(); // First log previous log message - cy.dataCy('console-output').should('contain', 'Log: Hello_world (Line: 1, Column: 1)'); + cy.dataCy('console-output').should('contain', 'log: Hello_world (1:1)'); // After the new logs cy.dataCy('console-output').should( 'contain', - [ - 'Info: 1 (Line: 1, Column: 1)', - 'Warn: true (Line: 2, Column: 1)', - 'Error: Fatal (Line: 3, Column: 1)', - 'TypeError: Test (Line: 4, Column: 7)', - ].join(''), + ['info: 1 (1:1)', 'warn: true (2:1)', 'error: Fatal (3:1)', 'error: Test (4:7)'].join(''), ); }); @@ -104,7 +99,7 @@ throw new TypeError('Test')`, setEditorScript(script); cy.dataCy('run-preflight-script').click(); - cy.dataCy('console-output').should('contain', 'Log: Hello_world (Line: 1, Column: 1)'); + cy.dataCy('console-output').should('contain', 'log: Hello_world (1:1)'); setEditorScript( dedent` @@ -118,12 +113,12 @@ throw new TypeError('Test')`, cy.dataCy('prompt').get('form').submit(); // First log previous log message - cy.dataCy('console-output').should('contain', 'Log: Hello_world (Line: 1, Column: 1)'); + cy.dataCy('console-output').should('contain', 'log: Hello_world (1:1)'); // After the new logs cy.dataCy('console-output').should( 'contain', dedent` - Info: test-username (Line: 2, Column: 1) + info: test-username (2:1) `, ); }); @@ -132,7 +127,7 @@ throw new TypeError('Test')`, setEditorScript(script); cy.dataCy('run-preflight-script').click(); - cy.dataCy('console-output').should('contain', 'Log: Hello_world (Line: 1, Column: 1)'); + cy.dataCy('console-output').should('contain', 'log: Hello_world (1:1)'); setEditorScript( dedent` @@ -146,12 +141,12 @@ throw new TypeError('Test')`, cy.dataCy('prompt').get('[data-cy="prompt-cancel"]').click(); // First log previous log message - cy.dataCy('console-output').should('contain', 'Log: Hello_world (Line: 1, Column: 1)'); + cy.dataCy('console-output').should('contain', 'log: Hello_world (1:1)'); // After the new logs cy.dataCy('console-output').should( 'contain', dedent` - Info: null (Line: 2, Column: 1) + info: null (2:1) `, ); }); @@ -170,10 +165,10 @@ throw new TypeError('Test')`, it('`crypto-js` can be used for generating hashes', () => { setEditorScript('console.log(lab.CryptoJS.SHA256("🐝"))'); cy.dataCy('run-preflight-script').click(); - cy.dataCy('console-output').should('contain', 'Info: Using crypto-js version:'); + cy.dataCy('console-output').should('contain', 'info: Using crypto-js version:'); cy.dataCy('console-output').should( 'contain', - 'Log: d5b51e79e4be0c4f4d6b9a14e16ca864de96afe68459e60a794e80393a4809e8', + 'log: d5b51e79e4be0c4f4d6b9a14e16ca864de96afe68459e60a794e80393a4809e8', ); }); @@ -339,12 +334,12 @@ describe('Execution', () => { cy.get('#preflight-script-logs [data-cy="logs"]').should( 'contain', [ - '> start running script: > Start running script', - 'info: 1 (Line: 1, Column: 1)', - 'warn: true (Line: 2, Column: 1)', - 'error: Fatal (Line: 3, Column: 1)', - 'error: TypeError: Test (Line: 4, Column: 7)', - '> preflight script failed: > Preflight script failed', + 'log: Running script...', + 'info: 1 (1:1)', + 'warn: true (2:1)', + 'error: Fatal (3:1)', + 'error: Test (4:7)', + 'log: Script failed', ].join(''), ); }); @@ -375,15 +370,16 @@ describe('Execution', () => { // it's because the button is not fully visible on the screen force: true, }); + cy.get('#preflight-script-logs [data-cy="logs"]').should( 'contain', [ - '> start running script: > Start running script', - 'info: 1 (Line: 1, Column: 1)', - 'warn: true (Line: 2, Column: 1)', - 'error: Fatal (Line: 3, Column: 1)', - 'error: TypeError: Test (Line: 4, Column: 7)', - '> preflight script failed: > Preflight script failed', + 'log: Running script...', + 'info: 1 (1:1)', + 'warn: true (2:1)', + 'error: Fatal (3:1)', + 'error: Test (4:7)', + 'log: Script failed', ].join(''), ); diff --git a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx index 4d7dcdefe..4f9ac0ce4 100644 --- a/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx +++ b/packages/web/app/src/lib/preflight-sandbox/graphiql-plugin.tsx @@ -29,19 +29,12 @@ import { FragmentType, graphql, useFragment } from '@/gql'; import { useLocalStorage, useToggle } from '@/lib/hooks'; import { GraphiQLPlugin } from '@graphiql/react'; import { Editor as MonacoEditor, OnMount, type Monaco } from '@monaco-editor/react'; -import { - Cross2Icon, - CrossCircledIcon, - ExclamationTriangleIcon, - InfoCircledIcon, - Pencil1Icon, - TriangleRightIcon, -} from '@radix-ui/react-icons'; +import { Cross2Icon, InfoCircledIcon, Pencil1Icon, TriangleRightIcon } from '@radix-ui/react-icons'; +import { captureException } from '@sentry/react'; import { useParams } from '@tanstack/react-router'; import { cn } from '../utils'; import labApiDefinitionRaw from './lab-api-declaration?raw'; -import type { LogMessage } from './preflight-script-worker'; -import { IFrameEvents } from './shared-types'; +import { IFrameEvents, LogMessage } from './shared-types'; export const preflightScriptPlugin: GraphiQLPlugin = { icon: () => ( @@ -188,7 +181,13 @@ export function usePreflightScript(args: { const id = crypto.randomUUID(); setState(PreflightWorkerState.running); const now = Date.now(); - setLogs(prev => [...prev, '> Start running script']); + setLogs(prev => [ + ...prev, + { + level: 'log', + message: 'Running script...', + }, + ]); try { const contentWindow = iframeRef.current?.contentWindow; @@ -270,7 +269,10 @@ export function usePreflightScript(args: { latestEnvironmentVariablesRef.current = mergedEnvironmentVariables; setLogs(logs => [ ...logs, - `> End running script. Done in ${(Date.now() - now) / 1000}s`, + { + level: 'log', + message: `Done in ${(Date.now() - now) / 1000}s`, + }, { type: 'separator' as const, }, @@ -283,8 +285,16 @@ export function usePreflightScript(args: { const error = ev.data.error; setLogs(logs => [ ...logs, - error, - '> Preflight script failed', + { + level: 'error', + message: error.message, + line: error.line, + column: error.column, + }, + { + level: 'log', + message: 'Script failed', + }, { type: 'separator' as const, }, @@ -323,8 +333,14 @@ export function usePreflightScript(args: { if (err instanceof Error) { setLogs(prev => [ ...prev, - err, - '> Preflight script failed', + { + level: 'error', + message: err.message, + }, + { + level: 'log', + message: 'Script failed', + }, { type: 'separator' as const, }, @@ -687,43 +703,12 @@ function PreflightScriptModal({
- {logs.map((log, index) => { - let type = ''; - if (log instanceof Error) { - type = 'error'; - log = `${log.name}: ${log.message}`; - } - if (typeof log === 'string') { - type ||= log.split(':')[0].toLowerCase(); - - const ComponentToUse = { - error: CrossCircledIcon, - warn: ExclamationTriangleIcon, - info: InfoCircledIcon, - }[type]; - - return ( -
- {ComponentToUse && } - {log} -
- ); - } - return
; - })} + {logs.map((log, index) => ( + + ))}
Environment Variables @@ -768,3 +753,30 @@ function PreflightScriptModal({ ); } + +const LOG_COLORS = { + error: 'text-red-400', + info: 'text-emerald-400', + warn: 'text-yellow-400', + log: 'text-gray-400', +}; + +export function LogLine({ log }: { log: LogRecord }) { + if ('type' in log && log.type === 'separator') { + return
; + } + + if ('level' in log && log.level in LOG_COLORS) { + return ( +
+ {log.level}: {log.message} + {log.line && log.column ? ` (${log.line}:${log.column})` : ''} +
+ ); + } + + captureException(new Error('Unexpected log type in Preflight Script output'), { + extra: { log }, + }); + return null; +} diff --git a/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts b/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts index 95b358f33..63c441655 100644 --- a/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts +++ b/packages/web/app/src/lib/preflight-sandbox/preflight-script-worker.ts @@ -2,9 +2,7 @@ import CryptoJS from 'crypto-js'; import CryptoJSPackageJson from 'crypto-js/package.json'; import { ALLOWED_GLOBALS } from './allowed-globals'; import { isJSONPrimitive } from './json'; -import { WorkerEvents } from './shared-types'; - -export type LogMessage = string | Error; +import { LogMessage, WorkerEvents } from './shared-types'; /** * Unique id for each prompt request. @@ -79,13 +77,22 @@ async function execute(args: WorkerEvents.Incoming.EventData): Promise { (level: 'log' | 'warn' | 'error' | 'info') => (...args: unknown[]) => { console[level](...args); - let message = `${level.charAt(0).toUpperCase()}${level.slice(1)}: ${args.map(String).join(' ')}`; - message += appendLineAndColumn(new Error(), { + const message = args.map(String).join(' '); + const { line, column } = readLineAndColumn(new Error(), { columnOffset: 'console.'.length, }); // The messages should be streamed to the main thread as they occur not gathered and send to // the main thread at the end of the execution of the preflight script - postMessage({ type: 'log', message }); + // const message: LogMessage = { level, message }; + postMessage({ + type: 'log', + message: { + level, + message, + line, + column, + } satisfies LogMessage, + }); }; function getValidEnvVariable(value: unknown) { @@ -161,10 +168,15 @@ ${script}})()`; 'undefined', )(labApi, consoleApi); } catch (error) { - if (error instanceof Error) { - error.message += appendLineAndColumn(error); - } - sendMessage({ type: WorkerEvents.Outgoing.Event.error, error: error as Error }); + const { line, column } = error instanceof Error ? readLineAndColumn(error) : {}; + sendMessage({ + type: WorkerEvents.Outgoing.Event.error, + error: { + message: error instanceof Error ? error.message : String(error), + line, + column, + }, + }); return; } sendMessage({ @@ -173,11 +185,14 @@ ${script}})()`; }); } -function appendLineAndColumn(error: Error, { columnOffset = 0 } = {}): string { +function readLineAndColumn(error: Error, { columnOffset = 0 } = {}) { const regex = /:(?\d+):(?\d+)/; // Regex to match the line and column numbers const { line, column } = error.stack?.match(regex)?.groups || {}; - return ` (Line: ${Number(line) - 3}, Column: ${Number(column) - columnOffset})`; + return { + line: Number(line) - 3, + column: Number(column) - columnOffset, + }; } sendMessage({ type: WorkerEvents.Outgoing.Event.ready }); diff --git a/packages/web/app/src/lib/preflight-sandbox/preflight-worker-embed.ts b/packages/web/app/src/lib/preflight-sandbox/preflight-worker-embed.ts index b1aa78664..538e58100 100644 --- a/packages/web/app/src/lib/preflight-sandbox/preflight-worker-embed.ts +++ b/packages/web/app/src/lib/preflight-sandbox/preflight-worker-embed.ts @@ -71,9 +71,9 @@ function handleEvent(data: IFrameEvents.Incoming.EventData) { postMessage({ type: IFrameEvents.Outgoing.Event.error, runId, - error: new Error( - `Preflight script execution timed out after ${PREFLIGHT_TIMEOUT / 1000} seconds`, - ), + error: { + message: `Preflight script execution timed out after ${PREFLIGHT_TIMEOUT / 1000} seconds`, + }, }); terminate(); }, PREFLIGHT_TIMEOUT); @@ -141,7 +141,9 @@ function handleEvent(data: IFrameEvents.Incoming.EventData) { postMessage({ type: IFrameEvents.Outgoing.Event.error, runId, - error: error as Error, + error: { + message: error instanceof Error ? error.message : String(error), + }, }); terminate(); } diff --git a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts index b96386e4b..fcbf75cff 100644 --- a/packages/web/app/src/lib/preflight-sandbox/shared-types.ts +++ b/packages/web/app/src/lib/preflight-sandbox/shared-types.ts @@ -2,6 +2,19 @@ type _MessageEvent = MessageEvent; +export type LogMessage = { + level: 'log' | 'warn' | 'error' | 'info'; + message: string; + line?: number; + column?: number; +}; + +export type ErrorMessage = { + message: string; + line?: number; + column?: number; +}; + export namespace IFrameEvents { export namespace Outgoing { export const enum Event { @@ -25,7 +38,7 @@ export namespace IFrameEvents { type LogEventData = { type: Event.log; runId: string; - log: string | Error; + log: LogMessage; }; type ResultEventData = { @@ -37,7 +50,7 @@ export namespace IFrameEvents { type ErrorEventData = { type: Event.error; runId: string; - error: Error; + error: ErrorMessage; }; type PromptEventData = { @@ -100,8 +113,8 @@ export namespace WorkerEvents { prompt = 'prompt', } - type LogEventData = { type: Event.log; message: string }; - type ErrorEventData = { type: Event.error; error: Error }; + type LogEventData = { type: Event.log; message: LogMessage }; + type ErrorEventData = { type: Event.error; error: ErrorMessage }; type PromptEventData = { type: Event.prompt; promptId: number; diff --git a/packages/web/app/src/pages/target-laboratory.tsx b/packages/web/app/src/pages/target-laboratory.tsx index 4278c9d23..5ccdf5f64 100644 --- a/packages/web/app/src/pages/target-laboratory.tsx +++ b/packages/web/app/src/pages/target-laboratory.tsx @@ -36,6 +36,7 @@ import { useSyncOperationState } from '@/lib/hooks/laboratory/use-sync-operation import { useOperationFromQueryString } from '@/lib/hooks/laboratory/useOperationFromQueryString'; import { useResetState } from '@/lib/hooks/use-reset-state'; import { + LogLine, LogRecord, preflightScriptPlugin, PreflightScriptProvider, @@ -57,7 +58,6 @@ import 'graphiql/style.css'; import '@graphiql/plugin-explorer/style.css'; import { PromptManager, PromptProvider } from '@/components/ui/prompt'; import { useRedirect } from '@/lib/access/common'; -import { captureException } from '@sentry/react'; const explorer = explorerPlugin(); @@ -702,13 +702,6 @@ function PreflightScriptLogs(props: { logs: LogRecord[]; onClear: () => void }) consoleEl?.scroll({ top: consoleEl.scrollHeight, behavior: 'smooth' }); }, [props.logs, isOpen]); - const logColor = { - error: 'text-red-400', - info: 'text-emerald-400', - warn: 'text-yellow-400', - log: '', // default - }; - return ( void }) ) : ( <> - {props.logs.map((log, index) => { - if (typeof log !== 'string' && 'type' in log && log.type === 'separator') { - return
; - } - - let logType: 'error' | 'warn' | 'info' | 'log' = 'log'; - let logMessage = ''; - - if (log instanceof Error) { - logType = 'error'; - logMessage = `${log.name}: ${log.message}`; - } else if (typeof log === 'string') { - logType = log.split(':')[0].toLowerCase() as 'error' | 'warn' | 'info' | 'log'; - logMessage = log.substring(log.indexOf(':') + 1).trim(); - } else { - captureException(new Error('Unexpected log type in Preflight Script Logs'), { - extra: { log }, - }); - return null; - } - - return ( -
- {logType}: {logMessage} -
- ); - })} + {props.logs.map((log, index) => ( + + ))} )}