@dev-blinq/cucumber_client 1.0.1475-dev → 1.0.1475-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.
Files changed (47) hide show
  1. package/bin/assets/bundled_scripts/recorder.js +49 -49
  2. package/bin/assets/scripts/recorder.js +87 -34
  3. package/bin/assets/scripts/snapshot_capturer.js +10 -17
  4. package/bin/assets/scripts/unique_locators.js +78 -28
  5. package/bin/assets/templates/_hooks_template.txt +6 -2
  6. package/bin/assets/templates/utils_template.txt +16 -16
  7. package/bin/client/code_cleanup/codemod/find_harcoded_locators.js +173 -0
  8. package/bin/client/code_cleanup/codemod/fix_hardcoded_locators.js +247 -0
  9. package/bin/client/code_cleanup/utils.js +16 -7
  10. package/bin/client/code_gen/code_inversion.js +125 -1
  11. package/bin/client/code_gen/duplication_analysis.js +2 -1
  12. package/bin/client/code_gen/function_signature.js +8 -0
  13. package/bin/client/code_gen/index.js +4 -0
  14. package/bin/client/code_gen/page_reflection.js +90 -9
  15. package/bin/client/code_gen/playwright_codeget.js +173 -77
  16. package/bin/client/codemod/find_harcoded_locators.js +173 -0
  17. package/bin/client/codemod/fix_hardcoded_locators.js +247 -0
  18. package/bin/client/codemod/index.js +8 -0
  19. package/bin/client/codemod/locators_array/find_misstructured_elements.js +148 -0
  20. package/bin/client/codemod/locators_array/fix_misstructured_elements.js +144 -0
  21. package/bin/client/codemod/locators_array/index.js +114 -0
  22. package/bin/client/codemod/types.js +1 -0
  23. package/bin/client/cucumber/feature.js +4 -17
  24. package/bin/client/cucumber/steps_definitions.js +17 -12
  25. package/bin/client/recorderv3/bvt_init.js +310 -0
  26. package/bin/client/recorderv3/bvt_recorder.js +1560 -1183
  27. package/bin/client/recorderv3/constants.js +45 -0
  28. package/bin/client/recorderv3/implemented_steps.js +2 -0
  29. package/bin/client/recorderv3/index.js +3 -293
  30. package/bin/client/recorderv3/mixpanel.js +39 -0
  31. package/bin/client/recorderv3/services.js +839 -142
  32. package/bin/client/recorderv3/step_runner.js +36 -7
  33. package/bin/client/recorderv3/step_utils.js +316 -98
  34. package/bin/client/recorderv3/update_feature.js +85 -37
  35. package/bin/client/recorderv3/utils.js +80 -0
  36. package/bin/client/recorderv3/wbr_entry.js +61 -0
  37. package/bin/client/recording.js +1 -0
  38. package/bin/client/types/locators.js +2 -0
  39. package/bin/client/upload-service.js +2 -0
  40. package/bin/client/utils/app_dir.js +21 -0
  41. package/bin/client/utils/socket_logger.js +100 -125
  42. package/bin/index.js +5 -0
  43. package/package.json +21 -6
  44. package/bin/client/recorderv3/app_dir.js +0 -23
  45. package/bin/client/recorderv3/network.js +0 -299
  46. package/bin/client/recorderv3/scriptTest.js +0 -5
  47. package/bin/client/recorderv3/ws_server.js +0 -72
@@ -1,8 +1,9 @@
1
- import { existsSync, readFileSync, writeFileSync } from "fs";
2
- import path from "path";
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
3
  import { getDefaultPrettierConfig } from "../code_cleanup/utils.js";
4
4
  import prettier from "prettier";
