diff --git a/src/code-executor/code-executor.service.ts b/src/code-executor/code-executor.service.ts index c0f42a2..3c4e1e4 100644 --- a/src/code-executor/code-executor.service.ts +++ b/src/code-executor/code-executor.service.ts @@ -8,6 +8,8 @@ export interface ExecResult { result: unknown; } +export type ScriptLogger = (level: 'log' | 'warn' | 'error', message: string) => void; + @Injectable() export class CodeExecutorService { private readonly logger = new TraceLogger(CodeExecutorService.name); @@ -29,13 +31,39 @@ export class CodeExecutorService { * Executes `code` as an async function body with `page` and `context` in * scope. Always call `validate()` before this method. */ - async execute(page: Page, context: BrowserContext, code: string): Promise { + async execute(page: Page, context: BrowserContext, code: string, log?: ScriptLogger): Promise { + const scriptLog: ScriptLogger = log ?? ((level, msg) => this.logger[level](msg)); + const toStr = (args: unknown[]) => args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' '); + try { - const pageHelpers = { dumpDom: (selector?: string) => dumpDom(page, selector) }; + const pageHelpers = { + dumpDom: (selector?: string) => dumpDom(page, selector), + log: (...args: unknown[]) => scriptLog('log', toStr(args)), + warn: (...args: unknown[]) => scriptLog('warn', toStr(args)), + error: (...args: unknown[]) => scriptLog('error', toStr(args)), + }; + + const prevLog = console.log; + const prevWarn = console.warn; + const prevError = console.error; + const prevInfo = console.info; + console.log = (...args: unknown[]) => scriptLog('log', toStr(args)); + console.warn = (...args: unknown[]) => scriptLog('warn', toStr(args)); + console.error = (...args: unknown[]) => scriptLog('error', toStr(args)); + console.info = (...args: unknown[]) => scriptLog('log', toStr(args)); + // eslint-disable-next-line no-new-func const fn = new Function('page', 'context', 'helpers', `return (async (page, context, helpers) => { ${code} })(page, context, helpers)`); this.logger.debug('Executing user code'); - const result = await fn(page, context, pageHelpers); + let result: unknown; + try { + result = await fn(page, context, pageHelpers); + } finally { + console.log = prevLog; + console.warn = prevWarn; + console.error = prevError; + console.info = prevInfo; + } return { result }; } catch (err) { throw new InternalServerErrorException(`Code execution failed: ${(err as Error).message}`, { cause: err }); diff --git a/src/interceptors/logging.interceptor.ts b/src/interceptors/logging.interceptor.ts index bba2bc8..13d2529 100644 --- a/src/interceptors/logging.interceptor.ts +++ b/src/interceptors/logging.interceptor.ts @@ -25,8 +25,9 @@ export class LoggingInterceptor implements NestInterceptor { return next.handle().pipe( tap((responseBody) => { const ms = Date.now() - start; - const resStr = responseBody != null ? ` ${JSON.stringify(responseBody)}` : ''; - this.logger.debug(`← ${method} ${url} ${res.statusCode} (${ms}ms)${resStr}`); + const len = responseBody != null ? JSON.stringify(responseBody).length : 0; + const lenStr = len > 0 ? ` [${len}b]` : ''; + this.logger.debug(`← ${method} ${url} ${res.statusCode} (${ms}ms)${lenStr}`); }), ); } diff --git a/src/scenario/scenario-scheduler.service.ts b/src/scenario/scenario-scheduler.service.ts index 7372c41..a0d0890 100644 --- a/src/scenario/scenario-scheduler.service.ts +++ b/src/scenario/scenario-scheduler.service.ts @@ -8,6 +8,7 @@ import type { Browser, BrowserContext, Page } from 'playwright'; import { ScenarioRunEntity } from './scenario-run.entity'; import { ScenarioRunStepEntity } from './scenario-run-step.entity'; import { ScenarioStepEntity } from './scenario-step.entity'; +import type { ScriptLogger } from '../code-executor/code-executor.service'; import { AuthService } from '../auth/auth.service'; import { CodeExecutorService } from '../code-executor/code-executor.service'; import { SessionService } from '../session/session.service'; @@ -39,6 +40,10 @@ export class ScenarioSchedulerService { private readonly sessionService: SessionService, ) {} + private stepLogger(stepRunId: number): ScriptLogger { + return (level, msg) => this.logger[level](`StepRun #${stepRunId} script: ${msg}`); + } + // ── Job 1: flip pending runs → in_progress ───────────────────────────────── @Interval(1000) @@ -157,7 +162,7 @@ export class ScenarioSchedulerService { if (step.validateCode) { const { page, context } = await this.getOrCreateBrowserHandle(stepRun.runId, step.sessionName); this.codeExecutor.validate(step.validateCode); - const { result } = await this.codeExecutor.execute(page, context, step.validateCode); + const { result } = await this.codeExecutor.execute(page, context, step.validateCode, this.stepLogger(stepRun.id)); const vr = this.parseValidateResult(result); if (!vr.success) throw new Error(vr.description ?? 'Validation failed'); } @@ -172,12 +177,12 @@ export class ScenarioSchedulerService { this.codeExecutor.validate(step.execCode); const { page, context } = await this.getOrCreateBrowserHandle(stepRun.runId, step.sessionName); - await this.codeExecutor.execute(page, context, step.execCode); + await this.codeExecutor.execute(page, context, step.execCode, this.stepLogger(stepRun.id)); this.logger.log(`StepRun #${stepRun.id}: exec OK`); if (step.validateCode) { this.codeExecutor.validate(step.validateCode); - const { result } = await this.codeExecutor.execute(page, context, step.validateCode); + const { result } = await this.codeExecutor.execute(page, context, step.validateCode, this.stepLogger(stepRun.id)); const vr = this.parseValidateResult(result); if (!vr.success) throw new Error(vr.description ?? 'Validation failed'); } @@ -203,7 +208,7 @@ export class ScenarioSchedulerService { if (step.validateCode) { this.codeExecutor.validate(step.validateCode); - const { result } = await this.codeExecutor.execute(page, context, step.validateCode); + const { result } = await this.codeExecutor.execute(page, context, step.validateCode, this.stepLogger(stepRun.id)); const vr = this.parseValidateResult(result); if (!vr.success) throw new Error(vr.description ?? 'Validation failed'); }