automation_model 1.0.771-dev → 1.0.771-stage

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/lib/network.js CHANGED
@@ -2,6 +2,61 @@ import path from "path";
2
2
  import fs from "fs";
3
3
  import { _stepNameToTemplate } from "./route.js";
4
4
  import crypto from "crypto";
5
+ import { tmpdir } from "os";
6
+ import createDebug from "debug";
7
+ const debug = createDebug("automation_model:network");
8
+ class SaveQueue {
9
+ queue = [];
10
+ isProcessing = false;
11
+ maxRetries = 3;
12
+ async enqueueSave(current) {
13
+ this.queue.push({ current, retryCount: 0 });
14
+ if (!this.isProcessing) {
15
+ await this.processQueue();
16
+ }
17
+ }
18
+ async processQueue() {
19
+ this.isProcessing = true;
20
+ while (this.queue.length > 0) {
21
+ const item = this.queue.shift();
22
+ try {
23
+ await this.saveSafely(item.current);
24
+ }
25
+ catch (error) {
26
+ console.error(`Save failed for ${item.current ? "current" : "previous"} step:`, error);
27
+ // Retry logic
28
+ if (item.retryCount < this.maxRetries) {
29
+ item.retryCount++;
30
+ this.queue.unshift(item); // Put it back at the front
31
+ await new Promise((resolve) => setTimeout(resolve, 100 * item.retryCount)); // Exponential backoff
32
+ }
33
+ }
34
+ }
35
+ this.isProcessing = false;
36
+ }
37
+ async saveSafely(current) {
38
+ const stepHash = current ? executionState.currentStepHash : executionState.previousStepHash;
39
+ if (!stepHash)
40
+ return;
41
+ const file = path.join(detailedNetworkFolder, `${stepHash}.json`);
42
+ const entries = current
43
+ ? Array.from(executionState.liveRequestsMap.values())
44
+ : Array.from(executionState.liveRequestsMapPrevious.values());
45
+ // Ensure all entries are JSON-serializable
46
+ const validEntries = entries.filter((entry) => {
47
+ try {
48
+ JSON.stringify(entry);
49
+ return true;
50
+ }
51
+ catch {
52
+ console.warn(`Skipping non-serializable entry: ${entry.requestId}`);
53
+ return false;
54
+ }
55
+ });
56
+ const jsonString = JSON.stringify(validEntries, null, 2);
57
+ await fs.promises.writeFile(file, jsonString, "utf8");
58
+ }
59
+ }
5
60
  function _getNetworkFile(world = null, web = null, context = null) {
6
61
  let networkFile = null;
7
62
  if (world && world.reportFolder) {
@@ -45,17 +100,22 @@ function registerDownloadEvent(page, world, context) {
45
100
  }
46
101
  }
47
102
  function registerNetworkEvents(world, web, context, page) {
103
+ // Map to hold request start times and IDs
48
104
  const networkFile = _getNetworkFile(world, web, context);
49
105
  function saveNetworkData() {
50
106
  if (context && context.networkData) {
51
- fs.writeFileSync(networkFile, JSON.stringify(context.networkData, null, 2), "utf8");
107
+ try {
108
+ fs.writeFileSync(networkFile, JSON.stringify(context.networkData, null, 2), "utf8");
109
+ }
110
+ catch (error) {
111
+ console.error("Error saving network data:", error);
112
+ }
52
113
  }
53
114
  }
54
115
  if (!context) {
55
116
  console.error("No context found to register network events");
56
117
  return;
57
118
  }
58
- // Map to hold request start times and IDs
59
119
  const requestTimes = new Map();
60
120
  let requestIdCounter = 0;
61
121
  if (page) {
@@ -64,118 +124,144 @@ function registerNetworkEvents(world, web, context, page) {
64
124
  const networkData = context.networkData;
65
125
  // Event listener for when a request is made
66
126
  page.on("request", (request) => {
67
- const requestId = requestIdCounter++;
68
- request.requestId = requestId; // Assign a unique ID to the request
69
- handleRequest(request);
70
- const startTime = Date.now();
71
- requestTimes.set(requestId, startTime);
72
- // Initialize data for this request
73
- networkData.push({
74
- requestId,
75
- requestStart: startTime,
76
- requestUrl: request.url(),
77
- method: request.method(),
78
- status: "Pending",
79
- responseTime: null,
80
- responseReceived: null,
81
- responseEnd: null,
82
- size: null,
83
- });
84
- saveNetworkData();
85
- });
86
- // Event listener for when a response is received
87
- page.on("response", async (response) => {
88
- const request = response.request();
89
- const requestId = request.requestId;
90
- const receivedTime = Date.now();
91
- // Find the corresponding data object
92
- const data = networkData.find((item) => item.requestId === requestId);
93
- if (data) {
94
- data.status = response.status();
95
- data.responseReceived = receivedTime;
127
+ try {
128
+ // console.log("Request started:", request.url());
129
+ const requestId = requestIdCounter++;
130
+ request.requestId = requestId; // Assign a unique ID to the request
131
+ handleRequest(request, context);
132
+ const startTime = Date.now();
133
+ requestTimes.set(requestId, startTime);
134
+ // Initialize data for this request
135
+ networkData.push({
136
+ requestId,
137
+ requestStart: startTime,
138
+ requestUrl: request.url(),
139
+ method: request.method(),
140
+ status: "Pending",
141
+ responseTime: null,
142
+ responseReceived: null,
143
+ responseEnd: null,
144
+ size: null,
145
+ });
96
146
  saveNetworkData();
97
147
  }
98
- else {
99
- console.error("No data found for request ID", requestId);
148
+ catch (error) {
149
+ // console.error("Error handling request:", error);
100
150
  }
101
151
  });
102
- // Event listener for when a request is finished
103
- page.on("requestfinished", async (request) => {
104
- const requestId = request.requestId;
105
- const endTime = Date.now();
106
- const startTime = requestTimes.get(requestId);
107
- await handleRequestFinishedOrFailed(request, false);
108
- const response = await request.response();
109
- const timing = request.timing();
110
- // Find the corresponding data object
111
- const data = networkData.find((item) => item.requestId === requestId);
112
- if (data) {
113
- data.responseEnd = endTime;
114
- data.responseTime = endTime - startTime;
115
- // Get response size
116
- try {
117
- const body = await response.body();
118
- data.size = body.length;
119
- }
120
- catch (e) {
121
- data.size = 0;
152
+ // Event listener for when a response is received
153
+ page.on("response", async (response) => {
154
+ try {
155
+ const request = response.request();
156
+ const requestId = request.requestId;
157
+ const receivedTime = Date.now();
158
+ // await handleRequestFinishedOrFailed(request, false);
159
+ // Find the corresponding data object
160
+ const data = networkData.find((item) => item.requestId === requestId);
161
+ if (data) {
162
+ data.status = response.status();
163
+ data.responseReceived = receivedTime;
164
+ saveNetworkData();
122
165
  }
123
- const type = request.resourceType();
124
- /*
125
- domainLookupStart: 80.655,
126
- domainLookupEnd: 80.668,
127
- connectStart: 80.668,
128
- secureConnectionStart: 106.688,
129
- connectEnd: 129.69,
130
- requestStart: 129.81,
131
- responseStart: 187.006,
132
- responseEnd: 188.209
133
- */
134
- data.type = type;
135
- data.domainLookupStart = timing.domainLookupStart;
136
- data.domainLookupEnd = timing.domainLookupEnd;
137
- data.connectStart = timing.connectStart;
138
- data.secureConnectionStart = timing.secureConnectionStart;
139
- data.connectEnd = timing.connectEnd;
140
- data.requestStart = timing.requestStart;
141
- data.responseStart = timing.responseStart;
142
- data.responseEnd = timing.responseEnd;
143
- saveNetworkData();
144
- if (world && world.attach) {
145
- world.attach(JSON.stringify(data), { mediaType: "application/json+network" });
166
+ else {
167
+ // console.error("No data found for request ID", requestId);
146
168
  }
147
169
  }
148
- else {
149
- console.error("No data found for request ID", requestId);
170
+ catch (error) {
171
+ // console.error("Error handling response:", error);
150
172
  }
151
173
  });
152
- // Event listener for when a request fails
153
- page.on("requestfailed", async (request) => {
154
- const requestId = request.requestId;
155
- const endTime = Date.now();
156
- const startTime = requestTimes.get(requestId);
157
- await handleRequestFinishedOrFailed(request, true);
174
+ // Event listener for when a request is finished
175
+ page.on("requestfinished", async (request) => {
158
176
  try {
159
- const res = await request.response();
160
- const statusCode = res ? res.status() : request.failure().errorText;
177
+ const requestId = request.requestId;
178
+ const endTime = Date.now();
179
+ const startTime = requestTimes.get(requestId);
180
+ await handleRequestFinishedOrFailed(request, false, context);
181
+ const response = await request.response();
182
+ const timing = request.timing();
161
183
  // Find the corresponding data object
162
184
  const data = networkData.find((item) => item.requestId === requestId);
163
185
  if (data) {
164
186
  data.responseEnd = endTime;
165
187
  data.responseTime = endTime - startTime;
166
- data.status = statusCode;
167
- data.size = 0;
188
+ // Get response size
189
+ try {
190
+ let size = 0;
191
+ if (responseHasBody(response)) {
192
+ const buf = await response.body();
193
+ size = buf?.length ?? 0;
194
+ }
195
+ data.size = size;
196
+ }
197
+ catch {
198
+ data.size = 0;
199
+ }
200
+ const type = request.resourceType();
201
+ /*
202
+ domainLookupStart: 80.655,
203
+ domainLookupEnd: 80.668,
204
+ connectStart: 80.668,
205
+ secureConnectionStart: 106.688,
206
+ connectEnd: 129.69,
207
+ requestStart: 129.81,
208
+ responseStart: 187.006,
209
+ responseEnd: 188.209
210
+ */
211
+ data.type = type;
212
+ data.domainLookupStart = timing.domainLookupStart;
213
+ data.domainLookupEnd = timing.domainLookupEnd;
214
+ data.connectStart = timing.connectStart;
215
+ data.secureConnectionStart = timing.secureConnectionStart;
216
+ data.connectEnd = timing.connectEnd;
217
+ data.requestStart = timing.requestStart;
218
+ data.responseStart = timing.responseStart;
219
+ data.responseEnd = timing.responseEnd;
168
220
  saveNetworkData();
169
221
  if (world && world.attach) {
170
222
  world.attach(JSON.stringify(data), { mediaType: "application/json+network" });
171
223
  }
172
224
  }
173
225
  else {
174
- console.error("No data found for request ID", requestId);
226
+ // console.error("No data found for request ID", requestId);
175
227
  }
176
228
  }
177
229
  catch (error) {
178
- // ignore
230
+ // console.error("Error handling request finished:", error);
231
+ }
232
+ });
233
+ // Event listener for when a request fails
234
+ page.on("requestfailed", async (request) => {
235
+ try {
236
+ const requestId = request.requestId;
237
+ const endTime = Date.now();
238
+ const startTime = requestTimes.get(requestId);
239
+ await handleRequestFinishedOrFailed(request, true, context);
240
+ try {
241
+ const res = await request.response();
242
+ const statusCode = res ? res.status() : request.failure().errorText;
243
+ // Find the corresponding data object
244
+ const data = networkData.find((item) => item.requestId === requestId);
245
+ if (data) {
246
+ data.responseEnd = endTime;
247
+ data.responseTime = endTime - startTime;
248
+ data.status = statusCode;
249
+ data.size = 0;
250
+ saveNetworkData();
251
+ if (world && world.attach) {
252
+ world.attach(JSON.stringify(data), { mediaType: "application/json+network" });
253
+ }
254
+ }
255
+ else {
256
+ // console.error("No data found for request ID", requestId);
257
+ }
258
+ }
259
+ catch (error) {
260
+ // ignore
261
+ }
262
+ }
263
+ catch (error) {
264
+ // console.error("Error handling request failed:", error);
179
265
  }
180
266
  });
181
267
  }
@@ -184,133 +270,225 @@ function registerNetworkEvents(world, web, context, page) {
184
270
  console.error("No page found to register network events");
185
271
  }
186
272
  }
187
- const storeDetailedNetworkData = process.env.STORE_DETAILED_NETWORK_DATA === "true";
188
- const detailedNetworkFolder = "temp/detailed_network_data";
273
+ async function appendEntryToStepFile(stepHash, entry) {
274
+ if (!stepHash)
275
+ return;
276
+ const debug = createDebug("network:appendEntryToStepFile");
277
+ const file = path.join(detailedNetworkFolder, `${stepHash}.json`);
278
+ debug("appending to step file:", file);
279
+ let data = [];
280
+ try {
281
+ /* read if it already exists */
282
+ const txt = await fs.promises.readFile(file, "utf8");
283
+ data = JSON.parse(txt);
284
+ }
285
+ catch {
286
+ /* ignore – file does not exist or cannot be parsed */
287
+ }
288
+ data.push(entry);
289
+ try {
290
+ debug("writing to step file:", file);
291
+ await fs.promises.writeFile(file, JSON.stringify(data, null, 2), "utf8");
292
+ }
293
+ catch (error) {
294
+ debug("Error writing to step file:", error);
295
+ }
296
+ }
297
+ const detailedNetworkFolder = path.join(tmpdir(), "blinq_network_events");
298
+ let outOfStep = true;
299
+ let timeoutId = null;
189
300
  const executionState = {
190
301
  currentStepHash: null,
302
+ previousStepHash: null,
191
303
  liveRequestsMap: new Map(),
304
+ liveRequestsMapPrevious: new Map(),
192
305
  };
193
- export function networkBeforeStep(stepName) {
194
- if (!storeDetailedNetworkData) {
306
+ const storeDetailedNetworkData = (context) => context && context.STORE_DETAILED_NETWORK_DATA === true;
307
+ export function networkBeforeStep(stepName, context) {
308
+ if (timeoutId) {
309
+ clearTimeout(timeoutId);
310
+ timeoutId = null;
311
+ }
312
+ outOfStep = false;
313
+ if (!storeDetailedNetworkData(context)) {
195
314
  return;
196
315
  }
197
316
  // check if the folder exists, if not create it
198
317
  if (!fs.existsSync(detailedNetworkFolder)) {
199
318
  fs.mkdirSync(detailedNetworkFolder, { recursive: true });
200
319
  }
201
- const stepHash = stepNameToHash(stepName);
320
+ // const stepHash = stepNameToHash(stepName);
321
+ let stepHash = "";
322
+ executionState.liveRequestsMapPrevious = executionState.liveRequestsMap;
323
+ executionState.liveRequestsMap = new Map();
324
+ stepHash = stepNameToHash(stepName);
325
+ executionState.previousStepHash = executionState.currentStepHash; // ➊ NEW
326
+ executionState.currentStepHash = stepHash;
202
327
  // check if the file exists, if exists delete it
203
328
  const networkFile = path.join(detailedNetworkFolder, `${stepHash}.json`);
204
- if (fs.existsSync(networkFile)) {
205
- fs.unlinkSync(networkFile);
329
+ try {
330
+ fs.rmSync(path.join(networkFile), { force: true });
206
331
  }
207
- executionState.currentStepHash = stepHash;
332
+ catch (err) {
333
+ // Ignore error if file does not exist
334
+ }
335
+ }
336
+ const saveQueue = new SaveQueue();
337
+ async function saveMap(current) {
338
+ await saveQueue.enqueueSave(current);
208
339
  }
209
- export function networkAfterStep(stepName) {
210
- if (!storeDetailedNetworkData) {
340
+ export async function networkAfterStep(stepName, context) {
341
+ if (!storeDetailedNetworkData(context)) {
211
342
  return;
212
343
  }
213
- executionState.currentStepHash = null;
344
+ //await new Promise((r) => setTimeout(r, 1000));
345
+ await saveMap(true);
346
+ /* reset for next step */
347
+ //executionState.previousStepHash = executionState.currentStepHash; // ➋ NEW
348
+ //executionState.liveRequestsMap.clear();
349
+ outOfStep = true;
350
+ // set a timer of 60 seconds to the outOfStep, after that it will be set to false so no network collection will happen
351
+ timeoutId = setTimeout(() => {
352
+ outOfStep = false;
353
+ context.STORE_DETAILED_NETWORK_DATA = false;
354
+ }, 60000);
214
355
  }
215
356
  function stepNameToHash(stepName) {
216
357
  const templateName = _stepNameToTemplate(stepName);
217
358
  // create hash from the template name
218
- return crypto.createHash("sha256").update(stepName).digest("hex");
359
+ return crypto.createHash("sha256").update(templateName).digest("hex");
219
360
  }
220
- function handleRequest(request) {
221
- if (!storeDetailedNetworkData || !executionState.currentStepHash) {
361
+ function handleRequest(request, context) {
362
+ const debug = createDebug("automation_model:network:handleRequest");
363
+ if (!storeDetailedNetworkData(context))
222
364
  return;
223
- }
224
- const requestId = request.requestId;
225
- const requestData = {
226
- requestId,
365
+ const entry = {
366
+ requestId: request.requestId,
227
367
  url: request.url(),
228
368
  method: request.method(),
229
369
  headers: request.headers(),
230
370
  postData: request.postData(),
231
- timestamp: Date.now(),
371
+ requestTimestamp: Date.now(),
232
372
  stepHash: executionState.currentStepHash,
233
373
  };
234
- executionState.liveRequestsMap.set(request, requestData);
374
+ executionState.liveRequestsMap.set(request, entry);
375
+ debug("Request to", request.url(), "with", request.requestId, "added to current step map at", Date.now());
235
376
  }
236
- function saveNetworkDataToFile(requestData) {
237
- if (!storeDetailedNetworkData) {
377
+ async function handleRequestFinishedOrFailed(request, failed, context) {
378
+ const debug = createDebug("automation_model:network:handleRequestFinishedOrFailed");
379
+ if (!storeDetailedNetworkData(context))
238
380
  return;
239
- }
240
- const networkFile = path.join(detailedNetworkFolder, `${requestData.stepHash}.json`);
241
- // read the existing data if it exists (should be an array)
242
- let existingData = [];
243
- if (fs.existsSync(networkFile)) {
244
- const data = fs.readFileSync(networkFile, "utf8");
245
- try {
246
- existingData = JSON.parse(data);
247
- }
248
- catch (e) {
249
- console.error("Failed to parse existing network data:", e);
381
+ const requestId = request.requestId;
382
+ debug("Request id in handleRequestFinishedOrFailed:", requestId, "at", Date.now());
383
+ // const response = await request.response(); // This may be null if the request failed
384
+ let entry = executionState.liveRequestsMap.get(request);
385
+ debug("Request entry found in current map:", entry?.requestId || false);
386
+ if (!entry) {
387
+ // check if the request is in the previous step's map
388
+ entry = executionState.liveRequestsMapPrevious.get(request);
389
+ debug("Request entry found in previous map:", entry?.requestId || false);
390
+ if (!entry) {
391
+ debug("No entry, creating fallback! for url:", request.url());
392
+ entry = {
393
+ requestId: request.requestId,
394
+ url: request.url(),
395
+ method: request.method?.() ?? "GET",
396
+ headers: request.headers?.() ?? {},
397
+ postData: request.postData?.() ?? undefined,
398
+ stepHash: executionState.previousStepHash ?? "unknown",
399
+ requestTimestamp: Date.now(),
400
+ };
250
401
  }
251
402
  }
252
- // Add the live requests to the existing data
253
- existingData.push(requestData);
254
- // Save the updated data back to the file
255
- fs.writeFileSync(networkFile, JSON.stringify(existingData, null, 2), "utf8");
256
- }
257
- async function handleRequestFinishedOrFailed(request, failed) {
258
- if (!storeDetailedNetworkData) {
259
- return;
260
- }
261
- const response = await request.response(); // This may be null if the request failed
262
- const requestData = executionState.liveRequestsMap.get(request);
263
- if (!requestData) {
264
- //console.warn("No request data found for request", request);
265
- return;
266
- }
267
403
  // Remove the request from the live requests map
268
- executionState.liveRequestsMap.delete(request);
269
- if (failed || !response) {
404
+ let respData;
405
+ // executionState.liveRequestsMap.delete(request);
406
+ if (failed) {
270
407
  // Handle failed request
271
- requestData.response = {
408
+ respData = {
272
409
  status: null,
273
410
  headers: {},
274
- timing: null,
275
411
  url: request.url(),
276
412
  timestamp: Date.now(),
277
413
  body: null,
278
414
  contentType: null,
279
415
  error: "Request failed",
280
416
  };
281
- saveNetworkDataToFile(requestData);
282
- return;
283
417
  }
284
- // Handle successful request with a response
285
- const headers = response.headers();
286
- const contentType = headers["content-type"] || "";
287
- let body = null;
288
- try {
289
- if (contentType.includes("application/json")) {
290
- const text = await response.text();
291
- body = JSON.parse(text);
418
+ else {
419
+ const response = await request.response();
420
+ const headers = response?.headers?.() || {};
421
+ let contentType = headers["content-type"] || null;
422
+ let body = null;
423
+ try {
424
+ if (responseHasBody(response)) {
425
+ if (contentType && contentType.includes("application/json")) {
426
+ body = JSON.parse(await response.text());
427
+ }
428
+ else if ((contentType && contentType.includes("text")) ||
429
+ (contentType && contentType.includes("application/csv"))) {
430
+ body = await response.text();
431
+ if (contentType.includes("application/csv"))
432
+ contentType = "text/csv";
433
+ }
434
+ else {
435
+ // If you want binary, you could read it here—but only when responseHasBody(response) is true
436
+ // const buffer = await response.body();
437
+ // body = buffer.toString("base64");
438
+ }
439
+ }
440
+ else {
441
+ // For redirects / no-body statuses, it's useful to keep redirect info
442
+ // e.g., include Location header if present
443
+ // body stays null
444
+ }
292
445
  }
293
- else if (contentType.includes("text")) {
446
+ catch (err) {
447
+ console.error("Error reading response body:", err);
294
448
  body = await response.text();
295
449
  }
296
- else {
297
- // Optionally handle binary here
298
- // const buffer = await response.body();
299
- // body = buffer.toString("base64"); // if you want to store binary safely
450
+ respData = {
451
+ status: response.status(),
452
+ headers,
453
+ url: response.url(),
454
+ timestamp: Date.now(),
455
+ body,
456
+ contentType,
457
+ };
458
+ }
459
+ if (executionState.liveRequestsMap.has(request)) {
460
+ /* “normal” path – keep it in the buffer */
461
+ entry.response = respData;
462
+ if (outOfStep && executionState.currentStepHash) {
463
+ await saveMap(true);
300
464
  }
301
465
  }
302
- catch (err) {
303
- console.error("Error reading response body:", err);
466
+ else {
467
+ if (executionState.liveRequestsMapPrevious.has(request)) {
468
+ entry.response = respData;
469
+ await saveMap(false);
470
+ }
471
+ else {
472
+ /* orphan response – append directly to the previous step file */
473
+ entry.response = respData;
474
+ await appendEntryToStepFile(entry.stepHash, entry); // ➍ NEW
475
+ }
304
476
  }
305
- requestData.response = {
306
- status: response.status(),
307
- headers,
308
- url: response.url(),
309
- timestamp: Date.now(),
310
- body,
311
- contentType,
312
- };
313
- saveNetworkDataToFile(requestData);
477
+ entry.response = respData;
478
+ return;
479
+ }
480
+ function responseHasBody(response) {
481
+ if (!response)
482
+ return false;
483
+ const s = response.status?.() ?? 0;
484
+ // RFC 7231: 1xx, 204, 205, 304 have no body. Playwright: 3xx (redirect) body is unavailable.
485
+ if ((s >= 100 && s < 200) || s === 204 || s === 205 || s === 304 || (s >= 300 && s < 400))
486
+ return false;
487
+ // HEAD responses have no body by definition
488
+ const method = response.request?.().method?.() ?? "GET";
489
+ if (method === "HEAD")
490
+ return false;
491
+ return true;
314
492
  }
315
493
  export { registerNetworkEvents, registerDownloadEvent };
316
494
  //# sourceMappingURL=network.js.map