donobu 5.18.1 → 5.18.3
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/test/testExtension.d.ts +2 -0
- package/dist/esm/lib/test/testExtension.js +26 -5
- package/dist/esm/managers/WebTargetInspector.js +2 -75
- package/dist/esm/utils/Logger.d.ts +5 -3
- package/dist/esm/utils/Logger.js +11 -5
- package/dist/esm/utils/PageLogListeners.d.ts +15 -0
- package/dist/esm/utils/PageLogListeners.js +94 -0
- package/dist/lib/test/testExtension.d.ts +2 -0
- package/dist/lib/test/testExtension.js +26 -5
- package/dist/managers/WebTargetInspector.js +2 -75
- package/dist/utils/Logger.d.ts +5 -3
- package/dist/utils/Logger.js +11 -5
- package/dist/utils/PageLogListeners.d.ts +15 -0
- package/dist/utils/PageLogListeners.js +94 -0
- package/package.json +1 -1
|
@@ -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,8 +22,10 @@ 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");
|
|
28
|
+
const PageLogListeners_1 = require("../../utils/PageLogListeners");
|
|
27
29
|
const cacheLocator_1 = require("../ai/cache/cacheLocator");
|
|
28
30
|
const extendPage_1 = require("../page/extendPage");
|
|
29
31
|
const tbd_1 = require("../page/tbd");
|
|
@@ -55,16 +57,19 @@ exports.test = test_1.test.extend({
|
|
|
55
57
|
// Donobu flow ID, so metadata would overwrite and concurrent executions could
|
|
56
58
|
// clobber each other.
|
|
57
59
|
const flowId = (0, crypto_1.randomUUID)();
|
|
58
|
-
const
|
|
60
|
+
const logBuffer = new FlowLogBuffer_1.FlowLogBuffer();
|
|
61
|
+
const flowContext = { flowId, logBuffer };
|
|
59
62
|
const asyncScope = new async_hooks_1.AsyncResource('DonobuFlowContext');
|
|
60
63
|
await Logger_1.loggingContext.run(flowContext, async () => {
|
|
61
64
|
Logger_1.loggingContext.enterWith(flowContext);
|
|
62
65
|
(0, Logger_1.setProcessLocalFlowId)(flowId);
|
|
66
|
+
(0, Logger_1.setProcessLocalLogBuffer)(logBuffer);
|
|
63
67
|
try {
|
|
64
|
-
await asyncScope.runInAsyncScope(() => use({ flowId }));
|
|
68
|
+
await asyncScope.runInAsyncScope(() => use({ flowId, logBuffer }));
|
|
65
69
|
}
|
|
66
70
|
finally {
|
|
67
71
|
(0, Logger_1.setProcessLocalFlowId)(null);
|
|
72
|
+
(0, Logger_1.setProcessLocalLogBuffer)(null);
|
|
68
73
|
}
|
|
69
74
|
});
|
|
70
75
|
},
|
|
@@ -84,7 +89,7 @@ exports.test = test_1.test.extend({
|
|
|
84
89
|
.optional()
|
|
85
90
|
.default(250)
|
|
86
91
|
.parse(testInfo.project.metadata['visualCueDurationMs']);
|
|
87
|
-
const { flowId } = flowLoggingContext;
|
|
92
|
+
const { flowId, logBuffer } = flowLoggingContext;
|
|
88
93
|
const extendedPage = await (0, extendPage_1.extendPage)(page, {
|
|
89
94
|
flowId: flowId,
|
|
90
95
|
visualCueDurationMs: visualCueDurationMs,
|
|
@@ -97,6 +102,11 @@ exports.test = test_1.test.extend({
|
|
|
97
102
|
});
|
|
98
103
|
extendedPage._dnb.donobuFlowMetadata.name = getSanitizedTestName(testInfo);
|
|
99
104
|
extendedPage._dnb.donobuFlowMetadata.overallObjective = overallObjective;
|
|
105
|
+
// Register browser console and network listeners so that logs from these
|
|
106
|
+
// sources are captured into the flow's logBuffer. In Studio-launched flows
|
|
107
|
+
// this is done by WebTargetInspector.initialize(), but that method is not
|
|
108
|
+
// called during test runs, so we wire the listeners up here directly.
|
|
109
|
+
(0, PageLogListeners_1.registerPageLogListeners)(page);
|
|
100
110
|
// Bind the Playwright-provided `use` callback to an async resource so that
|
|
101
111
|
// any microtasks scheduled inside the test body keep the flow logging
|
|
102
112
|
// context. Without this, Playwright may re-use earlier async resources that
|
|
@@ -113,7 +123,7 @@ exports.test = test_1.test.extend({
|
|
|
113
123
|
throw error;
|
|
114
124
|
}
|
|
115
125
|
finally {
|
|
116
|
-
await finalizeTest(extendedPage, testInfo);
|
|
126
|
+
await finalizeTest(extendedPage, testInfo, logBuffer);
|
|
117
127
|
}
|
|
118
128
|
},
|
|
119
129
|
});
|
|
@@ -224,7 +234,7 @@ async function attachStepScreenshots(sharedState, testInfo) {
|
|
|
224
234
|
contentType: 'application/json',
|
|
225
235
|
});
|
|
226
236
|
}
|
|
227
|
-
async function finalizeTest(page, testInfo) {
|
|
237
|
+
async function finalizeTest(page, testInfo, logBuffer) {
|
|
228
238
|
const sharedState = page._dnb;
|
|
229
239
|
try {
|
|
230
240
|
sharedState.donobuFlowMetadata.state =
|
|
@@ -237,6 +247,17 @@ async function finalizeTest(page, testInfo) {
|
|
|
237
247
|
body: JSON.stringify(sharedState.donobuFlowMetadata, null, 2),
|
|
238
248
|
contentType: 'application/json',
|
|
239
249
|
});
|
|
250
|
+
// Persist captured flow logs so they are available in the Donobu UI,
|
|
251
|
+
// mirroring what DonobuFlowsManager does for Studio-launched flows.
|
|
252
|
+
if (logBuffer) {
|
|
253
|
+
try {
|
|
254
|
+
const snapshot = logBuffer.snapshot();
|
|
255
|
+
await sharedState.persistence.setFlowFile(sharedState.donobuFlowMetadata.id, 'logs.json', Buffer.from(JSON.stringify(snapshot)));
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
Logger_1.appLogger.error('Failed to persist flow logs:', error);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
240
261
|
// Attach step-level screenshots from the flow's tool call history.
|
|
241
262
|
// These enable the HTML report to show a visual timeline of what the
|
|
242
263
|
// AI agent saw at each step.
|
|
@@ -6,6 +6,7 @@ const SetDonobuAnnotations_1 = require("../bindings/SetDonobuAnnotations");
|
|
|
6
6
|
const PageClosedException_1 = require("../exceptions/PageClosedException");
|
|
7
7
|
const BrowserUtils_1 = require("../utils/BrowserUtils");
|
|
8
8
|
const Logger_1 = require("../utils/Logger");
|
|
9
|
+
const PageLogListeners_1 = require("../utils/PageLogListeners");
|
|
9
10
|
const PlaywrightUtils_1 = require("../utils/PlaywrightUtils");
|
|
10
11
|
const PageInspector_1 = require("./PageInspector");
|
|
11
12
|
/**
|
|
@@ -185,81 +186,7 @@ The active (i.e. in focus) tab is ${this._target.current.url()}`;
|
|
|
185
186
|
/* ------------------------------------------------------------------ */
|
|
186
187
|
handleNewPage(page, callbacks) {
|
|
187
188
|
this._target.current = page;
|
|
188
|
-
|
|
189
|
-
const { url, lineNumber, columnNumber } = msg.location();
|
|
190
|
-
const hasSourceLocation = lineNumber !== 0 || columnNumber !== 0;
|
|
191
|
-
const meta = url
|
|
192
|
-
? hasSourceLocation
|
|
193
|
-
? { url, lineNumber, columnNumber }
|
|
194
|
-
: { url }
|
|
195
|
-
: undefined;
|
|
196
|
-
switch (msg.type()) {
|
|
197
|
-
case 'error':
|
|
198
|
-
Logger_1.browserLogger.error(msg.text(), meta);
|
|
199
|
-
break;
|
|
200
|
-
case 'warning':
|
|
201
|
-
Logger_1.browserLogger.warn(msg.text(), meta);
|
|
202
|
-
break;
|
|
203
|
-
case 'debug':
|
|
204
|
-
Logger_1.browserLogger.debug(msg.text(), meta);
|
|
205
|
-
break;
|
|
206
|
-
default:
|
|
207
|
-
Logger_1.browserLogger.info(msg.text(), meta);
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
page.on('pageerror', (error) => {
|
|
212
|
-
Logger_1.browserLogger.error(error.message, {
|
|
213
|
-
stack: error.stack,
|
|
214
|
-
url: page.url(),
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
// Fallback timing: track request start times keyed by the Request object
|
|
218
|
-
// itself (via WeakMap) so concurrent requests to the same URL don't collide.
|
|
219
|
-
// Prefer Playwright's native timing API when it has data.
|
|
220
|
-
const requestStartTimes = new WeakMap();
|
|
221
|
-
page.on('request', (request) => {
|
|
222
|
-
requestStartTimes.set(request, Date.now());
|
|
223
|
-
});
|
|
224
|
-
const getDuration = (request) => {
|
|
225
|
-
const timing = request.timing();
|
|
226
|
-
if (timing.responseEnd >= 0) {
|
|
227
|
-
return Math.round(timing.responseEnd);
|
|
228
|
-
}
|
|
229
|
-
const startTime = requestStartTimes.get(request);
|
|
230
|
-
return startTime !== undefined ? Date.now() - startTime : undefined;
|
|
231
|
-
};
|
|
232
|
-
page.on('response', (response) => {
|
|
233
|
-
const request = response.request();
|
|
234
|
-
const status = response.status();
|
|
235
|
-
const duration = getDuration(request);
|
|
236
|
-
const meta = {
|
|
237
|
-
method: request.method(),
|
|
238
|
-
url: request.url(),
|
|
239
|
-
status,
|
|
240
|
-
duration,
|
|
241
|
-
resourceType: request.resourceType(),
|
|
242
|
-
};
|
|
243
|
-
if (status >= 500) {
|
|
244
|
-
Logger_1.networkLogger.error(request.url(), meta);
|
|
245
|
-
}
|
|
246
|
-
else if (status >= 400) {
|
|
247
|
-
Logger_1.networkLogger.warn(request.url(), meta);
|
|
248
|
-
}
|
|
249
|
-
else {
|
|
250
|
-
Logger_1.networkLogger.info(request.url(), meta);
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
page.on('requestfailed', (request) => {
|
|
254
|
-
const duration = getDuration(request);
|
|
255
|
-
Logger_1.networkLogger.error(`${request.url()} FAILED: ${request.failure()?.errorText}`, {
|
|
256
|
-
method: request.method(),
|
|
257
|
-
url: request.url(),
|
|
258
|
-
resourceType: request.resourceType(),
|
|
259
|
-
failureReason: request.failure()?.errorText,
|
|
260
|
-
duration,
|
|
261
|
-
});
|
|
262
|
-
});
|
|
189
|
+
(0, PageLogListeners_1.registerPageLogListeners)(page);
|
|
263
190
|
if (callbacks.metadata.runMode !== 'INSTRUCT') {
|
|
264
191
|
page.on('domcontentloaded', async () => {
|
|
265
192
|
await this._interactionVisualizer.showMouse(page);
|
|
@@ -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();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
/**
|
|
3
|
+
* Registers Playwright page event listeners that route browser console messages
|
|
4
|
+
* and network request/response data to the {@link browserLogger} and
|
|
5
|
+
* {@link networkLogger} Winston loggers. These loggers include a
|
|
6
|
+
* {@link FlowLogBufferTransport} that captures entries into the per-flow
|
|
7
|
+
* log buffer (when one is available via AsyncLocalStorage or the process-local
|
|
8
|
+
* fallback).
|
|
9
|
+
*
|
|
10
|
+
* This function is called from both {@link WebTargetInspector.handleNewPage}
|
|
11
|
+
* (Studio-launched flows) and the Playwright test extension fixture, ensuring
|
|
12
|
+
* identical logging behaviour across both runtime contexts.
|
|
13
|
+
*/
|
|
14
|
+
export declare function registerPageLogListeners(page: Page): void;
|
|
15
|
+
//# sourceMappingURL=PageLogListeners.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerPageLogListeners = registerPageLogListeners;
|
|
4
|
+
const Logger_1 = require("./Logger");
|
|
5
|
+
/**
|
|
6
|
+
* Registers Playwright page event listeners that route browser console messages
|
|
7
|
+
* and network request/response data to the {@link browserLogger} and
|
|
8
|
+
* {@link networkLogger} Winston loggers. These loggers include a
|
|
9
|
+
* {@link FlowLogBufferTransport} that captures entries into the per-flow
|
|
10
|
+
* log buffer (when one is available via AsyncLocalStorage or the process-local
|
|
11
|
+
* fallback).
|
|
12
|
+
*
|
|
13
|
+
* This function is called from both {@link WebTargetInspector.handleNewPage}
|
|
14
|
+
* (Studio-launched flows) and the Playwright test extension fixture, ensuring
|
|
15
|
+
* identical logging behaviour across both runtime contexts.
|
|
16
|
+
*/
|
|
17
|
+
function registerPageLogListeners(page) {
|
|
18
|
+
page.on('console', (msg) => {
|
|
19
|
+
const { url, lineNumber, columnNumber } = msg.location();
|
|
20
|
+
const hasSourceLocation = lineNumber !== 0 || columnNumber !== 0;
|
|
21
|
+
const meta = url
|
|
22
|
+
? hasSourceLocation
|
|
23
|
+
? { url, lineNumber, columnNumber }
|
|
24
|
+
: { url }
|
|
25
|
+
: undefined;
|
|
26
|
+
switch (msg.type()) {
|
|
27
|
+
case 'error':
|
|
28
|
+
Logger_1.browserLogger.error(msg.text(), meta);
|
|
29
|
+
break;
|
|
30
|
+
case 'warning':
|
|
31
|
+
Logger_1.browserLogger.warn(msg.text(), meta);
|
|
32
|
+
break;
|
|
33
|
+
case 'debug':
|
|
34
|
+
Logger_1.browserLogger.debug(msg.text(), meta);
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
Logger_1.browserLogger.info(msg.text(), meta);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
page.on('pageerror', (error) => {
|
|
42
|
+
Logger_1.browserLogger.error(error.message, {
|
|
43
|
+
stack: error.stack,
|
|
44
|
+
url: page.url(),
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
// Fallback timing: track request start times keyed by the Request object
|
|
48
|
+
// itself (via WeakMap) so concurrent requests to the same URL don't collide.
|
|
49
|
+
// Prefer Playwright's native timing API when it has data.
|
|
50
|
+
const requestStartTimes = new WeakMap();
|
|
51
|
+
page.on('request', (request) => {
|
|
52
|
+
requestStartTimes.set(request, Date.now());
|
|
53
|
+
});
|
|
54
|
+
const getDuration = (request) => {
|
|
55
|
+
const timing = request.timing();
|
|
56
|
+
if (timing.responseEnd >= 0) {
|
|
57
|
+
return Math.round(timing.responseEnd);
|
|
58
|
+
}
|
|
59
|
+
const startTime = requestStartTimes.get(request);
|
|
60
|
+
return startTime !== undefined ? Date.now() - startTime : undefined;
|
|
61
|
+
};
|
|
62
|
+
page.on('response', (response) => {
|
|
63
|
+
const request = response.request();
|
|
64
|
+
const status = response.status();
|
|
65
|
+
const duration = getDuration(request);
|
|
66
|
+
const meta = {
|
|
67
|
+
method: request.method(),
|
|
68
|
+
url: request.url(),
|
|
69
|
+
status,
|
|
70
|
+
duration,
|
|
71
|
+
resourceType: request.resourceType(),
|
|
72
|
+
};
|
|
73
|
+
if (status >= 500) {
|
|
74
|
+
Logger_1.networkLogger.error(request.url(), meta);
|
|
75
|
+
}
|
|
76
|
+
else if (status >= 400) {
|
|
77
|
+
Logger_1.networkLogger.warn(request.url(), meta);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
Logger_1.networkLogger.info(request.url(), meta);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
page.on('requestfailed', (request) => {
|
|
84
|
+
const duration = getDuration(request);
|
|
85
|
+
Logger_1.networkLogger.error(`${request.url()} FAILED: ${request.failure()?.errorText}`, {
|
|
86
|
+
method: request.method(),
|
|
87
|
+
url: request.url(),
|
|
88
|
+
resourceType: request.resourceType(),
|
|
89
|
+
failureReason: request.failure()?.errorText,
|
|
90
|
+
duration,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=PageLogListeners.js.map
|
|
@@ -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,8 +22,10 @@ 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");
|
|
28
|
+
const PageLogListeners_1 = require("../../utils/PageLogListeners");
|
|
27
29
|
const cacheLocator_1 = require("../ai/cache/cacheLocator");
|
|
28
30
|
const extendPage_1 = require("../page/extendPage");
|
|
29
31
|
const tbd_1 = require("../page/tbd");
|
|
@@ -55,16 +57,19 @@ exports.test = test_1.test.extend({
|
|
|
55
57
|
// Donobu flow ID, so metadata would overwrite and concurrent executions could
|
|
56
58
|
// clobber each other.
|
|
57
59
|
const flowId = (0, crypto_1.randomUUID)();
|
|
58
|
-
const
|
|
60
|
+
const logBuffer = new FlowLogBuffer_1.FlowLogBuffer();
|
|
61
|
+
const flowContext = { flowId, logBuffer };
|
|
59
62
|
const asyncScope = new async_hooks_1.AsyncResource('DonobuFlowContext');
|
|
60
63
|
await Logger_1.loggingContext.run(flowContext, async () => {
|
|
61
64
|
Logger_1.loggingContext.enterWith(flowContext);
|
|
62
65
|
(0, Logger_1.setProcessLocalFlowId)(flowId);
|
|
66
|
+
(0, Logger_1.setProcessLocalLogBuffer)(logBuffer);
|
|
63
67
|
try {
|
|
64
|
-
await asyncScope.runInAsyncScope(() => use({ flowId }));
|
|
68
|
+
await asyncScope.runInAsyncScope(() => use({ flowId, logBuffer }));
|
|
65
69
|
}
|
|
66
70
|
finally {
|
|
67
71
|
(0, Logger_1.setProcessLocalFlowId)(null);
|
|
72
|
+
(0, Logger_1.setProcessLocalLogBuffer)(null);
|
|
68
73
|
}
|
|
69
74
|
});
|
|
70
75
|
},
|
|
@@ -84,7 +89,7 @@ exports.test = test_1.test.extend({
|
|
|
84
89
|
.optional()
|
|
85
90
|
.default(250)
|
|
86
91
|
.parse(testInfo.project.metadata['visualCueDurationMs']);
|
|
87
|
-
const { flowId } = flowLoggingContext;
|
|
92
|
+
const { flowId, logBuffer } = flowLoggingContext;
|
|
88
93
|
const extendedPage = await (0, extendPage_1.extendPage)(page, {
|
|
89
94
|
flowId: flowId,
|
|
90
95
|
visualCueDurationMs: visualCueDurationMs,
|
|
@@ -97,6 +102,11 @@ exports.test = test_1.test.extend({
|
|
|
97
102
|
});
|
|
98
103
|
extendedPage._dnb.donobuFlowMetadata.name = getSanitizedTestName(testInfo);
|
|
99
104
|
extendedPage._dnb.donobuFlowMetadata.overallObjective = overallObjective;
|
|
105
|
+
// Register browser console and network listeners so that logs from these
|
|
106
|
+
// sources are captured into the flow's logBuffer. In Studio-launched flows
|
|
107
|
+
// this is done by WebTargetInspector.initialize(), but that method is not
|
|
108
|
+
// called during test runs, so we wire the listeners up here directly.
|
|
109
|
+
(0, PageLogListeners_1.registerPageLogListeners)(page);
|
|
100
110
|
// Bind the Playwright-provided `use` callback to an async resource so that
|
|
101
111
|
// any microtasks scheduled inside the test body keep the flow logging
|
|
102
112
|
// context. Without this, Playwright may re-use earlier async resources that
|
|
@@ -113,7 +123,7 @@ exports.test = test_1.test.extend({
|
|
|
113
123
|
throw error;
|
|
114
124
|
}
|
|
115
125
|
finally {
|
|
116
|
-
await finalizeTest(extendedPage, testInfo);
|
|
126
|
+
await finalizeTest(extendedPage, testInfo, logBuffer);
|
|
117
127
|
}
|
|
118
128
|
},
|
|
119
129
|
});
|
|
@@ -224,7 +234,7 @@ async function attachStepScreenshots(sharedState, testInfo) {
|
|
|
224
234
|
contentType: 'application/json',
|
|
225
235
|
});
|
|
226
236
|
}
|
|
227
|
-
async function finalizeTest(page, testInfo) {
|
|
237
|
+
async function finalizeTest(page, testInfo, logBuffer) {
|
|
228
238
|
const sharedState = page._dnb;
|
|
229
239
|
try {
|
|
230
240
|
sharedState.donobuFlowMetadata.state =
|
|
@@ -237,6 +247,17 @@ async function finalizeTest(page, testInfo) {
|
|
|
237
247
|
body: JSON.stringify(sharedState.donobuFlowMetadata, null, 2),
|
|
238
248
|
contentType: 'application/json',
|
|
239
249
|
});
|
|
250
|
+
// Persist captured flow logs so they are available in the Donobu UI,
|
|
251
|
+
// mirroring what DonobuFlowsManager does for Studio-launched flows.
|
|
252
|
+
if (logBuffer) {
|
|
253
|
+
try {
|
|
254
|
+
const snapshot = logBuffer.snapshot();
|
|
255
|
+
await sharedState.persistence.setFlowFile(sharedState.donobuFlowMetadata.id, 'logs.json', Buffer.from(JSON.stringify(snapshot)));
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
Logger_1.appLogger.error('Failed to persist flow logs:', error);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
240
261
|
// Attach step-level screenshots from the flow's tool call history.
|
|
241
262
|
// These enable the HTML report to show a visual timeline of what the
|
|
242
263
|
// AI agent saw at each step.
|
|
@@ -6,6 +6,7 @@ const SetDonobuAnnotations_1 = require("../bindings/SetDonobuAnnotations");
|
|
|
6
6
|
const PageClosedException_1 = require("../exceptions/PageClosedException");
|
|
7
7
|
const BrowserUtils_1 = require("../utils/BrowserUtils");
|
|
8
8
|
const Logger_1 = require("../utils/Logger");
|
|
9
|
+
const PageLogListeners_1 = require("../utils/PageLogListeners");
|
|
9
10
|
const PlaywrightUtils_1 = require("../utils/PlaywrightUtils");
|
|
10
11
|
const PageInspector_1 = require("./PageInspector");
|
|
11
12
|
/**
|
|
@@ -185,81 +186,7 @@ The active (i.e. in focus) tab is ${this._target.current.url()}`;
|
|
|
185
186
|
/* ------------------------------------------------------------------ */
|
|
186
187
|
handleNewPage(page, callbacks) {
|
|
187
188
|
this._target.current = page;
|
|
188
|
-
|
|
189
|
-
const { url, lineNumber, columnNumber } = msg.location();
|
|
190
|
-
const hasSourceLocation = lineNumber !== 0 || columnNumber !== 0;
|
|
191
|
-
const meta = url
|
|
192
|
-
? hasSourceLocation
|
|
193
|
-
? { url, lineNumber, columnNumber }
|
|
194
|
-
: { url }
|
|
195
|
-
: undefined;
|
|
196
|
-
switch (msg.type()) {
|
|
197
|
-
case 'error':
|
|
198
|
-
Logger_1.browserLogger.error(msg.text(), meta);
|
|
199
|
-
break;
|
|
200
|
-
case 'warning':
|
|
201
|
-
Logger_1.browserLogger.warn(msg.text(), meta);
|
|
202
|
-
break;
|
|
203
|
-
case 'debug':
|
|
204
|
-
Logger_1.browserLogger.debug(msg.text(), meta);
|
|
205
|
-
break;
|
|
206
|
-
default:
|
|
207
|
-
Logger_1.browserLogger.info(msg.text(), meta);
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
page.on('pageerror', (error) => {
|
|
212
|
-
Logger_1.browserLogger.error(error.message, {
|
|
213
|
-
stack: error.stack,
|
|
214
|
-
url: page.url(),
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
// Fallback timing: track request start times keyed by the Request object
|
|
218
|
-
// itself (via WeakMap) so concurrent requests to the same URL don't collide.
|
|
219
|
-
// Prefer Playwright's native timing API when it has data.
|
|
220
|
-
const requestStartTimes = new WeakMap();
|
|
221
|
-
page.on('request', (request) => {
|
|
222
|
-
requestStartTimes.set(request, Date.now());
|
|
223
|
-
});
|
|
224
|
-
const getDuration = (request) => {
|
|
225
|
-
const timing = request.timing();
|
|
226
|
-
if (timing.responseEnd >= 0) {
|
|
227
|
-
return Math.round(timing.responseEnd);
|
|
228
|
-
}
|
|
229
|
-
const startTime = requestStartTimes.get(request);
|
|
230
|
-
return startTime !== undefined ? Date.now() - startTime : undefined;
|
|
231
|
-
};
|
|
232
|
-
page.on('response', (response) => {
|
|
233
|
-
const request = response.request();
|
|
234
|
-
const status = response.status();
|
|
235
|
-
const duration = getDuration(request);
|
|
236
|
-
const meta = {
|
|
237
|
-
method: request.method(),
|
|
238
|
-
url: request.url(),
|
|
239
|
-
status,
|
|
240
|
-
duration,
|
|
241
|
-
resourceType: request.resourceType(),
|
|
242
|
-
};
|
|
243
|
-
if (status >= 500) {
|
|
244
|
-
Logger_1.networkLogger.error(request.url(), meta);
|
|
245
|
-
}
|
|
246
|
-
else if (status >= 400) {
|
|
247
|
-
Logger_1.networkLogger.warn(request.url(), meta);
|
|
248
|
-
}
|
|
249
|
-
else {
|
|
250
|
-
Logger_1.networkLogger.info(request.url(), meta);
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
page.on('requestfailed', (request) => {
|
|
254
|
-
const duration = getDuration(request);
|
|
255
|
-
Logger_1.networkLogger.error(`${request.url()} FAILED: ${request.failure()?.errorText}`, {
|
|
256
|
-
method: request.method(),
|
|
257
|
-
url: request.url(),
|
|
258
|
-
resourceType: request.resourceType(),
|
|
259
|
-
failureReason: request.failure()?.errorText,
|
|
260
|
-
duration,
|
|
261
|
-
});
|
|
262
|
-
});
|
|
189
|
+
(0, PageLogListeners_1.registerPageLogListeners)(page);
|
|
263
190
|
if (callbacks.metadata.runMode !== 'INSTRUCT') {
|
|
264
191
|
page.on('domcontentloaded', async () => {
|
|
265
192
|
await this._interactionVisualizer.showMouse(page);
|
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();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
/**
|
|
3
|
+
* Registers Playwright page event listeners that route browser console messages
|
|
4
|
+
* and network request/response data to the {@link browserLogger} and
|
|
5
|
+
* {@link networkLogger} Winston loggers. These loggers include a
|
|
6
|
+
* {@link FlowLogBufferTransport} that captures entries into the per-flow
|
|
7
|
+
* log buffer (when one is available via AsyncLocalStorage or the process-local
|
|
8
|
+
* fallback).
|
|
9
|
+
*
|
|
10
|
+
* This function is called from both {@link WebTargetInspector.handleNewPage}
|
|
11
|
+
* (Studio-launched flows) and the Playwright test extension fixture, ensuring
|
|
12
|
+
* identical logging behaviour across both runtime contexts.
|
|
13
|
+
*/
|
|
14
|
+
export declare function registerPageLogListeners(page: Page): void;
|
|
15
|
+
//# sourceMappingURL=PageLogListeners.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerPageLogListeners = registerPageLogListeners;
|
|
4
|
+
const Logger_1 = require("./Logger");
|
|
5
|
+
/**
|
|
6
|
+
* Registers Playwright page event listeners that route browser console messages
|
|
7
|
+
* and network request/response data to the {@link browserLogger} and
|
|
8
|
+
* {@link networkLogger} Winston loggers. These loggers include a
|
|
9
|
+
* {@link FlowLogBufferTransport} that captures entries into the per-flow
|
|
10
|
+
* log buffer (when one is available via AsyncLocalStorage or the process-local
|
|
11
|
+
* fallback).
|
|
12
|
+
*
|
|
13
|
+
* This function is called from both {@link WebTargetInspector.handleNewPage}
|
|
14
|
+
* (Studio-launched flows) and the Playwright test extension fixture, ensuring
|
|
15
|
+
* identical logging behaviour across both runtime contexts.
|
|
16
|
+
*/
|
|
17
|
+
function registerPageLogListeners(page) {
|
|
18
|
+
page.on('console', (msg) => {
|
|
19
|
+
const { url, lineNumber, columnNumber } = msg.location();
|
|
20
|
+
const hasSourceLocation = lineNumber !== 0 || columnNumber !== 0;
|
|
21
|
+
const meta = url
|
|
22
|
+
? hasSourceLocation
|
|
23
|
+
? { url, lineNumber, columnNumber }
|
|
24
|
+
: { url }
|
|
25
|
+
: undefined;
|
|
26
|
+
switch (msg.type()) {
|
|
27
|
+
case 'error':
|
|
28
|
+
Logger_1.browserLogger.error(msg.text(), meta);
|
|
29
|
+
break;
|
|
30
|
+
case 'warning':
|
|
31
|
+
Logger_1.browserLogger.warn(msg.text(), meta);
|
|
32
|
+
break;
|
|
33
|
+
case 'debug':
|
|
34
|
+
Logger_1.browserLogger.debug(msg.text(), meta);
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
Logger_1.browserLogger.info(msg.text(), meta);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
page.on('pageerror', (error) => {
|
|
42
|
+
Logger_1.browserLogger.error(error.message, {
|
|
43
|
+
stack: error.stack,
|
|
44
|
+
url: page.url(),
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
// Fallback timing: track request start times keyed by the Request object
|
|
48
|
+
// itself (via WeakMap) so concurrent requests to the same URL don't collide.
|
|
49
|
+
// Prefer Playwright's native timing API when it has data.
|
|
50
|
+
const requestStartTimes = new WeakMap();
|
|
51
|
+
page.on('request', (request) => {
|
|
52
|
+
requestStartTimes.set(request, Date.now());
|
|
53
|
+
});
|
|
54
|
+
const getDuration = (request) => {
|
|
55
|
+
const timing = request.timing();
|
|
56
|
+
if (timing.responseEnd >= 0) {
|
|
57
|
+
return Math.round(timing.responseEnd);
|
|
58
|
+
}
|
|
59
|
+
const startTime = requestStartTimes.get(request);
|
|
60
|
+
return startTime !== undefined ? Date.now() - startTime : undefined;
|
|
61
|
+
};
|
|
62
|
+
page.on('response', (response) => {
|
|
63
|
+
const request = response.request();
|
|
64
|
+
const status = response.status();
|
|
65
|
+
const duration = getDuration(request);
|
|
66
|
+
const meta = {
|
|
67
|
+
method: request.method(),
|
|
68
|
+
url: request.url(),
|
|
69
|
+
status,
|
|
70
|
+
duration,
|
|
71
|
+
resourceType: request.resourceType(),
|
|
72
|
+
};
|
|
73
|
+
if (status >= 500) {
|
|
74
|
+
Logger_1.networkLogger.error(request.url(), meta);
|
|
75
|
+
}
|
|
76
|
+
else if (status >= 400) {
|
|
77
|
+
Logger_1.networkLogger.warn(request.url(), meta);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
Logger_1.networkLogger.info(request.url(), meta);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
page.on('requestfailed', (request) => {
|
|
84
|
+
const duration = getDuration(request);
|
|
85
|
+
Logger_1.networkLogger.error(`${request.url()} FAILED: ${request.failure()?.errorText}`, {
|
|
86
|
+
method: request.method(),
|
|
87
|
+
url: request.url(),
|
|
88
|
+
resourceType: request.resourceType(),
|
|
89
|
+
failureReason: request.failure()?.errorText,
|
|
90
|
+
duration,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=PageLogListeners.js.map
|