fix(scenario-logs): fix out-of-order run log persistence
- Add seq column assigned in call order to fix logs racing each other as fire-and-forget writes (e.g. a snippet's "started" entry could be saved after its "finished" entry) - Log snippet start/finish/failure via context.runSnippet, matching existing step start/pass/fail logging - Rework Markdown log export to per-entry sections instead of a table - Version bump: 1.10.0 -> 1.10.1
This commit is contained in:
@@ -199,6 +199,7 @@ export class CodeExecutorService {
|
||||
if (snippetCode == null) {
|
||||
throw new Error(`Snippet alias "${alias}" not found`);
|
||||
}
|
||||
scriptLog("log", `Snippet "${alias}" started`);
|
||||
const snippetFn = new Function(
|
||||
"context",
|
||||
"console",
|
||||
@@ -206,7 +207,22 @@ export class CodeExecutorService {
|
||||
"expect",
|
||||
`return (async (context, ...args) => { ${snippetCode} })(context, ...snippetArgs)`,
|
||||
);
|
||||
return snippetFn(scriptContext, fakeConsole, args, playwrightExpect);
|
||||
try {
|
||||
const snippetResult = await snippetFn(
|
||||
scriptContext,
|
||||
fakeConsole,
|
||||
args,
|
||||
playwrightExpect,
|
||||
);
|
||||
scriptLog("log", `Snippet "${alias}" finished`);
|
||||
return snippetResult;
|
||||
} catch (err) {
|
||||
scriptLog(
|
||||
"error",
|
||||
`Snippet "${alias}" failed: ${(err as Error).message}`,
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
getScenarioFiles: async (opts?: { limit?: number; offset?: number }) => {
|
||||
if (!fileService || !scenarioId) {
|
||||
|
||||
@@ -39,6 +39,11 @@ export class ScenarioRunLogEntity {
|
||||
@Column({ type: "text" })
|
||||
message: string;
|
||||
|
||||
// Assigned in call order per run, since concurrent unawaited writes can
|
||||
// otherwise persist out of order (createdAt alone isn't a reliable tiebreaker).
|
||||
@Column({ type: "int", default: 0 })
|
||||
seq: number;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ export class ScenarioSchedulerService implements OnModuleInit {
|
||||
private readonly runScenarioTimeouts = new Map<string, number | null>();
|
||||
// Cache scenarioId per run
|
||||
private readonly runScenarioIds = new Map<string, string>();
|
||||
// Monotonic log sequence per run, since persistLog writes are unawaited and
|
||||
// can otherwise resolve out of call order
|
||||
private readonly runLogSeq = new Map<string, number>();
|
||||
|
||||
private static readonly DEFAULT_SCENARIO_TIMEOUT_SEC = 600;
|
||||
private static readonly DEFAULT_STEP_TIMEOUT_SEC = 60;
|
||||
@@ -138,8 +141,10 @@ export class ScenarioSchedulerService implements OnModuleInit {
|
||||
level: "log" | "warn" | "error",
|
||||
message: string,
|
||||
): void {
|
||||
const seq = (this.runLogSeq.get(runId) ?? 0) + 1;
|
||||
this.runLogSeq.set(runId, seq);
|
||||
void this.runLogRepo.save(
|
||||
this.runLogRepo.create({ runId, stepRunId, level, message }),
|
||||
this.runLogRepo.create({ runId, stepRunId, level, message, seq }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -216,6 +221,7 @@ export class ScenarioSchedulerService implements OnModuleInit {
|
||||
this.runEnvironments.delete(run.id);
|
||||
this.runScenarioTimeouts.delete(run.id);
|
||||
this.runScenarioIds.delete(run.id);
|
||||
this.runLogSeq.delete(run.id);
|
||||
this.logger.error(
|
||||
`Run #${run.id}: setup failed — ${String(err)}`,
|
||||
);
|
||||
@@ -279,6 +285,7 @@ export class ScenarioSchedulerService implements OnModuleInit {
|
||||
this.runEnvironments.delete(runId);
|
||||
this.runScenarioTimeouts.delete(runId);
|
||||
this.runScenarioIds.delete(runId);
|
||||
this.runLogSeq.delete(runId);
|
||||
await this.maybePreserveSession(runId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,7 +402,7 @@ export class ScenarioService {
|
||||
);
|
||||
const logs = await this.runLogRepo.find({
|
||||
where: q?.trim() ? { runId, message: Like(`%${q.trim()}%`) } : { runId },
|
||||
order: { createdAt: "ASC" },
|
||||
order: { seq: "ASC", createdAt: "ASC" },
|
||||
});
|
||||
return Object.assign(run, { logs });
|
||||
}
|
||||
@@ -422,7 +422,7 @@ export class ScenarioService {
|
||||
);
|
||||
const logs = await this.runLogRepo.find({
|
||||
where: { runId },
|
||||
order: { createdAt: "ASC" },
|
||||
order: { seq: "ASC", createdAt: "ASC" },
|
||||
});
|
||||
|
||||
if (format === "csv") {
|
||||
@@ -439,17 +439,10 @@ export class ScenarioService {
|
||||
return rows.join("\n");
|
||||
}
|
||||
|
||||
const lines = [
|
||||
`# Run ${runId} logs`,
|
||||
"",
|
||||
"| Timestamp | Level | Message |",
|
||||
"| --- | --- | --- |",
|
||||
...logs.map(
|
||||
(l) =>
|
||||
`| ${l.createdAt.toISOString()} | ${l.level} | ${l.message.replace(/\|/g, "\\|").replace(/\n/g, "<br>")} |`,
|
||||
),
|
||||
];
|
||||
return lines.join("\n");
|
||||
const sections = logs.map(
|
||||
(l) => `## [${l.level}] ${l.createdAt.toISOString()}\n\n${l.message}`,
|
||||
);
|
||||
return [`# Run ${runId} logs`, ...sections].join("\n\n");
|
||||
}
|
||||
|
||||
async waitForRun(
|
||||
|
||||
Reference in New Issue
Block a user