5
- export function containsScenario({ featureFileContent, scenarioName }) {
5
+
6
+ function containsScenario({ featureFileContent, scenarioName }) {
6
7
  const lines = featureFileContent.split("\n");
7
8
  for (const line of lines) {
8
9
  const trimmedLine = line.trim();
@@ -14,7 +15,7 @@ export function containsScenario({ featureFileContent, scenarioName }) {
14
15
  }
15
16
  }
16
17
 
17
- export function testStringForRegex(text) {
18
+ function testStringForRegex(text) {
18
19
  const regexEndPattern = /\/([gimuy]*)$/;
19
20
  if (text.startsWith("/")) {
20
21
  const match = regexEndPattern.test(text);
@@ -32,6 +33,7 @@ export function testStringForRegex(text) {
32
33
  }
33
34
 
34
35
  const escapeNonPrintables = (text) => {
36
+ if (typeof text !== "string") return text.toString();
35
37
  const t = text.replace(/\n/g, "\\n").replace(/\t/g, "\\t"); // .replace(/\|/g, "\\|");
36
38
  let result = "";
37
39
  // replace \ with \\ and | with \|
@@ -61,7 +63,8 @@ const escapeNonPrintables = (text) => {
61
63
  }
62
64
  return result;
63
65
  };
64
- export function getCommandContent(command) {
66
+
67
+ function getCommandContent(command) {
65
68
  switch (command.type) {
66
69
  case "click_element": {
67
70
  return `${command.count === 2 ? "Double click" : "Click"} on ${escapeNonPrintables(command.element.name)}`;
@@ -108,7 +111,18 @@ export function getCommandContent(command) {
108
111
  }
109
112
  }
110
113
 
111
- export function getExamplesContent(parametersMap) {
114
+ function getExamplesContent(parametersMap, datasets) {
115
+ if (datasets && datasets.length > 0) {
116
+ let result = "";
117
+ const keys = Object.keys(parametersMap);
118
+ result += "\t\tExamples:\n";
119
+
120
+ result += `\t\t| ${keys.join(" | ")} |\n`;
121
+ for (const dataset of datasets) {
122
+ result += `\t\t| ${dataset.data.map((d) => escapeNonPrintables(d.value)).join(" | ")} |\n`;
123
+ }
124
+ return result;
125
+ }
112
126
  const keys = Object.keys(parametersMap);
113
127
  const l = keys.length;
114
128
  let result = "\t\tExamples:\n";
@@ -144,19 +158,19 @@ function getTagsContent(scenario, featureFileObject) {
144
158
  return tagsContent;
145
159
  }
146
160
 
147
- export function getScenarioContent({ scenario }, featureFileObject) {
161
+ function getScenarioContent({ scenario }, featureFileObject) {
148
162
  const prametersMap = scenario.parametersMap ?? {};
149
163
  const isParmatersMapEmpty = Object.keys(prametersMap).length === 0;
150
164
  let scenarioContent = isParmatersMapEmpty ? `Scenario: ${scenario.name}\n` : `Scenario Outline: ${scenario.name}\n`;
151
165
 
152
- let tagsContent = getTagsContent(scenario, featureFileObject);
166
+ const tagsContent = getTagsContent(scenario, featureFileObject);
153
167
 
154
- scenarioContent = "\t" + tagsContent + "\n" + scenarioContent;
168
+ scenarioContent = `\t${tagsContent}\n${scenarioContent}`;
155
169
 
156
170
  for (const step of scenario.steps) {
157
171
  const commands = step.commands ?? [];
158
- let commentContents = commands.map(getCommandContent).map(escapeNonPrintables);
159
- let commentContent = commentContents.filter((content) => content.length > 0).join(", ");
172
+ const commentContents = commands.map(getCommandContent).map(escapeNonPrintables);
173
+ const commentContent = commentContents.filter((content) => content.length > 0).join(", ");
160
174
 
161
175
  if (commentContent) {
162
176
  scenarioContent += `\t# ${commentContent}\n`;
@@ -164,18 +178,19 @@ export function getScenarioContent({ scenario }, featureFileObject) {
164
178
  scenarioContent += `\t${step.keyword} ${escapeString(step.text)}\n`;
165
179
  }
166
180
  if (!isParmatersMapEmpty) {
167
- scenarioContent += getExamplesContent(prametersMap);
181
+ scenarioContent += getExamplesContent(prametersMap, scenario.datasets);
168
182
  }
169
183
  return scenarioContent;
170
184
  }
171
- export const escapeString = (str) => {
185
+
186
+ function escapeString(str) {
172
187
  // Step 1: Replace all literal newline characters with a temporary placeholder
173
188
  const placeholder = "\\n";
174
189
  str = str.replace(/\n/g, placeholder);
175
190
  return str;
176
- };
191
+ }
177
192
 
178
- const GherkinToObject = (gherkin) => {
193
+ function GherkinToObject(gherkin) {
179
194
  const obj = {
180
195
  featureName: "",
181
196
  scenarios: [],
@@ -211,7 +226,7 @@ const GherkinToObject = (gherkin) => {
211
226
  }
212
227
 
213
228
  const getScenario = () => {
214
- let scenario = {
229
+ const scenario = {
215
230
  name: "",
216
231
  tags: [],
217
232
  steps: [],
@@ -235,7 +250,7 @@ const GherkinToObject = (gherkin) => {
235
250
  skipEmptyLines();
236
251
 
237
252
  if (idx >= lines.length) {
238
- return -1;
253
+ return scenario;
239
254
  }
240
255
 
241
256
  while (
@@ -288,29 +303,50 @@ const GherkinToObject = (gherkin) => {
288
303
  skipEmptyLines();
289
304
  }
290
305
  return obj;
291
- };
306
+ }
292
307
 
293
308
  // remove lines starting with "Scenario:" or "Scenario Outline:" that contain the scenario name until the next scenario and then append the new scenario content
294
309
  // assumes that there are no multiple scenarios with the same name and having comments and tags in each scenario
295
- function updateExistingScenario({ featureFileContent, scenarioName, scenarioContent }) {
310
+ function updateExistingScenario({
311
+ featureFileContent,
312
+ scenarioName: newScenarioName,
313
+ scenarioContent: newScenarioContent,
314
+ }) {
296
315
  const featureFileObject = GherkinToObject(featureFileContent);
297
316
  if (featureFileObject.error) return "error";
317
+
298
318
  const results = [];
299
- let skipScenarioIndex = -1;
300
319
  results.push(`Feature: ${featureFileObject.featureName}`);
320
+
321
+ let indexOfScenarioToUpdate = -1;
322
+
323
+ for (let i = 0; i < featureFileObject.scenarios.length; i++) {
324
+ if (featureFileObject.scenarios[i].name === newScenarioName) {
325
+ indexOfScenarioToUpdate = i;
326
+ break;
327
+ }
328
+ }
329
+
301
330
  for (let i = 0; i < featureFileObject.scenarios.length; i++) {
302
331
  const scenario = featureFileObject.scenarios[i];
303
- if (scenario.name === scenarioName) {
304
- skipScenarioIndex = i;
332
+
333
+ if (i === indexOfScenarioToUpdate) {
334
+ results.push("");
335
+ results.push(newScenarioContent.trim());
336
+ results.push("");
305
337
  continue;
306
338
  }
307
- let scenarioContent = `${scenario.hasParams ? "Scenario Outline" : "Scenario"}: ${scenario.name}`;
339
+
340
+ const scenarioHeader = `${scenario.hasParams ? "Scenario Outline" : "Scenario"}: ${scenario.name}`;
308
341
  let tagsLine;
342
+
309
343
  if (scenario.tags?.length > 0) {
310
- tagsLine = `${scenario.tags.map((t) => `@${t}`).join(" ")}`;
344
+ tagsLine = scenario.tags.map((t) => `@${t}`).join(" ");
311
345
  }
312
- if (tagsLine) results.push("\t" + tagsLine);
313
- results.push(`\t${scenarioContent}`);
346
+
347
+ if (tagsLine) results.push(`\t${tagsLine}`);
348
+ results.push(`\t${scenarioHeader}`);
349
+
314
350
  for (const step of scenario.steps) {
315
351
  if (step.type === "examples") {
316
352
  results.push(`\t\tExamples:`);
@@ -323,14 +359,12 @@ function updateExistingScenario({ featureFileContent, scenarioName, scenarioCont
323
359
  }
324
360
  results.push("");
325
361
  }
326
- let finalContent = results.join("\n");
327
- if (skipScenarioIndex !== -1) {
328
- finalContent = results.join("\n") + "\n" + scenarioContent;
329
- }
330
- return finalContent;
362
+
363
+ return results.join("\n");
331
364
  }
332
- export async function updateFeatureFile({ featureName, scenario, override, projectDir }) {
333
- const featureFilePath = path.join(projectDir, "features", featureName + ".feature");
365
+
366
+ async function updateFeatureFile({ featureName, scenario, override, projectDir, logger }) {
367
+ const featureFilePath = path.join(projectDir, "features", `${featureName}.feature`);
334
368
  const isFeatureFileExists = existsSync(featureFilePath);
335
369
  const scenarioContent = getScenarioContent(
336
370
  { scenario },
@@ -361,13 +395,13 @@ export async function updateFeatureFile({ featureName, scenario, override, proje
361
395
  plugins: ["prettier-plugin-gherkin"],
362
396
  });
363
397
  } catch (error) {
364
- console.error("Error formatting feature file content with Prettier:", error);
398
+ logger.error("Error formatting feature file content with Prettier:", error);
365
399
  }
366
400
  writeFileSync(featureFilePath, updatedFeatureFileContent);
367
401
  return;
368
402
  }
369
403
  }
370
- let updatedFeatureFileContent = featureFileContent + "\n" + scenarioContent;
404
+ let updatedFeatureFileContent = `${featureFileContent}\n${scenarioContent}`;
371
405
  try {
372
406
  updatedFeatureFileContent = await prettier.format(updatedFeatureFileContent, {
373
407
  ...prettierConfig,
@@ -375,7 +409,7 @@ export async function updateFeatureFile({ featureName, scenario, override, proje
375
409
  plugins: ["prettier-plugin-gherkin"],
376
410
  });
377
411
  } catch (error) {
378
- console.error("Error formatting feature file content with Prettier:", error);
412
+ logger.error("Error formatting feature file content with Prettier:", error);
379
413
  }
380
414
  writeFileSync(featureFilePath, updatedFeatureFileContent);
381
415
  } else {
@@ -387,8 +421,22 @@ export async function updateFeatureFile({ featureName, scenario, override, proje
387
421
  plugins: ["prettier-plugin-gherkin"],
388
422
  });
389
423
  } catch (error) {
390
- console.error("Error formatting feature file content with Prettier:", error);
424
+ logger.error("Error formatting feature file content with Prettier:", error);
391
425
  }
392
426
  writeFileSync(featureFilePath, featureFileContent);
393
427
  }
394
428
  }
429
+
430
+ export {
431
+ containsScenario,
432
+ testStringForRegex,
433
+ escapeNonPrintables,
434
+ getCommandContent,
435
+ getExamplesContent,
436
+ getTagsContent,
437
+ getScenarioContent,
438
+ escapeString,
439
+ GherkinToObject,
440
+ updateExistingScenario,
441
+ updateFeatureFile,
442
+ };
@@ -0,0 +1,80 @@
1
+ import { getErrorMessage } from "../utils/socket_logger.js";
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import {
5
+ FIXED_FILE_NAMES,
6
+ FIXED_FOLDER_NAMES,
7
+ SaveJobErrorType,
8
+ UpdateStepDefinitionsError,
9
+ UTF8_ENCODING,
10
+ } from "./constants.js";
11
+ import url from "node:url";
12
+ const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
13
+
14
+ export function handleFileInitOps(projectDir, logger) {
15
+ try {
16
+ const stepDefinitionFolderPath = path.join(
17
+ projectDir,
18
+ FIXED_FOLDER_NAMES.FEATURES,
19
+ FIXED_FOLDER_NAMES.STEP_DEFINITIONS
20
+ );
21
+ if (!existsSync(stepDefinitionFolderPath)) {
22
+ mkdirSync(stepDefinitionFolderPath, { recursive: true });
23
+ }
24
+
25
+ const utilsFilePath = path.join(
26
+ projectDir,
27
+ FIXED_FOLDER_NAMES.FEATURES,
28
+ FIXED_FOLDER_NAMES.STEP_DEFINITIONS,
29
+ FIXED_FILE_NAMES.UTILS
30
+ );
31
+ const utilsTemplateFilePath = path.join(
32
+ __dirname,
33
+ `../../${FIXED_FOLDER_NAMES.ASSETS}`,
34
+ FIXED_FOLDER_NAMES.TEMPLATES,
35
+ FIXED_FILE_NAMES.UTILS_TEMPLATE
36
+ );
37
+
38
+ if (!existsSync(utilsTemplateFilePath)) {
39
+ throw new Error(`Utils template not found at: ${utilsTemplateFilePath}`);
40
+ }
41
+
42
+ const utilsContent = readFileSync(utilsTemplateFilePath, UTF8_ENCODING);
43
+ writeFileSync(utilsFilePath, utilsContent, UTF8_ENCODING);
44
+
45
+ const hooksTemplateFilePath = path.join(
46
+ __dirname,
47
+ `../../${FIXED_FOLDER_NAMES.ASSETS}`,
48
+ FIXED_FOLDER_NAMES.TEMPLATES,
49
+ FIXED_FILE_NAMES.HOOKS_TEMPLATE
50
+ );
51
+ if (existsSync(hooksTemplateFilePath)) {
52
+ const hooksFilePath = path.join(stepDefinitionFolderPath, FIXED_FILE_NAMES.HOOKS);
53
+ const hooksContent = readFileSync(hooksTemplateFilePath, UTF8_ENCODING);
54
+ writeFileSync(hooksFilePath, hooksContent, UTF8_ENCODING);
55
+ }
56
+
57
+ const featureFolder = path.join(projectDir, FIXED_FOLDER_NAMES.FEATURES);
58
+
59
+ return { featureFolder, utilsFilePath, utilsContent };
60
+ } catch (error) {
61
+ throw new UpdateStepDefinitionsError({
62
+ type: SaveJobErrorType.FILE_SYSTEM_ERROR,
63
+ message: "❌ Failed to initialize step definition folder structure",
64
+ meta: { reason: getErrorMessage(error) },
65
+ });
66
+ }
67
+ }
68
+
69
+ export async function potentialErrorWrapper(fn, errorMessage, type, logger) {
70
+ try {
71
+ return await fn();
72
+ } catch (error) {
73
+ logger.error(`❌ ${errorMessage}: ${getErrorMessage(error)}`);
74
+ throw new UpdateStepDefinitionsError({
75
+ type: type,
76
+ message: errorMessage,
77
+ meta: { reason: getErrorMessage(error) },
78
+ });
79
+ }
80
+ }
@@ -0,0 +1,61 @@
1
+ import { io } from "socket.io-client";
2
+ import { BVTRecorderInit } from "./bvt_init.js";
3
+ import { loadArgs, showUsage, validateCLIArg } from "../cli_helpers.js";
4
+ const requiredEnvVars = ["PROJECT_ID", "BLINQ_TOKEN", "SESSION_ID", "WORKER_WS_SERVER_URL", "REMOTE_ORIGINS_URL"];
5
+ function validateEnvVariables() {
6
+ const missingVars = requiredEnvVars.filter((varName) => !process.env[varName]);
7
+ if (missingVars.length > 0) {
8
+ throw new Error(`Missing required environment variables: ${missingVars.join(", ")}`);
9
+ }
10
+ else {
11
+ console.log("All required environment variables are set.");
12
+ requiredEnvVars.forEach((varName) => {
13
+ console.log(`${varName}: ${process.env[varName]}`);
14
+ });
15
+ }
16
+ }
17
+ function getEnvironmentConfig() {
18
+ const args = loadArgs();
19
+ const projectDir = args[0] || process.env.PROJECT_ID;
20
+ const envName = args[1]?.split("=")[1] || process.env.ENV_NAME;
21
+ const roomId = args[2] || process.env.SESSION_ID;
22
+ const shouldTakeScreenshot = args[3] || process.env.SHOULD_TAKE_SCREENSHOT;
23
+ const TOKEN = process.env.BLINQ_TOKEN;
24
+ try {
25
+ validateCLIArg(projectDir, "projectDir");
26
+ validateCLIArg(envName, "envName");
27
+ validateCLIArg(roomId, "roomId");
28
+ validateCLIArg(shouldTakeScreenshot, "shouldTakeScreenshot");
29
+ if (!TOKEN) {
30
+ throw new Error("BLINQ_TOKEN env variable not set");
31
+ }
32
+ }
33
+ catch (error) {
34
+ const usage = `Usage: node bvt_recorder.js <projectDir> <envName> <roomId>`;
35
+ if (error instanceof Error) {
36
+ showUsage(error, usage);
37
+ }
38
+ else {
39
+ const unknownError = new Error("An unknown error occurred");
40
+ showUsage(unknownError, usage);
41
+ }
42
+ }
43
+ return { envName, projectDir, roomId, TOKEN };
44
+ }
45
+ function initWebBVTRecorder() {
46
+ const socket = io(process.env.WORKER_WS_SERVER_URL, {
47
+ path: "/ws",
48
+ transports: ["websocket", "polling"],
49
+ reconnection: true,
50
+ });
51
+ validateEnvVariables();
52
+ const { envName, projectDir, roomId, TOKEN } = getEnvironmentConfig();
53
+ BVTRecorderInit({
54
+ envName: envName,
55
+ projectDir,
56
+ roomId,
57
+ TOKEN,
58
+ socket,
59
+ });
60
+ }
61
+ initWebBVTRecorder();
@@ -53,6 +53,7 @@ const Types = {
53
53
  SET_INPUT_FILES: "set_input_files",
54
54
  VERIFY_PAGE_SNAPSHOT: "verify_page_snapshot",
55
55
  CONDITIONAL_WAIT: "conditional_wait",
56
+ SLEEP: "sleep",
56
57
  };
57
58
  class Recording {
58
59
  steps = [];
@@ -0,0 +1,2 @@
1
+ // Shared locator schema used by recorder, automation model, and codegen.
2
+ export {};
@@ -12,6 +12,7 @@ class ScenarioUploadService {
12
12
  this.runsApiBaseURL + "/scenarios/create",
13
13
  {
14
14
  name,
15
+ branch: process.env.GIT_BRANCH ? process.env.GIT_BRANCH : "main",
15
16
  },
16
17
  {
17
18
  headers: {
@@ -107,6 +108,7 @@ class ScenarioUploadService {
107
108
  {
108
109
  scenarioId,
109
110
  projectId,
111
+ testcase_id: process.env.TESTCASE_ID,
110
112
  },
111
113
  {
112
114
  headers: {
@@ -0,0 +1,21 @@
1
+ import path from "node:path";
2
+ import os from "node:os";
3
+ function getAppDataDir(project_id) {
4
+ if (process.env.BLINQ_APPDATA_DIR) {
5
+ return path.join(process.env.BLINQ_APPDATA_DIR, "blinq.io", project_id);
6
+ }
7
+ let appDataDir;
8
+ switch (process.platform) {
9
+ case "win32":
10
+ appDataDir = path.join(process.env.APPDATA, "blinq.io", project_id);
11
+ break;
12
+ case "darwin":
13
+ appDataDir = path.join(os.homedir(), "Library", "Application Support", "blinq.io", project_id);
14
+ break;
15
+ default:
16
+ appDataDir = path.join(os.homedir(), ".config", "blinq.io", project_id);
17
+ break;
18
+ }
19
+ return appDataDir;
20
+ }
21
+ export { getAppDataDir };
@@ -1,132 +1,107 @@
1
- /**
2
- * @typedef {Object} SocketLoggerEventPayload
3
- * @property {string} level Log level (e.g. "info", "warn", "error", "debug")
4
- * @property {string} context Log context/subsystem (e.g. "BVTRecorder")
5
- * @property {*} data The log message payload (string, object, etc)
6
- * @property {string} timestamp ISO string when the log was emitted
7
- * @property {number} dataSize Size of data in bytes (-1 if unknown)
8
- */
9
-
10
- /**
11
- * @typedef {Object} SocketLoggerInitOptions
12
- * @property {string=} context Default context for all logs (optional)
13
- * @property {string=} eventName Default event name (default: "recorder.log")
14
- */
15
-
1
+ function getErrorMessage(err) {
2
+ if (typeof err === "string") {
3
+ return err;
4
+ }
5
+ else if (err instanceof Error) {
6
+ return err.message;
7
+ }
8
+ else {
9
+ try {
10
+ return JSON.stringify(err);
11
+ }
12
+ catch {
13
+ return String(err);
14
+ }
15
+ }
16
+ }
17
+ function responseSize(response) {
18
+ try {
19
+ if (typeof response !== "string") {
20
+ return new Blob([JSON.stringify(response)]).size;
21
+ }
22
+ else {
23
+ return new Blob([response]).size;
24
+ }
25
+ }
26
+ catch {
27
+ return -1;
28
+ }
29
+ }
16
30
  /**
17
31
  * SocketLogger - Singleton for structured socket-based logging.
18
32
  *
19
- * @namespace SocketLogger
20
- * @property {function(import('socket.io-client').Socket|import('socket.io').Socket, SocketLoggerInitOptions=):void} init
21
- * @property {function(string, (string|*), Object=, string=, string=):void} log
22
- * @property {function((string|*), Object=):void} info
23
- * @property {function((string|*), Object=):void} warn
24
- * @property {function((string|*), Object=):void} debug
25
- * @property {function((string|*), Object=):void} error
26
- *
27
33
  * @example
28
- * import logger from "./socket_logger.js";
29
- * logger.init(socket, { context: "BVTRecorder" });
30
- * logger.info("Step started", { step: 2 });
31
- * logger.error("Failed!", { error: "bad stuff" });
34
+ * import SocketLogger from "./socket_logger";
35
+ * SocketLogger.getInstance().init(socket, { context: "BVTRecorder" });
36
+ * SocketLogger.getInstance().info("Step started", { step: 2 });
37
+ * SocketLogger.getInstance().error("Failed!", { error: "bad stuff" });
32
38
  */
33
- const SocketLogger = (function () {
34
- /** @type {import('socket.io-client').Socket|import('socket.io').Socket|null} */
35
- let socket = null;
36
- /** @type {string} */
37
- let defaultContext = "";
38
- /** @type {string} */
39
- let defaultEventName = "recorder.log";
40
-
41
- /**
42
- * Initialize the logger (call once).
43
- * @param {import('socket.io-client').Socket|import('socket.io').Socket} sock
44
- * @param {SocketLoggerInitOptions=} opts
45
- */
46
- function init(sock, opts) {
47
- socket = sock;
48
- defaultContext = (opts && opts.context) || "";
49
- defaultEventName = (opts && opts.eventName) || "recorder.log";
50
- }
51
-
52
- /**
53
- * Low-level log method (most users use info/warn/debug/error).
54
- * @param {string} level Log level ("info", "warn", "debug", "error")
55
- * @param {string|*} message The log message or object
56
- * @param {Object=} extra Extra fields (will be merged into log payload)
57
- * @param {string=} eventName Override event name for this log (default: "recorder.log")
58
- * @param {string=} context Override log context for this log (default: set in init)
59
- */
60
- function log(level, message, extra, eventName, context) {
61
- if (!socket || typeof socket.emit !== "function") return;
62
- /** @type {*} */
63
- var data = typeof message === "object" ? message : { message: message };
64
- /** @type {number} */
65
- var dataSize = 0;
66
- try {
67
- dataSize = Buffer.byteLength(JSON.stringify(data || ""), "utf8");
68
- } catch (e) {
69
- dataSize = -1;
39
+ export class SocketLogger {
40
+ static instance;
41
+ socket = null;
42
+ defaultContext = "BVTRecorder";
43
+ defaultEventName = "BVTRecorder.log";
44
+ constructor() { }
45
+ static getInstance() {
46
+ if (!SocketLogger.instance) {
47
+ SocketLogger.instance = new SocketLogger();
48
+ }
49
+ return SocketLogger.instance;
70
50
  }
71
- /** @type {SocketLoggerEventPayload} */
72
- var eventPayload = Object.assign(
73
- {
74
- level: level,
75
- context: context || defaultContext,
76
- data: data,
77
- timestamp: new Date().toISOString(),
78
- dataSize: dataSize,
79
- },
80
- extra || {}
81
- );
82
- // @ts-ignore
83
- try {
84
- if (socket) {
85
- socket.emit(eventName || defaultEventName, eventPayload);
86
- }
87
- } catch (e) {
88
- console.error("Socket logging error:", e);
89
- console.log("Socket event payload:", eventPayload);
51
+ init(sock, opts) {
52
+ this.socket = sock;
53
+ this.defaultContext = opts?.context || "";
54
+ this.defaultEventName = opts?.eventName || "recorder.log";
55
+ }
56
+ log(level, message, extra, context) {
57
+ if (!this.socket || typeof this.socket.emit !== "function")
58
+ return;
59
+ const data = typeof message === "object" ? message : { message };
60
+ let dataSize = 0;
61
+ try {
62
+ dataSize = Buffer.byteLength(JSON.stringify(data || ""), "utf8");
63
+ }
64
+ catch {
65
+ dataSize = -1;
66
+ }
67
+ const eventPayload = {
68
+ level,
69
+ context: context || this.defaultContext,
70
+ data: JSON.stringify({
71
+ ...data,
72
+ errorMessage: level === "error" && data ? getErrorMessage(data) : undefined,
73
+ }),
74
+ timestamp: new Date().toISOString(),
75
+ dataSize,
76
+ ...extra,
77
+ };
78
+ try {
79
+ if (this.socket) {
80
+ this.socket.emit(this.defaultEventName, eventPayload);
81
+ }
82
+ console.log(`${context ?? this.defaultContext} [${level.toUpperCase()}]:`, {
83
+ ...data,
84
+ ...extra,
85
+ });
86
+ }
87
+ catch (e) {
88
+ console.error("Socket logging error:", e);
89
+ console.log("Socket event payload:", eventPayload);
90
+ }
91
+ }
92
+ info(msg, ext, ctx) {
93
+ this.log("info", msg, ext, ctx);
94
+ }
95
+ warn(msg, ext, ctx) {
96
+ this.log("warn", msg, ext, ctx);
97
+ }
98
+ debug(msg, ext, ctx) {
99
+ this.log("debug", msg, ext, ctx);
100
+ }
101
+ error(msg, ext, ctx) {
102
+ this.log("error", msg, ext, ctx);
90
103
  }
91
- }
92
-
93
- /**
94
- * Write an info-level log event.
95
- * @param {string|*} msg The message or object
96
- * @param {Object=} ext Any extra fields/metadata
97
- */
98
- function info(msg, ext) {
99
- log("info", msg, ext);
100
- }
101
-
102
- /**
103
- * Write a warn-level log event.
104
- * @param {string|*} msg The message or object
105
- * @param {Object=} ext Any extra fields/metadata
106
- */
107
- function warn(msg, ext) {
108
- log("warn", msg, ext);
109
- }
110
-
111
- /**
112
- * Write a debug-level log event.
113
- * @param {string|*} msg The message or object
114
- * @param {Object=} ext Any extra fields/metadata
115
- */
116
- function debug(msg, ext) {
117
- log("debug", msg, ext);
118
- }
119
-
120
- /**
121
- * Write an error-level log event.
122
- * @param {string|*} msg The message or object
123
- * @param {Object=} ext Any extra fields/metadata
124
- */
125
- function error(msg, ext) {
126
- log("error", msg, ext);
127
- }
128
-
129
- return { init, log, info, warn, debug, error };
130
- })();
131
-
132
- export default SocketLogger;
104
+ }
105
+ const socketLoggerInstance = SocketLogger.getInstance();
106
+ export default socketLoggerInstance;
107
+ export { getErrorMessage, responseSize };