donobu 5.18.0 → 5.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/lib/page/extendPage.js +25 -6
- package/dist/esm/lib/test/testExtension.d.ts +2 -0
- package/dist/esm/lib/test/testExtension.js +20 -5
- package/dist/esm/utils/Logger.d.ts +5 -3
- package/dist/esm/utils/Logger.js +11 -5
- package/dist/lib/page/extendPage.js +25 -6
- package/dist/lib/test/testExtension.d.ts +2 -0
- package/dist/lib/test/testExtension.js +20 -5
- package/dist/utils/Logger.d.ts +5 -3
- package/dist/utils/Logger.js +11 -5
- package/package.json +1 -1
|
@@ -31,6 +31,24 @@ const donobuTestStack_1 = require("../test/utils/donobuTestStack");
|
|
|
31
31
|
const originalGotoRegistry_1 = require("./originalGotoRegistry");
|
|
32
32
|
const SmartSelector_1 = require("./SmartSelector");
|
|
33
33
|
const tbd_1 = require("./tbd");
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a URL against the browser context's baseURL (if set), mirroring
|
|
36
|
+
* what Playwright does internally before issuing the navigation request.
|
|
37
|
+
* This gives us the *intended* URL (before any server-side redirects).
|
|
38
|
+
*/
|
|
39
|
+
function resolveBaseUrl(page, url) {
|
|
40
|
+
const baseURL = page.context()._options?.baseURL;
|
|
41
|
+
// Only resolve against baseURL if the URL is relative (no scheme like https:).
|
|
42
|
+
if (baseURL && !/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)) {
|
|
43
|
+
try {
|
|
44
|
+
return new URL(url, baseURL).href;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return url;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return url;
|
|
51
|
+
}
|
|
34
52
|
// Donobu page extension helpers: decorate Playwright pages with Donobu behaviors and keep one
|
|
35
53
|
// coherent flow (and persistence record) per browser context so new tabs share state safely.
|
|
36
54
|
const PLACEHOLDER_FLOW_URL = 'https://example.com';
|
|
@@ -340,13 +358,14 @@ Use this information to return an appropriate JSON object.`,
|
|
|
340
358
|
page.goto = async (url, options) => {
|
|
341
359
|
const startedAt = new Date().getTime();
|
|
342
360
|
const flowId = sharedState.donobuFlowMetadata.id;
|
|
361
|
+
const effectiveUrl = resolveBaseUrl(page, url);
|
|
343
362
|
// First navigation sets the target website and records the tool call with screenshots.
|
|
344
363
|
if (sharedState.donobuFlowMetadata.web?.targetWebsite === PLACEHOLDER_FLOW_URL) {
|
|
345
364
|
sharedState.donobuFlowMetadata = {
|
|
346
365
|
...sharedState.donobuFlowMetadata,
|
|
347
366
|
web: {
|
|
348
367
|
...sharedState.donobuFlowMetadata.web,
|
|
349
|
-
targetWebsite:
|
|
368
|
+
targetWebsite: effectiveUrl,
|
|
350
369
|
},
|
|
351
370
|
};
|
|
352
371
|
await sharedState.persistence.setFlowMetadata(sharedState.donobuFlowMetadata);
|
|
@@ -362,18 +381,18 @@ Use this information to return an appropriate JSON object.`,
|
|
|
362
381
|
id: MiscUtils_1.MiscUtils.createAdHocToolCallId(),
|
|
363
382
|
toolName: GoToWebpageTool_1.GoToWebpageTool.NAME,
|
|
364
383
|
parameters: {
|
|
365
|
-
url:
|
|
384
|
+
url: effectiveUrl,
|
|
366
385
|
},
|
|
367
386
|
outcome: {
|
|
368
387
|
isSuccessful: true,
|
|
369
|
-
forLlm: `Successfully navigated to ${
|
|
388
|
+
forLlm: `Successfully navigated to ${effectiveUrl}`,
|
|
370
389
|
metadata: {
|
|
371
390
|
pageTitle: pageTitle,
|
|
372
391
|
resolvedUrl: page.url(),
|
|
373
392
|
},
|
|
374
393
|
},
|
|
375
394
|
postCallImageId: postCallImageId,
|
|
376
|
-
page:
|
|
395
|
+
page: effectiveUrl,
|
|
377
396
|
startedAt: startedAt,
|
|
378
397
|
completedAt: completedAt,
|
|
379
398
|
});
|
|
@@ -390,7 +409,7 @@ Use this information to return an appropriate JSON object.`,
|
|
|
390
409
|
id: MiscUtils_1.MiscUtils.createAdHocToolCallId(),
|
|
391
410
|
toolName: GoToWebpageTool_1.GoToWebpageTool.NAME,
|
|
392
411
|
parameters: {
|
|
393
|
-
url:
|
|
412
|
+
url: effectiveUrl,
|
|
394
413
|
},
|
|
395
414
|
outcome: {
|
|
396
415
|
isSuccessful: false,
|
|
@@ -398,7 +417,7 @@ Use this information to return an appropriate JSON object.`,
|
|
|
398
417
|
metadata: null,
|
|
399
418
|
},
|
|
400
419
|
postCallImageId: postCallImageId,
|
|
401
|
-
page:
|
|
420
|
+
page: effectiveUrl,
|
|
402
421
|
startedAt: startedAt,
|
|
403
422
|
completedAt: completedAt,
|
|
404
423
|
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { GptClient } from '../../clients/GptClient';
|
|
2
2
|
import type { BrowserStorageState } from '../../models/BrowserStorageState';
|
|
3
|
+
import { FlowLogBuffer } from '../../utils/FlowLogBuffer';
|
|
3
4
|
import type { DonobuExtendedPage } from '../page/DonobuExtendedPage';
|
|
4
5
|
export * from 'playwright/test';
|
|
5
6
|
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
|
|
6
7
|
flowLoggingContext: {
|
|
7
8
|
flowId: string;
|
|
9
|
+
logBuffer: FlowLogBuffer;
|
|
8
10
|
};
|
|
9
11
|
storageState?: BrowserStorageState | Promise<BrowserStorageState>;
|
|
10
12
|
gptClient?: GptClient;
|
|
@@ -22,6 +22,7 @@ const v4_1 = require("zod/v4");
|
|
|
22
22
|
const envVars_1 = require("../../envVars");
|
|
23
23
|
const DonobuFlowsManager_1 = require("../../managers/DonobuFlowsManager");
|
|
24
24
|
const BrowserUtils_1 = require("../../utils/BrowserUtils");
|
|
25
|
+
const FlowLogBuffer_1 = require("../../utils/FlowLogBuffer");
|
|
25
26
|
const Logger_1 = require("../../utils/Logger");
|
|
26
27
|
const MiscUtils_1 = require("../../utils/MiscUtils");
|
|
27
28
|
const cacheLocator_1 = require("../ai/cache/cacheLocator");
|
|
@@ -55,16 +56,19 @@ exports.test = test_1.test.extend({
|
|
|
55
56
|
// Donobu flow ID, so metadata would overwrite and concurrent executions could
|
|
56
57
|
// clobber each other.
|
|
57
58
|
const flowId = (0, crypto_1.randomUUID)();
|
|
58
|
-
const
|
|
59
|
+
const logBuffer = new FlowLogBuffer_1.FlowLogBuffer();
|
|
60
|
+
const flowContext = { flowId, logBuffer };
|
|
59
61
|
const asyncScope = new async_hooks_1.AsyncResource('DonobuFlowContext');
|
|
60
62
|
await Logger_1.loggingContext.run(flowContext, async () => {
|
|
61
63
|
Logger_1.loggingContext.enterWith(flowContext);
|
|
62
64
|
(0, Logger_1.setProcessLocalFlowId)(flowId);
|
|
65
|
+
(0, Logger_1.setProcessLocalLogBuffer)(logBuffer);
|
|
63
66
|
try {
|
|
64
|
-
await asyncScope.runInAsyncScope(() => use({ flowId }));
|
|
67
|
+
await asyncScope.runInAsyncScope(() => use({ flowId, logBuffer }));
|
|
65
68
|
}
|
|
66
69
|
finally {
|
|
67
70
|
(0, Logger_1.setProcessLocalFlowId)(null);
|
|
71
|
+
(0, Logger_1.setProcessLocalLogBuffer)(null);
|
|
68
72
|
}
|
|
69
73
|
});
|
|
70
74
|
},
|
|
@@ -84,7 +88,7 @@ exports.test = test_1.test.extend({
|
|
|
84
88
|
.optional()
|
|
85
89
|
.default(250)
|
|
86
90
|
.parse(testInfo.project.metadata['visualCueDurationMs']);
|
|
87
|
-
const { flowId } = flowLoggingContext;
|
|
91
|
+
const { flowId, logBuffer } = flowLoggingContext;
|
|
88
92
|
const extendedPage = await (0, extendPage_1.extendPage)(page, {
|
|
89
93
|
flowId: flowId,
|
|
90
94
|
visualCueDurationMs: visualCueDurationMs,
|
|
@@ -113,7 +117,7 @@ exports.test = test_1.test.extend({
|
|
|
113
117
|
throw error;
|
|
114
118
|
}
|
|
115
119
|
finally {
|
|
116
|
-
await finalizeTest(extendedPage, testInfo);
|
|
120
|
+
await finalizeTest(extendedPage, testInfo, logBuffer);
|
|
117
121
|
}
|
|
118
122
|
},
|
|
119
123
|
});
|
|
@@ -224,7 +228,7 @@ async function attachStepScreenshots(sharedState, testInfo) {
|
|
|
224
228
|
contentType: 'application/json',
|
|
225
229
|
});
|
|
226
230
|
}
|
|
227
|
-
async function finalizeTest(page, testInfo) {
|
|
231
|
+
async function finalizeTest(page, testInfo, logBuffer) {
|
|
228
232
|
const sharedState = page._dnb;
|
|
229
233
|
try {
|
|
230
234
|
sharedState.donobuFlowMetadata.state =
|
|
@@ -237,6 +241,17 @@ async function finalizeTest(page, testInfo) {
|
|
|
237
241
|
body: JSON.stringify(sharedState.donobuFlowMetadata, null, 2),
|
|
238
242
|
contentType: 'application/json',
|
|
239
243
|
});
|
|
244
|
+
// Persist captured flow logs so they are available in the Donobu UI,
|
|
245
|
+
// mirroring what DonobuFlowsManager does for Studio-launched flows.
|
|
246
|
+
if (logBuffer) {
|
|
247
|
+
try {
|
|
248
|
+
const snapshot = logBuffer.snapshot();
|
|
249
|
+
await sharedState.persistence.setFlowFile(sharedState.donobuFlowMetadata.id, 'logs.json', Buffer.from(JSON.stringify(snapshot)));
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
Logger_1.appLogger.error('Failed to persist flow logs:', error);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
240
255
|
// Attach step-level screenshots from the flow's tool call history.
|
|
241
256
|
// These enable the HTML report to show a visual timeline of what the
|
|
242
257
|
// AI agent saw at each step.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
2
|
import winston from 'winston';
|
|
3
|
+
import type { FlowLogBuffer } from '../utils/FlowLogBuffer';
|
|
3
4
|
export declare const loggingContext: AsyncLocalStorage<unknown>;
|
|
4
5
|
/**
|
|
5
6
|
* ###################################################################################
|
|
@@ -12,11 +13,12 @@ export declare const loggingContext: AsyncLocalStorage<unknown>;
|
|
|
12
13
|
* outside the ALS store even though we seed it later in the fixture.
|
|
13
14
|
*
|
|
14
15
|
* We therefore keep a *process-local* fallback that stores the most recent flow
|
|
15
|
-
* ID for the worker. Because Playwright guarantees only one test
|
|
16
|
-
* per process, this is safe: there is no race between concurrent
|
|
17
|
-
* same worker, yet we still avoid leaking IDs across workers.
|
|
16
|
+
* ID and log buffer for the worker. Because Playwright guarantees only one test
|
|
17
|
+
* runs at a time per process, this is safe: there is no race between concurrent
|
|
18
|
+
* tests in the same worker, yet we still avoid leaking IDs across workers.
|
|
18
19
|
*/
|
|
19
20
|
export declare function setProcessLocalFlowId(flowId: string | null): void;
|
|
21
|
+
export declare function setProcessLocalLogBuffer(buffer: FlowLogBuffer | null): void;
|
|
20
22
|
export declare const appLogger: winston.Logger;
|
|
21
23
|
export declare const accessLogger: winston.Logger;
|
|
22
24
|
export declare const browserLogger: winston.Logger;
|
package/dist/esm/utils/Logger.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.networkLogger = exports.browserLogger = exports.accessLogger = exports.appLogger = exports.loggingContext = void 0;
|
|
7
7
|
exports.setProcessLocalFlowId = setProcessLocalFlowId;
|
|
8
|
+
exports.setProcessLocalLogBuffer = setProcessLocalLogBuffer;
|
|
8
9
|
exports.logErrorWithoutStack = logErrorWithoutStack;
|
|
9
10
|
exports.formatLogInfoForTest = formatLogInfoForTest;
|
|
10
11
|
const async_hooks_1 = require("async_hooks");
|
|
@@ -142,13 +143,15 @@ class FlowLogBufferTransport extends winston_transport_1.default {
|
|
|
142
143
|
}
|
|
143
144
|
log(info, callback) {
|
|
144
145
|
const store = exports.loggingContext.getStore();
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
const buffer = store?.logBuffer || processLocalLogBuffer;
|
|
147
|
+
if (buffer) {
|
|
148
|
+
buffer.push({ ...info, source: this.source });
|
|
147
149
|
}
|
|
148
150
|
callback();
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
let processLocalFlowId = null;
|
|
154
|
+
let processLocalLogBuffer = null;
|
|
152
155
|
/**
|
|
153
156
|
* ###################################################################################
|
|
154
157
|
* # WARNING! Only use this function within the context of Playwright test fixtures! #
|
|
@@ -160,13 +163,16 @@ let processLocalFlowId = null;
|
|
|
160
163
|
* outside the ALS store even though we seed it later in the fixture.
|
|
161
164
|
*
|
|
162
165
|
* We therefore keep a *process-local* fallback that stores the most recent flow
|
|
163
|
-
* ID for the worker. Because Playwright guarantees only one test
|
|
164
|
-
* per process, this is safe: there is no race between concurrent
|
|
165
|
-
* same worker, yet we still avoid leaking IDs across workers.
|
|
166
|
+
* ID and log buffer for the worker. Because Playwright guarantees only one test
|
|
167
|
+
* runs at a time per process, this is safe: there is no race between concurrent
|
|
168
|
+
* tests in the same worker, yet we still avoid leaking IDs across workers.
|
|
166
169
|
*/
|
|
167
170
|
function setProcessLocalFlowId(flowId) {
|
|
168
171
|
processLocalFlowId = flowId;
|
|
169
172
|
}
|
|
173
|
+
function setProcessLocalLogBuffer(buffer) {
|
|
174
|
+
processLocalLogBuffer = buffer;
|
|
175
|
+
}
|
|
170
176
|
// Format to add the currently running flow's ID (if any) from AsyncLocalStorage
|
|
171
177
|
const flowIdFormat = winston_1.default.format((info) => {
|
|
172
178
|
const store = exports.loggingContext.getStore();
|
|
@@ -31,6 +31,24 @@ const donobuTestStack_1 = require("../test/utils/donobuTestStack");
|
|
|
31
31
|
const originalGotoRegistry_1 = require("./originalGotoRegistry");
|
|
32
32
|
const SmartSelector_1 = require("./SmartSelector");
|
|
33
33
|
const tbd_1 = require("./tbd");
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a URL against the browser context's baseURL (if set), mirroring
|
|
36
|
+
* what Playwright does internally before issuing the navigation request.
|
|
37
|
+
* This gives us the *intended* URL (before any server-side redirects).
|
|
38
|
+
*/
|
|
39
|
+
function resolveBaseUrl(page, url) {
|
|
40
|
+
const baseURL = page.context()._options?.baseURL;
|
|
41
|
+
// Only resolve against baseURL if the URL is relative (no scheme like https:).
|
|
42
|
+
if (baseURL && !/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url)) {
|
|
43
|
+
try {
|
|
44
|
+
return new URL(url, baseURL).href;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return url;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return url;
|
|
51
|
+
}
|
|
34
52
|
// Donobu page extension helpers: decorate Playwright pages with Donobu behaviors and keep one
|
|
35
53
|
// coherent flow (and persistence record) per browser context so new tabs share state safely.
|
|
36
54
|
const PLACEHOLDER_FLOW_URL = 'https://example.com';
|
|
@@ -340,13 +358,14 @@ Use this information to return an appropriate JSON object.`,
|
|
|
340
358
|
page.goto = async (url, options) => {
|
|
341
359
|
const startedAt = new Date().getTime();
|
|
342
360
|
const flowId = sharedState.donobuFlowMetadata.id;
|
|
361
|
+
const effectiveUrl = resolveBaseUrl(page, url);
|
|
343
362
|
// First navigation sets the target website and records the tool call with screenshots.
|
|
344
363
|
if (sharedState.donobuFlowMetadata.web?.targetWebsite === PLACEHOLDER_FLOW_URL) {
|
|
345
364
|
sharedState.donobuFlowMetadata = {
|
|
346
365
|
...sharedState.donobuFlowMetadata,
|
|
347
366
|
web: {
|
|
348
367
|
...sharedState.donobuFlowMetadata.web,
|
|
349
|
-
targetWebsite:
|
|
368
|
+
targetWebsite: effectiveUrl,
|
|
350
369
|
},
|
|
351
370
|
};
|
|
352
371
|
await sharedState.persistence.setFlowMetadata(sharedState.donobuFlowMetadata);
|
|
@@ -362,18 +381,18 @@ Use this information to return an appropriate JSON object.`,
|
|
|
362
381
|
id: MiscUtils_1.MiscUtils.createAdHocToolCallId(),
|
|
363
382
|
toolName: GoToWebpageTool_1.GoToWebpageTool.NAME,
|
|
364
383
|
parameters: {
|
|
365
|
-
url:
|
|
384
|
+
url: effectiveUrl,
|
|
366
385
|
},
|
|
367
386
|
outcome: {
|
|
368
387
|
isSuccessful: true,
|
|
369
|
-
forLlm: `Successfully navigated to ${
|
|
388
|
+
forLlm: `Successfully navigated to ${effectiveUrl}`,
|
|
370
389
|
metadata: {
|
|
371
390
|
pageTitle: pageTitle,
|
|
372
391
|
resolvedUrl: page.url(),
|
|
373
392
|
},
|
|
374
393
|
},
|
|
375
394
|
postCallImageId: postCallImageId,
|
|
376
|
-
page:
|
|
395
|
+
page: effectiveUrl,
|
|
377
396
|
startedAt: startedAt,
|
|
378
397
|
completedAt: completedAt,
|
|
379
398
|
});
|
|
@@ -390,7 +409,7 @@ Use this information to return an appropriate JSON object.`,
|
|
|
390
409
|
id: MiscUtils_1.MiscUtils.createAdHocToolCallId(),
|
|
391
410
|
toolName: GoToWebpageTool_1.GoToWebpageTool.NAME,
|
|
392
411
|
parameters: {
|
|
393
|
-
url:
|
|
412
|
+
url: effectiveUrl,
|
|
394
413
|
},
|
|
395
414
|
outcome: {
|
|
396
415
|
isSuccessful: false,
|
|
@@ -398,7 +417,7 @@ Use this information to return an appropriate JSON object.`,
|
|
|
398
417
|
metadata: null,
|
|
399
418
|
},
|
|
400
419
|
postCallImageId: postCallImageId,
|
|
401
|
-
page:
|
|
420
|
+
page: effectiveUrl,
|
|
402
421
|
startedAt: startedAt,
|
|
403
422
|
completedAt: completedAt,
|
|
404
423
|
});
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { GptClient } from '../../clients/GptClient';
|
|
2
2
|
import type { BrowserStorageState } from '../../models/BrowserStorageState';
|
|
3
|
+
import { FlowLogBuffer } from '../../utils/FlowLogBuffer';
|
|
3
4
|
import type { DonobuExtendedPage } from '../page/DonobuExtendedPage';
|
|
4
5
|
export * from 'playwright/test';
|
|
5
6
|
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
|
|
6
7
|
flowLoggingContext: {
|
|
7
8
|
flowId: string;
|
|
9
|
+
logBuffer: FlowLogBuffer;
|
|
8
10
|
};
|
|
9
11
|
storageState?: BrowserStorageState | Promise<BrowserStorageState>;
|
|
10
12
|
gptClient?: GptClient;
|
|
@@ -22,6 +22,7 @@ const v4_1 = require("zod/v4");
|
|
|
22
22
|
const envVars_1 = require("../../envVars");
|
|
23
23
|
const DonobuFlowsManager_1 = require("../../managers/DonobuFlowsManager");
|
|
24
24
|
const BrowserUtils_1 = require("../../utils/BrowserUtils");
|
|
25
|
+
const FlowLogBuffer_1 = require("../../utils/FlowLogBuffer");
|
|
25
26
|
const Logger_1 = require("../../utils/Logger");
|
|
26
27
|
const MiscUtils_1 = require("../../utils/MiscUtils");
|
|
27
28
|
const cacheLocator_1 = require("../ai/cache/cacheLocator");
|
|
@@ -55,16 +56,19 @@ exports.test = test_1.test.extend({
|
|
|
55
56
|
// Donobu flow ID, so metadata would overwrite and concurrent executions could
|
|
56
57
|
// clobber each other.
|
|
57
58
|
const flowId = (0, crypto_1.randomUUID)();
|
|
58
|
-
const
|
|
59
|
+
const logBuffer = new FlowLogBuffer_1.FlowLogBuffer();
|
|
60
|
+
const flowContext = { flowId, logBuffer };
|
|
59
61
|
const asyncScope = new async_hooks_1.AsyncResource('DonobuFlowContext');
|
|
60
62
|
await Logger_1.loggingContext.run(flowContext, async () => {
|
|
61
63
|
Logger_1.loggingContext.enterWith(flowContext);
|
|
62
64
|
(0, Logger_1.setProcessLocalFlowId)(flowId);
|
|
65
|
+
(0, Logger_1.setProcessLocalLogBuffer)(logBuffer);
|
|
63
66
|
try {
|
|
64
|
-
await asyncScope.runInAsyncScope(() => use({ flowId }));
|
|
67
|
+
await asyncScope.runInAsyncScope(() => use({ flowId, logBuffer }));
|
|
65
68
|
}
|
|
66
69
|
finally {
|
|
67
70
|
(0, Logger_1.setProcessLocalFlowId)(null);
|
|
71
|
+
(0, Logger_1.setProcessLocalLogBuffer)(null);
|
|
68
72
|
}
|
|
69
73
|
});
|
|
70
74
|
},
|
|
@@ -84,7 +88,7 @@ exports.test = test_1.test.extend({
|
|
|
84
88
|
.optional()
|
|
85
89
|
.default(250)
|
|
86
90
|
.parse(testInfo.project.metadata['visualCueDurationMs']);
|
|
87
|
-
const { flowId } = flowLoggingContext;
|
|
91
|
+
const { flowId, logBuffer } = flowLoggingContext;
|
|
88
92
|
const extendedPage = await (0, extendPage_1.extendPage)(page, {
|
|
89
93
|
flowId: flowId,
|
|
90
94
|
visualCueDurationMs: visualCueDurationMs,
|
|
@@ -113,7 +117,7 @@ exports.test = test_1.test.extend({
|
|
|
113
117
|
throw error;
|
|
114
118
|
}
|
|
115
119
|
finally {
|
|
116
|
-
await finalizeTest(extendedPage, testInfo);
|
|
120
|
+
await finalizeTest(extendedPage, testInfo, logBuffer);
|
|
117
121
|
}
|
|
118
122
|
},
|
|
119
123
|
});
|
|
@@ -224,7 +228,7 @@ async function attachStepScreenshots(sharedState, testInfo) {
|
|
|
224
228
|
contentType: 'application/json',
|
|
225
229
|
});
|
|
226
230
|
}
|
|
227
|
-
async function finalizeTest(page, testInfo) {
|
|
231
|
+
async function finalizeTest(page, testInfo, logBuffer) {
|
|
228
232
|
const sharedState = page._dnb;
|
|
229
233
|
try {
|
|
230
234
|
sharedState.donobuFlowMetadata.state =
|
|
@@ -237,6 +241,17 @@ async function finalizeTest(page, testInfo) {
|
|
|
237
241
|
body: JSON.stringify(sharedState.donobuFlowMetadata, null, 2),
|
|
238
242
|
contentType: 'application/json',
|
|
239
243
|
});
|
|
244
|
+
// Persist captured flow logs so they are available in the Donobu UI,
|
|
245
|
+
// mirroring what DonobuFlowsManager does for Studio-launched flows.
|
|
246
|
+
if (logBuffer) {
|
|
247
|
+
try {
|
|
248
|
+
const snapshot = logBuffer.snapshot();
|
|
249
|
+
await sharedState.persistence.setFlowFile(sharedState.donobuFlowMetadata.id, 'logs.json', Buffer.from(JSON.stringify(snapshot)));
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
Logger_1.appLogger.error('Failed to persist flow logs:', error);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
240
255
|
// Attach step-level screenshots from the flow's tool call history.
|
|
241
256
|
// These enable the HTML report to show a visual timeline of what the
|
|
242
257
|
// AI agent saw at each step.
|
package/dist/utils/Logger.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
2
2
|
import winston from 'winston';
|
|
3
|
+
import type { FlowLogBuffer } from '../utils/FlowLogBuffer';
|
|
3
4
|
export declare const loggingContext: AsyncLocalStorage<unknown>;
|
|
4
5
|
/**
|
|
5
6
|
* ###################################################################################
|
|
@@ -12,11 +13,12 @@ export declare const loggingContext: AsyncLocalStorage<unknown>;
|
|
|
12
13
|
* outside the ALS store even though we seed it later in the fixture.
|
|
13
14
|
*
|
|
14
15
|
* We therefore keep a *process-local* fallback that stores the most recent flow
|
|
15
|
-
* ID for the worker. Because Playwright guarantees only one test
|
|
16
|
-
* per process, this is safe: there is no race between concurrent
|
|
17
|
-
* same worker, yet we still avoid leaking IDs across workers.
|
|
16
|
+
* ID and log buffer for the worker. Because Playwright guarantees only one test
|
|
17
|
+
* runs at a time per process, this is safe: there is no race between concurrent
|
|
18
|
+
* tests in the same worker, yet we still avoid leaking IDs across workers.
|
|
18
19
|
*/
|
|
19
20
|
export declare function setProcessLocalFlowId(flowId: string | null): void;
|
|
21
|
+
export declare function setProcessLocalLogBuffer(buffer: FlowLogBuffer | null): void;
|
|
20
22
|
export declare const appLogger: winston.Logger;
|
|
21
23
|
export declare const accessLogger: winston.Logger;
|
|
22
24
|
export declare const browserLogger: winston.Logger;
|
package/dist/utils/Logger.js
CHANGED
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.networkLogger = exports.browserLogger = exports.accessLogger = exports.appLogger = exports.loggingContext = void 0;
|
|
7
7
|
exports.setProcessLocalFlowId = setProcessLocalFlowId;
|
|
8
|
+
exports.setProcessLocalLogBuffer = setProcessLocalLogBuffer;
|
|
8
9
|
exports.logErrorWithoutStack = logErrorWithoutStack;
|
|
9
10
|
exports.formatLogInfoForTest = formatLogInfoForTest;
|
|
10
11
|
const async_hooks_1 = require("async_hooks");
|
|
@@ -142,13 +143,15 @@ class FlowLogBufferTransport extends winston_transport_1.default {
|
|
|
142
143
|
}
|
|
143
144
|
log(info, callback) {
|
|
144
145
|
const store = exports.loggingContext.getStore();
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
const buffer = store?.logBuffer || processLocalLogBuffer;
|
|
147
|
+
if (buffer) {
|
|
148
|
+
buffer.push({ ...info, source: this.source });
|
|
147
149
|
}
|
|
148
150
|
callback();
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
let processLocalFlowId = null;
|
|
154
|
+
let processLocalLogBuffer = null;
|
|
152
155
|
/**
|
|
153
156
|
* ###################################################################################
|
|
154
157
|
* # WARNING! Only use this function within the context of Playwright test fixtures! #
|
|
@@ -160,13 +163,16 @@ let processLocalFlowId = null;
|
|
|
160
163
|
* outside the ALS store even though we seed it later in the fixture.
|
|
161
164
|
*
|
|
162
165
|
* We therefore keep a *process-local* fallback that stores the most recent flow
|
|
163
|
-
* ID for the worker. Because Playwright guarantees only one test
|
|
164
|
-
* per process, this is safe: there is no race between concurrent
|
|
165
|
-
* same worker, yet we still avoid leaking IDs across workers.
|
|
166
|
+
* ID and log buffer for the worker. Because Playwright guarantees only one test
|
|
167
|
+
* runs at a time per process, this is safe: there is no race between concurrent
|
|
168
|
+
* tests in the same worker, yet we still avoid leaking IDs across workers.
|
|
166
169
|
*/
|
|
167
170
|
function setProcessLocalFlowId(flowId) {
|
|
168
171
|
processLocalFlowId = flowId;
|
|
169
172
|
}
|
|
173
|
+
function setProcessLocalLogBuffer(buffer) {
|
|
174
|
+
processLocalLogBuffer = buffer;
|
|
175
|
+
}
|
|
170
176
|
// Format to add the currently running flow's ID (if any) from AsyncLocalStorage
|
|
171
177
|
const flowIdFormat = winston_1.default.format((info) => {
|
|
172
178
|
const store = exports.loggingContext.getStore();
|