@alternative-path/testlens-playwright-reporter 0.4.9 → 0.4.11

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 (6) hide show
  1. package/README.md +248 -246
  2. package/cross-env.js +2 -2
  3. package/index.d.ts +17 -1
  4. package/index.js +275 -199
  5. package/index.ts +320 -201
  6. package/package.json +75 -75
package/index.js CHANGED
@@ -10,65 +10,135 @@ const https = tslib_1.__importStar(require("https"));
10
10
  const axios_1 = tslib_1.__importDefault(require("axios"));
11
11
  const child_process_1 = require("child_process");
12
12
  const pino_1 = tslib_1.__importDefault(require("pino"));
13
- const isVerboseLogs = process.env.TESTLENS_LOG_VERBOSE === '1' ||
14
- process.env.TESTLENS_LOG_VERBOSE === 'true';
15
- const noisyInfoPrefixes = [
16
- '[OK]',
17
- '[UPLOAD]',
18
- '[ARTIFACT]',
19
- '[SKIP]',
20
- '[TestLens] onTestEnd called',
21
- '[TestLens] Sending testEnd'
22
- ];
23
- const noisyInfoSubstrings = [
24
- 'Sent runStart event to TestLens',
25
- 'Sent specStart event to TestLens',
26
- 'Sent testStart event to TestLens',
27
- 'Sent testEnd event to TestLens',
28
- 'Sent runEnd event to TestLens',
29
- 'Sent artifact event to TestLens',
30
- 'S3 upload completed',
31
- 'Upload confirmed',
32
- 'Uploading ',
33
- 'Processed artifact',
34
- 'Spec code blocks',
35
- '\n[METADATA]',
36
- 'Custom Metadata Detected',
37
- 'Using API key from environment variable'
38
- ];
39
- const shouldSuppressInfo = (msg) => {
40
- if (isVerboseLogs)
41
- return false;
42
- if (!msg)
43
- return false;
44
- return (noisyInfoPrefixes.some(prefix => msg.startsWith(prefix)) ||
45
- noisyInfoSubstrings.some(sub => msg.includes(sub)));
46
- };
47
- // Create pino logger instance
48
- const logger = (0, pino_1.default)({
49
- level: process.env.TESTLENS_LOG_LEVEL || process.env.LOG_LEVEL || 'info',
50
- base: null, // remove pid/hostname
51
- timestamp: false, // remove time
52
- formatters: {
53
- level: () => ({}) // remove level
54
- },
55
- hooks: {
56
- logMethod(args, method, level) {
57
- // pino level is numeric (info = 30)
58
- if (level === 30) {
59
- const msg = typeof args[0] === 'string'
60
- ? args[0]
61
- : typeof args[1] === 'string'
62
- ? args[1]
63
- : '';
64
- if (shouldSuppressInfo(msg)) {
65
- return;
13
+ const TESTLENS_X_WORKFLOW_AUTH = 'd5f8f1d1a4546e9b49dcedbe51f483aca4bb0e2357b513082b0ce49e59583d38';
14
+ /**
15
+ * Logger class for TestLens Reporter with 2-level logging support
16
+ * - 'info' level: Shows test start/completion with status, artifact status, and errors
17
+ * - 'debug' level: Shows all logs including detailed internal operations
18
+ *
19
+ * CloudWatch-compatible: outputs JSON with level field for filtering
20
+ */
21
+ class Logger {
22
+ constructor(logLevel = 'info') {
23
+ this.logLevel = logLevel;
24
+ // Create base pino logger with CloudWatch-compatible settings
25
+ this.pinoLogger = (0, pino_1.default)({
26
+ level: 'debug', // Always log at debug level, we filter manually
27
+ base: null, // remove pid/hostname - CloudWatch provides this
28
+ timestamp: false, // CloudWatch adds timestamps
29
+ formatters: {
30
+ level: (label) => {
31
+ return { level: label }; // Include level for CloudWatch filtering
66
32
  }
67
33
  }
68
- return method.apply(this, args);
34
+ });
35
+ }
36
+ /**
37
+ * Set run ID context for all subsequent logs
38
+ * This allows correlating all logs to a specific test run in CloudWatch
39
+ */
40
+ setRunId(runId) {
41
+ this.runId = runId;
42
+ // Recreate logger with runId context for CloudWatch querying
43
+ this.pinoLogger = this.pinoLogger.child({ runId });
44
+ }
45
+ /**
46
+ * Check if we're in debug mode
47
+ */
48
+ isDebug() {
49
+ return this.logLevel === 'debug';
50
+ }
51
+ /**
52
+ * Debug level logging - only shown when logLevel is 'debug'
53
+ */
54
+ debug(msg, ...args) {
55
+ if (this.logLevel === 'debug') {
56
+ this.pinoLogger.debug(msg, ...args);
69
57
  }
70
58
  }
71
- });
59
+ /**
60
+ * Debug level logging with object
61
+ */
62
+ debugObj(obj, msg) {
63
+ if (this.logLevel === 'debug') {
64
+ this.pinoLogger.debug(obj, msg);
65
+ }
66
+ }
67
+ /**
68
+ * Info level logging - shown for both 'info' and 'debug' levels
69
+ */
70
+ info(msg, ...args) {
71
+ this.pinoLogger.info(msg, ...args);
72
+ }
73
+ /**
74
+ * Info level logging with object
75
+ */
76
+ infoObj(obj, msg) {
77
+ this.pinoLogger.info(obj, msg);
78
+ }
79
+ /**
80
+ * Warn level logging - shown for both 'info' and 'debug' levels
81
+ */
82
+ warn(msg, ...args) {
83
+ this.pinoLogger.warn(msg, ...args);
84
+ }
85
+ /**
86
+ * Error level logging - always shown regardless of log level
87
+ */
88
+ error(msg, ...args) {
89
+ if (typeof msg === 'string') {
90
+ this.pinoLogger.error(msg, ...args);
91
+ }
92
+ else {
93
+ this.pinoLogger.error(msg, ...args);
94
+ }
95
+ }
96
+ /**
97
+ * Log test case start - debug level only
98
+ * In info mode, we only show logs when there are issues
99
+ */
100
+ logTestStart(testName) {
101
+ this.debug(`[TEST START] ${testName}`);
102
+ }
103
+ /**
104
+ * Log test case completion - debug level only
105
+ * In info mode, we only show logs when there are issues
106
+ */
107
+ logTestEnd(testName, status, duration) {
108
+ const statusEmoji = status === 'passed' ? '✓' : status === 'failed' ? '✗' : '○';
109
+ const durationStr = `${duration}ms`;
110
+ this.debug(`[TEST END] ${statusEmoji} ${testName} - ${status} (${durationStr})`);
111
+ }
112
+ /**
113
+ * Log artifact status - info level only for failures
114
+ * Success is logged at debug level
115
+ */
116
+ logArtifactStatus(testName, artifactType, status) {
117
+ if (status === 'failed') {
118
+ this.error(`[ARTIFACT] ✗ ${artifactType} for "${testName}" - failed`);
119
+ }
120
+ else {
121
+ this.debug(`[ARTIFACT] ${status === 'uploaded' ? '✓' : '○'} ${artifactType} for "${testName}" - ${status}`);
122
+ }
123
+ }
124
+ /**
125
+ * Log run summary - debug level only
126
+ * In info mode, we only show logs when there are issues
127
+ */
128
+ logRunSummary(passed, failed, skipped, timedOut) {
129
+ this.debug(`[RUN SUMMARY] ${passed} passed, ${failed} failed (${timedOut} timeouts), ${skipped} skipped`);
130
+ }
131
+ }
132
+ // Legacy logger for backward compatibility during transition
133
+ // Will be replaced with Logger instance per reporter
134
+ let globalLogger = new Logger('info');
135
+ const logger = {
136
+ get debug() { return globalLogger.debug.bind(globalLogger); },
137
+ get info() { return globalLogger.info.bind(globalLogger); },
138
+ get warn() { return globalLogger.warn.bind(globalLogger); },
139
+ get error() { return globalLogger.error.bind(globalLogger); },
140
+ get levelVal() { return globalLogger.isDebug() ? 20 : 30; }
141
+ };
72
142
  // Lazy-load mime module to support ESM
73
143
  let mimeModule = null;
74
144
  async function getMime() {
@@ -80,6 +150,17 @@ async function getMime() {
80
150
  return mimeModule;
81
151
  }
82
152
  class TestLensReporter {
153
+ /**
154
+ * Get environment variable value with case-insensitive key match (e.g. TL_BUILDNAME, tl_buildname).
155
+ */
156
+ static getEnvCaseInsensitive(name) {
157
+ const upper = name.toUpperCase();
158
+ for (const key of Object.keys(process.env)) {
159
+ if (key.toUpperCase() === upper)
160
+ return process.env[key];
161
+ }
162
+ return undefined;
163
+ }
83
164
  /**
84
165
  * Parse custom metadata from environment variables
85
166
  * Checks for common metadata environment variables
@@ -88,11 +169,12 @@ class TestLensReporter {
88
169
  const customArgs = {};
89
170
  // Common environment variable names for build metadata
90
171
  const envVarMappings = {
91
- // Support both TestLens-specific names (recommended) and common CI names
92
- 'testlensBuildTag': ['testlensBuildTag', 'TESTLENS_BUILD_TAG', 'TESTLENS_BUILDTAG', 'BUILDTAG', 'BUILD_TAG', 'TestlensBuildTag', 'TestLensBuildTag'],
93
- 'testlensBuildName': ['testlensBuildName', 'TESTLENS_BUILD_NAME', 'TESTLENS_BUILDNAME', 'BUILDNAME', 'BUILD_NAME', 'TestlensBuildName', 'TestLensBuildName'],
172
+ // Support both TestLens-specific names (recommended) and common CI names; TL_* are short aliases; keys matched case-insensitively
173
+ 'testlensBuildTag': ['TL_BUILDTAG', 'testlensBuildTag', 'TESTLENS_BUILD_TAG', 'TESTLENS_BUILDTAG', 'BUILDTAG', 'BUILD_TAG', 'TestlensBuildTag', 'TestLensBuildTag'],
174
+ 'testlensBuildName': ['TL_BUILDNAME', 'testlensBuildName', 'TESTLENS_BUILD_NAME', 'TESTLENS_BUILDNAME', 'BUILDNAME', 'BUILD_NAME', 'TestlensBuildName', 'TestLensBuildName'],
94
175
  // Execution ID for one run per pipeline (checked in order; prefer TESTLENS_EXECUTION_ID, then CI-specific UUIDs)
95
176
  'executionId': [
177
+ 'TL_EXECUTIONID',
96
178
  'TESTLENS_EXECUTION_ID',
97
179
  'TestlensExecutionId',
98
180
  'TestLensExecutionId',
@@ -114,10 +196,10 @@ class TestLensReporter {
114
196
  'project': ['PROJECT', 'PROJECT_NAME'],
115
197
  'customvalue': ['CUSTOMVALUE', 'CUSTOM_VALUE']
116
198
  };
117
- // Check for each metadata key
199
+ // Check for each metadata key (case-insensitive env lookup)
118
200
  Object.entries(envVarMappings).forEach(([key, envVars]) => {
119
201
  for (const envVar of envVars) {
120
- const value = process.env[envVar];
202
+ const value = TestLensReporter.getEnvCaseInsensitive(envVar);
121
203
  if (value) {
122
204
  // For testlensBuildTag, support comma-separated values
123
205
  if (key === 'testlensBuildTag' && value.includes(',')) {
@@ -139,13 +221,20 @@ class TestLensReporter {
139
221
  this.runCreationFailed = false; // Track if run creation failed due to limits
140
222
  this.cliArgs = {}; // Store CLI args separately
141
223
  this.pendingUploads = new Set(); // Track pending artifact uploads
142
- this.traceNetworkRows = []; // Network requests/responses from trace zip for current test
143
224
  this.artifactStats = {
144
225
  uploaded: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
145
226
  skipped: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
146
227
  failed: { screenshot: 0, video: 0, trace: 0, attachment: 0 }
147
228
  };
148
229
  this.artifactsSeen = 0;
230
+ // Initialize logger first with user-configured log level
231
+ const logLevel = options.logLevel ||
232
+ process.env.TESTLENS_LOG_LEVEL ||
233
+ (process.env.LOG_LEVEL === 'debug' ? 'debug' : 'info') ||
234
+ 'info';
235
+ this.logger = new Logger(logLevel);
236
+ // Update global logger reference for any legacy code
237
+ globalLogger = this.logger;
149
238
  // Parse custom CLI arguments
150
239
  const customArgs = TestLensReporter.parseCustomArgs();
151
240
  this.cliArgs = customArgs; // Store CLI args separately for later use
@@ -176,13 +265,14 @@ class TestLensReporter {
176
265
  rejectUnauthorized: options.rejectUnauthorized,
177
266
  ignoreSslErrors: options.ignoreSslErrors,
178
267
  customMetadata: { ...options.customMetadata, ...customArgs }, // Config metadata first, then CLI args override
179
- executionId: options.executionId
268
+ executionId: options.executionId,
269
+ logLevel: logLevel
180
270
  };
181
271
  if (!this.config.apiKey) {
182
272
  throw new Error('API_KEY is required for TestLensReporter. Pass it as apiKey option in your playwright config or set one of these environment variables: TESTLENS_API_KEY, TESTLENS_KEY, PLAYWRIGHT_API_KEY, PW_API_KEY, API_KEY, or APIKEY.');
183
273
  }
184
274
  if (apiKey !== options.apiKey) {
185
- logger.debug('✓ Using API key from environment variable');
275
+ this.logger.debug('✓ Using API key from environment variable');
186
276
  }
187
277
  // Default environment to allow self-signed certs unless explicitly set
188
278
  if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === undefined) {
@@ -193,18 +283,18 @@ class TestLensReporter {
193
283
  // Check various ways SSL validation can be disabled or enforced (in order of precedence)
194
284
  if (this.config.ignoreSslErrors === true) {
195
285
  rejectUnauthorized = false;
196
- logger.debug('[DEBUG] SSL certificate validation disabled via ignoreSslErrors option');
286
+ this.logger.debug('[DEBUG] SSL certificate validation disabled via ignoreSslErrors option');
197
287
  }
198
288
  else if (this.config.rejectUnauthorized === false) {
199
289
  rejectUnauthorized = false;
200
- logger.debug('[DEBUG] SSL certificate validation disabled via rejectUnauthorized option');
290
+ this.logger.debug('[DEBUG] SSL certificate validation disabled via rejectUnauthorized option');
201
291
  }
202
292
  else if (this.config.rejectUnauthorized === true) {
203
293
  rejectUnauthorized = true;
204
294
  }
205
295
  else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
206
296
  rejectUnauthorized = false;
207
- logger.debug('[DEBUG] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
297
+ this.logger.debug('[DEBUG] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
208
298
  }
209
299
  else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1') {
210
300
  rejectUnauthorized = true;
@@ -217,6 +307,7 @@ class TestLensReporter {
217
307
  timeout: this.config.timeout,
218
308
  headers: {
219
309
  'Content-Type': 'application/json',
310
+ 'x-workflow-auth': TESTLENS_X_WORKFLOW_AUTH,
220
311
  ...(this.config.apiKey && { 'X-API-Key': this.config.apiKey }),
221
312
  },
222
313
  // Enhanced SSL handling with flexible TLS configuration
@@ -253,17 +344,19 @@ class TestLensReporter {
253
344
  const executionId = typeof executionIdRaw === 'string' && executionIdRaw.trim() ? String(executionIdRaw).trim() : undefined;
254
345
  this.runId = executionId || (0, crypto_1.randomUUID)();
255
346
  this.usedExecutionId = !!executionId;
347
+ // Set runId in logger context for CloudWatch correlation
348
+ this.logger.setRunId(this.runId);
256
349
  this.runMetadata = this.initializeRunMetadata();
257
350
  this.specMap = new Map();
258
351
  this.testMap = new Map();
259
352
  this.runCreationFailed = false;
260
353
  // Log custom metadata if any
261
354
  if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
262
- logger.debug('\n[METADATA] Custom Metadata Detected:');
355
+ this.logger.debug('\n[METADATA] Custom Metadata Detected:');
263
356
  Object.entries(this.config.customMetadata).forEach(([key, value]) => {
264
- logger.debug(` ${key}: ${value}`);
357
+ this.logger.debug(` ${key}: ${value}`);
265
358
  });
266
- logger.debug('');
359
+ this.logger.debug('');
267
360
  }
268
361
  }
269
362
  initializeRunMetadata() {
@@ -326,27 +419,21 @@ class TestLensReporter {
326
419
  return status;
327
420
  }
328
421
  async onBegin(config, suite) {
329
- logger.debug(`[REPORTER] source=${__filename} enableArtifacts=${this.config.enableArtifacts}`);
330
- // Show Build Name if provided, otherwise show Run ID
331
- if (this.runMetadata.testlensBuildName) {
332
- logger.debug(`TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
333
- logger.debug(` Run ID: ${this.runId}`);
334
- }
335
- else {
336
- logger.debug(`TestLens Reporter starting - Run ID: ${this.runId}`);
337
- }
422
+ this.logger.debug(`[REPORTER] source=${__filename} enableArtifacts=${this.config.enableArtifacts}`);
423
+ // Only show startup info in debug mode - in info mode we only log issues
424
+ this.logger.debug(`TestLens Reporter starting - Run ID: ${this.runId}`);
338
425
  // Collect Git information if enabled
339
426
  if (this.config.enableGitInfo) {
340
427
  this.runMetadata.gitInfo = await this.collectGitInfo();
341
428
  if (this.runMetadata.gitInfo) {
342
- logger.debug(`Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
429
+ this.logger.debug(`Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
343
430
  }
344
431
  else {
345
- logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
432
+ this.logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
346
433
  }
347
434
  }
348
435
  else {
349
- logger.debug(`[INFO] Git info collection disabled (enableGitInfo: false)`);
436
+ this.logger.debug(`[INFO] Git info collection disabled (enableGitInfo: false)`);
350
437
  }
351
438
  // Add shard information if available
352
439
  if (config.shard) {
@@ -364,8 +451,8 @@ class TestLensReporter {
364
451
  });
365
452
  }
366
453
  async onTestBegin(test, result) {
367
- // Log which test is starting
368
- logger.debug(`[TEST] Running test: ${test.title}`);
454
+ // Log which test is starting (info level for test start)
455
+ this.logger.logTestStart(test.title);
369
456
  const specPath = test.location.file;
370
457
  const specKey = `${specPath}-${test.parent.title}`;
371
458
  // Create or update spec data
@@ -436,27 +523,14 @@ class TestLensReporter {
436
523
  }
437
524
  }
438
525
  async onTestEnd(test, result) {
439
- this.traceNetworkRows = []; // Reset at start of each test
526
+ this.logger.debug(`[ARTIFACTS] attachments=${result.attachments?.length ?? 0}`);
440
527
  const testId = this.getTestId(test);
441
528
  let testCaseId = '';
529
+ const localTraceNetworkRows = [];
442
530
  let testData = this.testMap.get(testId);
443
- logger.debug(`[ARTIFACTS] attachments=${result.attachments?.length ?? 0}`);
444
- if (result.attachments && result.attachments.length > 0) {
445
- for (const attachment of result.attachments) {
446
- const artifactType = this.getArtifactType(attachment.name);
447
- this.artifactsSeen += 1;
448
- if (attachment.path) {
449
- this.bumpArtifactStat('uploaded', artifactType);
450
- }
451
- else {
452
- this.bumpArtifactStat('skipped', artifactType);
453
- }
454
- }
455
- }
456
- logger.debug(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
457
531
  // For skipped tests, onTestBegin might not be called, so we need to create the test data here
458
532
  if (!testData) {
459
- logger.debug(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
533
+ this.logger.debug(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
460
534
  // Create spec data if not exists (skipped tests might not have spec data either)
461
535
  const specPath = test.location.file;
462
536
  const specKey = `${specPath}-${test.parent.title}`;
@@ -601,7 +675,7 @@ class TestLensReporter {
601
675
  });
602
676
  // Send testEnd event for all tests, regardless of status
603
677
  // This ensures tests that are interrupted or have unexpected statuses are properly recorded
604
- logger.debug(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
678
+ this.logger.debug(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
605
679
  // Send test end event to API and get response
606
680
  const testEndResponse = await this.sendToApi({
607
681
  type: 'testEnd',
@@ -610,14 +684,16 @@ class TestLensReporter {
610
684
  test: testData
611
685
  });
612
686
  testCaseId = testEndResponse?.testCaseId;
687
+ // Log test completion with status (info level)
688
+ this.logger.logTestEnd(test.title, testData.status, testData.duration);
613
689
  // Handle artifacts (test case is now guaranteed to be in database)
614
690
  if (this.config.enableArtifacts) {
615
691
  // Pass test case DB ID if available for faster lookups; pass status/endTime so backend
616
692
  // can fix up test case status if testEnd failed but artifact request succeeds
617
- await this.processArtifacts(testId, result, testEndResponse?.testCaseId, {
693
+ await this.processArtifacts(testId, test.title, result, testEndResponse?.testCaseId, {
618
694
  status: testData.status,
619
695
  endTime: testData.endTime
620
- });
696
+ }, localTraceNetworkRows);
621
697
  }
622
698
  else if (result.attachments && result.attachments.length > 0) {
623
699
  for (const attachment of result.attachments) {
@@ -687,7 +763,7 @@ class TestLensReporter {
687
763
  spec: specData
688
764
  });
689
765
  // Send spec code blocks to API
690
- await this.sendSpecCodeBlocks(specPath, test.title, testData?.errors || [], this.runId, testId, testCaseId);
766
+ await this.sendSpecCodeBlocks(specPath, test.title, testData?.errors || [], this.runId, testId, testCaseId, localTraceNetworkRows);
691
767
  }
692
768
  }
693
769
  }
@@ -696,29 +772,32 @@ class TestLensReporter {
696
772
  this.runMetadata.duration = Date.now() - new Date(this.runMetadata.startTime).getTime();
697
773
  // Wait for all pending artifact uploads to complete before sending runEnd
698
774
  if (this.pendingUploads.size > 0) {
699
- logger.debug(`Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
775
+ this.logger.debug(`Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
700
776
  try {
701
777
  await Promise.all(Array.from(this.pendingUploads));
702
- logger.debug(`[OK] All artifact uploads completed`);
778
+ this.logger.debug(`[OK] All artifact uploads completed`);
703
779
  }
704
780
  catch (error) {
705
- logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
781
+ this.logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
706
782
  }
707
783
  }
708
784
  const uploaded = this.artifactStats.uploaded;
709
785
  const skipped = this.artifactStats.skipped;
710
786
  const failed = this.artifactStats.failed;
711
- // Always show summary so users can confirm artifact settings
787
+ // Calculate total uploaded/failed for info level summary
788
+ const totalUploaded = uploaded.screenshot + uploaded.video + uploaded.trace + uploaded.attachment;
789
+ const totalFailed = failed.screenshot + failed.video + failed.trace + failed.attachment;
790
+ const totalSkipped = skipped.screenshot + skipped.video + skipped.trace + skipped.attachment;
791
+ // Info level: only show artifact summary if there are failures
792
+ if (totalFailed > 0) {
793
+ this.logger.error(`[ARTIFACTS] ${totalUploaded} uploaded, ${totalFailed} failed, ${totalSkipped} skipped`);
794
+ }
795
+ // Debug level: show detailed breakdown
712
796
  const summary = `[ARTIFACTS] seen=${this.artifactsSeen} | ` +
713
797
  `uploaded screenshot=${uploaded.screenshot}, video=${uploaded.video}, trace=${uploaded.trace}, attachment=${uploaded.attachment} | ` +
714
798
  `skipped screenshot=${skipped.screenshot}, video=${skipped.video}, trace=${skipped.trace}, attachment=${skipped.attachment} | ` +
715
799
  `failed screenshot=${failed.screenshot}, video=${failed.video}, trace=${failed.trace}, attachment=${failed.attachment}`;
716
- if (logger.levelVal > 30) {
717
- console.log(summary);
718
- }
719
- else {
720
- logger.debug(summary);
721
- }
800
+ this.logger.debug(summary);
722
801
  // Calculate final stats
723
802
  const totalTests = Array.from(this.testMap.values()).length;
724
803
  const passedTests = Array.from(this.testMap.values()).filter(t => t.status === 'passed').length;
@@ -746,15 +825,10 @@ class TestLensReporter {
746
825
  aggregationMode: this.usedExecutionId ? 'append' : 'replace'
747
826
  }
748
827
  });
749
- // Show Build Name if provided, otherwise show Run ID
750
- if (this.runMetadata.testlensBuildName) {
751
- logger.debug(`[COMPLETE] TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
752
- logger.debug(` Run ID: ${this.runId}`);
753
- }
754
- else {
755
- logger.debug(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
756
- }
757
- logger.debug(`[RESULTS] ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
828
+ // Only show completion info in debug mode - in info mode we only log issues
829
+ this.logger.debug(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
830
+ // Log run summary at debug level
831
+ this.logger.logRunSummary(passedTests, failedTests, skippedTests, timedOutTests);
758
832
  }
759
833
  async sendToApi(payload) {
760
834
  // Skip sending if run creation already failed
@@ -768,7 +842,7 @@ class TestLensReporter {
768
842
  }
769
843
  });
770
844
  if (this.config.enableRealTimeStream) {
771
- logger.debug(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
845
+ this.logger.debug(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
772
846
  }
773
847
  // Return response data for caller to use
774
848
  return response.data;
@@ -782,64 +856,64 @@ class TestLensReporter {
782
856
  if (payload.type === 'runStart' && errorData?.limit_type === 'test_runs') {
783
857
  this.runCreationFailed = true;
784
858
  }
785
- logger.error('\n' + '='.repeat(80));
859
+ this.logger.error('\n' + '='.repeat(80));
786
860
  if (errorData?.limit_type === 'test_cases') {
787
- logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
861
+ this.logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
788
862
  }
789
863
  else if (errorData?.limit_type === 'test_runs') {
790
- logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
864
+ this.logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
791
865
  }
792
866
  else {
793
- logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
867
+ this.logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
794
868
  }
795
- logger.error('='.repeat(80));
796
- logger.error('');
797
- logger.error(errorData?.message || 'You have reached your plan limit.');
798
- logger.error('');
799
- logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
800
- logger.error('');
801
- logger.error('To continue, please upgrade your plan.');
802
- logger.error('Contact: support@alternative-path.com');
803
- logger.error('');
804
- logger.error('='.repeat(80));
805
- logger.error('');
869
+ this.logger.error('='.repeat(80));
870
+ this.logger.error('');
871
+ this.logger.error(errorData?.message || 'You have reached your plan limit.');
872
+ this.logger.error('');
873
+ this.logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
874
+ this.logger.error('');
875
+ this.logger.error('To continue, please upgrade your plan.');
876
+ this.logger.error('Contact: support@alternative-path.com');
877
+ this.logger.error('');
878
+ this.logger.error('='.repeat(80));
879
+ this.logger.error('');
806
880
  return; // Don't log the full error object for limit errors
807
881
  }
808
882
  // Check for trial expiration, subscription errors, or limit errors (401)
809
883
  if (status === 401) {
810
884
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
811
885
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
812
- logger.error('\n' + '='.repeat(80));
886
+ this.logger.error('\n' + '='.repeat(80));
813
887
  if (errorData?.error === 'test_cases_limit_reached') {
814
- logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
888
+ this.logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
815
889
  }
816
890
  else if (errorData?.error === 'test_runs_limit_reached') {
817
- logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
891
+ this.logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
818
892
  }
819
893
  else {
820
- logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
894
+ this.logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
821
895
  }
822
- logger.error('='.repeat(80));
823
- logger.error('');
824
- logger.error(errorData?.message || 'Your trial period has expired.');
825
- logger.error('');
826
- logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
827
- logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
828
- logger.error('');
896
+ this.logger.error('='.repeat(80));
897
+ this.logger.error('');
898
+ this.logger.error(errorData?.message || 'Your trial period has expired.');
899
+ this.logger.error('');
900
+ this.logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
901
+ this.logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
902
+ this.logger.error('');
829
903
  if (errorData?.trial_end_date) {
830
- logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
831
- logger.error('');
904
+ this.logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
905
+ this.logger.error('');
832
906
  }
833
- logger.error('='.repeat(80));
834
- logger.error('');
907
+ this.logger.error('='.repeat(80));
908
+ this.logger.error('');
835
909
  }
836
910
  else {
837
- logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
911
+ this.logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
838
912
  }
839
913
  }
840
914
  else if (status !== 403) {
841
915
  // Log other errors (but not 403 which we handled above)
842
- logger.error({
916
+ this.logger.error({
843
917
  message: `[ERROR] Failed to send ${payload.type} event to TestLens`,
844
918
  error: error?.message || 'Unknown error',
845
919
  status: status,
@@ -853,7 +927,7 @@ class TestLensReporter {
853
927
  // Don't throw error to avoid breaking test execution
854
928
  }
855
929
  }
856
- async processArtifacts(testId, result, testCaseDbId, testEndPayload) {
930
+ async processArtifacts(testId, testName, result, testCaseDbId, testEndPayload, traceNetworkRows) {
857
931
  // Skip artifact processing if run creation failed
858
932
  if (this.runCreationFailed) {
859
933
  return;
@@ -861,19 +935,20 @@ class TestLensReporter {
861
935
  const attachments = result.attachments;
862
936
  for (const attachment of attachments) {
863
937
  const artifactType = this.getArtifactType(attachment.name);
938
+ this.artifactsSeen += 1;
864
939
  if (attachment.path) {
865
940
  // Check if attachment should be processed based on config
866
941
  const isVideo = artifactType === 'video' || attachment.contentType?.startsWith('video/');
867
942
  const isScreenshot = artifactType === 'screenshot' || attachment.contentType?.startsWith('image/');
868
943
  // Skip video if disabled in config
869
944
  if (isVideo && !this.config.enableVideo) {
870
- logger.debug(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
945
+ this.logger.debug(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
871
946
  this.bumpArtifactStat('skipped', artifactType);
872
947
  continue;
873
948
  }
874
949
  // Skip screenshot if disabled in config
875
950
  if (isScreenshot && !this.config.enableScreenshot) {
876
- logger.debug(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
951
+ this.logger.debug(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
877
952
  this.bumpArtifactStat('skipped', artifactType);
878
953
  continue;
879
954
  }
@@ -940,16 +1015,12 @@ class TestLensReporter {
940
1015
  }
941
1016
  }
942
1017
  const durationMs = Date.now() - traceStart;
943
- if (networkRows.length > 0) {
944
- this.traceNetworkRows = networkRows;
945
- }
946
- else {
947
- this.traceNetworkRows = [];
1018
+ if (networkRows.length > 0 && traceNetworkRows) {
1019
+ traceNetworkRows.push(...networkRows);
948
1020
  }
949
1021
  }
950
1022
  catch (e) {
951
- logger.warn('[TRACE] Could not read trace zip: ' + (e && e.message));
952
- this.traceNetworkRows = [];
1023
+ this.logger.warn('[TRACE] Could not read trace zip: ' + (e && e.message));
953
1024
  }
954
1025
  }
955
1026
  try {
@@ -987,14 +1058,14 @@ class TestLensReporter {
987
1058
  const uploadPromise = Promise.resolve().then(async () => {
988
1059
  try {
989
1060
  if (!attachment.path) {
990
- logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
1061
+ this.logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
991
1062
  this.bumpArtifactStat('skipped', artifactType);
992
1063
  return;
993
1064
  }
994
1065
  const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
995
1066
  // Skip if upload failed or file was too large
996
1067
  if (!s3Data) {
997
- logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
1068
+ this.logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
998
1069
  this.bumpArtifactStat('failed', artifactType);
999
1070
  return;
1000
1071
  }
@@ -1021,10 +1092,15 @@ class TestLensReporter {
1021
1092
  timestamp: new Date().toISOString(),
1022
1093
  artifact: artifactData
1023
1094
  });
1024
- logger.debug(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
1095
+ // Log artifact upload success at info level
1096
+ this.logger.logArtifactStatus(testName, artifactType, 'uploaded');
1097
+ this.bumpArtifactStat('uploaded', artifactType);
1098
+ this.logger.debug(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
1025
1099
  }
1026
1100
  catch (error) {
1027
- logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${error.message}`);
1101
+ // Log artifact upload failure at error level
1102
+ this.logger.logArtifactStatus(testName, artifactType, 'failed');
1103
+ this.logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${error.message}`);
1028
1104
  this.bumpArtifactStat('failed', artifactType);
1029
1105
  }
1030
1106
  });
@@ -1037,7 +1113,7 @@ class TestLensReporter {
1037
1113
  // They will be awaited in onEnd
1038
1114
  }
1039
1115
  catch (error) {
1040
- logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${error.message}`);
1116
+ this.logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${error.message}`);
1041
1117
  this.bumpArtifactStat('failed', artifactType);
1042
1118
  }
1043
1119
  }
@@ -1046,7 +1122,7 @@ class TestLensReporter {
1046
1122
  }
1047
1123
  }
1048
1124
  }
1049
- async sendSpecCodeBlocks(specPath, testName, errors, runId, test_id, testCaseId) {
1125
+ async sendSpecCodeBlocks(specPath, testName, errors, runId, test_id, testCaseId, traceNetworkRows = []) {
1050
1126
  try {
1051
1127
  // Extract code blocks using built-in parser
1052
1128
  const testBlocks = this.extractTestBlocks(specPath);
@@ -1070,22 +1146,22 @@ class TestLensReporter {
1070
1146
  filePath: path.relative(process.cwd(), specPath),
1071
1147
  codeBlocks,
1072
1148
  errors,
1073
- traceNetworkRows: this.traceNetworkRows,
1149
+ traceNetworkRows: traceNetworkRows,
1074
1150
  testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, ''),
1075
1151
  runId,
1076
1152
  test_id,
1077
1153
  testCaseId
1078
1154
  });
1079
- logger.debug(`Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
1155
+ this.logger.debug(`Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
1080
1156
  }
1081
1157
  catch (error) {
1082
1158
  const errorData = error?.response?.data;
1083
1159
  // Handle duplicate spec code blocks gracefully (when re-running tests)
1084
1160
  if (errorData?.error && errorData.error.includes('duplicate key value violates unique constraint')) {
1085
- logger.debug(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
1161
+ this.logger.debug(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
1086
1162
  return;
1087
1163
  }
1088
- logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
1164
+ this.logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
1089
1165
  }
1090
1166
  }
1091
1167
  extractTestBlocks(filePath) {
@@ -1185,7 +1261,7 @@ class TestLensReporter {
1185
1261
  return blocks;
1186
1262
  }
1187
1263
  catch (error) {
1188
- logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
1264
+ this.logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
1189
1265
  return [];
1190
1266
  }
1191
1267
  }
@@ -1208,7 +1284,7 @@ class TestLensReporter {
1208
1284
  }
1209
1285
  catch (e) {
1210
1286
  // Remote info is optional - handle gracefully
1211
- logger.debug('[INFO] No git remote configured, skipping remote info');
1287
+ this.logger.debug('[INFO] No git remote configured, skipping remote info');
1212
1288
  }
1213
1289
  const isDirty = (0, child_process_1.execSync)('git status --porcelain', { encoding: 'utf-8' }).trim().length > 0;
1214
1290
  return {
@@ -1286,7 +1362,7 @@ class TestLensReporter {
1286
1362
  // Check file size first
1287
1363
  const fileSize = this.getFileSize(filePath);
1288
1364
  const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
1289
- logger.debug(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
1365
+ this.logger.debug(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
1290
1366
  const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
1291
1367
  // Step 1: Request pre-signed URL from server
1292
1368
  const presignedUrlEndpoint = `${baseUrl}/api/v1/artifacts/public/presigned-url`;
@@ -1310,7 +1386,7 @@ class TestLensReporter {
1310
1386
  }
1311
1387
  const { uploadUrl, s3Key, metadata } = presignedResponse.data;
1312
1388
  // Step 2: Upload directly to S3 using presigned URL
1313
- logger.debug(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1389
+ this.logger.debug(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1314
1390
  const fileBuffer = fs.readFileSync(filePath);
1315
1391
  // IMPORTANT: When using presigned URLs, we MUST include exactly the headers that were signed
1316
1392
  // The backend signs with ServerSideEncryption:'AES256', so we must send that header
@@ -1328,7 +1404,7 @@ class TestLensReporter {
1328
1404
  if (uploadResponse.status !== 200) {
1329
1405
  throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
1330
1406
  }
1331
- logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1407
+ this.logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1332
1408
  // Step 3: Confirm upload with server to save metadata
1333
1409
  const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
1334
1410
  const confirmBody = {
@@ -1349,7 +1425,7 @@ class TestLensReporter {
1349
1425
  });
1350
1426
  if (confirmResponse.status === 201 && confirmResponse.data.success) {
1351
1427
  const artifact = confirmResponse.data.artifact;
1352
- logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
1428
+ this.logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
1353
1429
  return {
1354
1430
  key: s3Key,
1355
1431
  url: artifact.s3Url,
@@ -1368,29 +1444,29 @@ class TestLensReporter {
1368
1444
  const errorData = error?.response?.data;
1369
1445
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
1370
1446
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
1371
- logger.error('\n' + '='.repeat(80));
1447
+ this.logger.error('\n' + '='.repeat(80));
1372
1448
  if (errorData?.error === 'test_cases_limit_reached') {
1373
- logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
1449
+ this.logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
1374
1450
  }
1375
1451
  else if (errorData?.error === 'test_runs_limit_reached') {
1376
- logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
1452
+ this.logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
1377
1453
  }
1378
1454
  else {
1379
- logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
1455
+ this.logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
1380
1456
  }
1381
- logger.error('='.repeat(80));
1382
- logger.error('');
1383
- logger.error(errorData?.message || 'Your trial period has expired.');
1384
- logger.error('');
1385
- logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
1386
- logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1387
- logger.error('');
1457
+ this.logger.error('='.repeat(80));
1458
+ this.logger.error('');
1459
+ this.logger.error(errorData?.message || 'Your trial period has expired.');
1460
+ this.logger.error('');
1461
+ this.logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
1462
+ this.logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1463
+ this.logger.error('');
1388
1464
  if (errorData?.trial_end_date) {
1389
- logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1390
- logger.error('');
1465
+ this.logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1466
+ this.logger.error('');
1391
1467
  }
1392
- logger.error('='.repeat(80));
1393
- logger.error('');
1468
+ this.logger.error('='.repeat(80));
1469
+ this.logger.error('');
1394
1470
  return null;
1395
1471
  }
1396
1472
  }
@@ -1408,9 +1484,9 @@ class TestLensReporter {
1408
1484
  else if (error.response?.status === 403) {
1409
1485
  errorMsg = `Access denied (403) - presigned URL may have expired`;
1410
1486
  }
1411
- logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
1487
+ this.logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
1412
1488
  if (error.response?.data) {
1413
- logger.error({ errorDetails: error.response.data }, 'Error details');
1489
+ this.logger.error({ errorDetails: error.response.data }, 'Error details');
1414
1490
  }
1415
1491
  // Don't throw, just return null to continue with other artifacts
1416
1492
  return null;
@@ -1455,7 +1531,7 @@ class TestLensReporter {
1455
1531
  return stats.size;
1456
1532
  }
1457
1533
  catch (error) {
1458
- logger.warn(`Could not get file size for ${filePath}: ${error.message}`);
1534
+ this.logger.warn(`Could not get file size for ${filePath}: ${error.message}`);
1459
1535
  return 0;
1460
1536
  }
1461
1537
  }