mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
Use structured logs instead of strings in Preflight Script (#6404)
This commit is contained in:
parent
cade7b34cb
commit
403a95e6f7
6 changed files with 141 additions and 134 deletions
|
|
@ -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(''),
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
|||
</div>
|
||||
<section
|
||||
ref={consoleRef}
|
||||
className='h-1/2 overflow-hidden overflow-y-scroll bg-[#10151f] py-2.5 pl-[26px] pr-2.5 font-[Menlo,Monaco,"Courier_New",monospace] text-xs/[18px]'
|
||||
className="h-1/2 overflow-hidden overflow-y-scroll bg-[#10151f] py-2.5 pl-[26px] pr-2.5 font-mono text-xs/[18px]"
|
||||
data-cy="console-output"
|
||||
>
|
||||
{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 (
|
||||
<div
|
||||
key={index}
|
||||
className={clsx(
|
||||
'relative',
|
||||
{
|
||||
error: 'text-red-500',
|
||||
warn: 'text-yellow-500',
|
||||
info: 'text-green-500',
|
||||
}[type],
|
||||
)}
|
||||
>
|
||||
{ComponentToUse && <ComponentToUse className={classes.icon} />}
|
||||
{log}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return <hr key={index} className="my-2 border-dashed border-current" />;
|
||||
})}
|
||||
{logs.map((log, index) => (
|
||||
<LogLine key={index} log={log} />
|
||||
))}
|
||||
</section>
|
||||
<EditorTitle className="flex gap-2 p-2">
|
||||
Environment Variables
|
||||
|
|
@ -768,3 +753,30 @@ function PreflightScriptModal({
|
|||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
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 <hr className="my-2 border-dashed border-current" />;
|
||||
}
|
||||
|
||||
if ('level' in log && log.level in LOG_COLORS) {
|
||||
return (
|
||||
<div className={LOG_COLORS[log.level]}>
|
||||
{log.level}: {log.message}
|
||||
{log.line && log.column ? ` (${log.line}:${log.column})` : ''}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
captureException(new Error('Unexpected log type in Preflight Script output'), {
|
||||
extra: { log },
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
|||
(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 = /<anonymous>:(?<line>\d+):(?<column>\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 });
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,19 @@
|
|||
|
||||
type _MessageEvent<T> = MessageEvent<T>;
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Collapsible
|
||||
open={isOpen}
|
||||
|
|
@ -768,33 +761,9 @@ function PreflightScriptLogs(props: { logs: LogRecord[]; onClear: () => void })
|
|||
</div>
|
||||
) : (
|
||||
<>
|
||||
{props.logs.map((log, index) => {
|
||||
if (typeof log !== 'string' && 'type' in log && log.type === 'separator') {
|
||||
return <hr key={index} className="my-2 border-dashed border-current" />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div key={index} className={logColor[logType] ?? ''}>
|
||||
{logType}: {logMessage}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{props.logs.map((log, index) => (
|
||||
<LogLine key={index} log={log} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</CollapsibleContent>
|
||||
|
|
|
|||
Loading…
Reference in a new issue