@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.ts CHANGED
@@ -8,69 +8,154 @@ import type { Reporter, TestCase, TestResult, FullConfig, Suite } from '@playwri
8
8
  import { execSync } from 'child_process';
9
9
  import pino from 'pino';
10
10
 
11
- const isVerboseLogs =
12
- process.env.TESTLENS_LOG_VERBOSE === '1' ||
13
- process.env.TESTLENS_LOG_VERBOSE === 'true';
14
- const noisyInfoPrefixes = [
15
- '[OK]',
16
- '[UPLOAD]',
17
- '[ARTIFACT]',
18
- '[SKIP]',
19
- '[TestLens] onTestEnd called',
20
- '[TestLens] Sending testEnd'
21
- ];
22
- const noisyInfoSubstrings = [
23
- 'Sent runStart event to TestLens',
24
- 'Sent specStart event to TestLens',
25
- 'Sent testStart event to TestLens',
26
- 'Sent testEnd event to TestLens',
27
- 'Sent runEnd event to TestLens',
28
- 'Sent artifact event to TestLens',
29
- 'S3 upload completed',
30
- 'Upload confirmed',
31
- 'Uploading ',
32
- 'Processed artifact',
33
- 'Spec code blocks',
34
- '\n[METADATA]',
35
- 'Custom Metadata Detected',
36
- 'Using API key from environment variable'
37
- ];
38
-
39
- const shouldSuppressInfo = (msg: string): boolean => {
40
- if (isVerboseLogs) return false;
41
- if (!msg) return false;
42
- return (
43
- noisyInfoPrefixes.some(prefix => msg.startsWith(prefix)) ||
44
- noisyInfoSubstrings.some(sub => msg.includes(sub))
45
- );
46
- };
47
-
48
- // Create pino logger instance
49
- const logger = pino({
50
- level: process.env.TESTLENS_LOG_LEVEL || process.env.LOG_LEVEL || 'info',
51
- base: null, // remove pid/hostname
52
- timestamp: false, // remove time
53
- formatters: {
54
- level: () => ({}) // remove level
55
- },
56
- hooks: {
57
- logMethod(args, method, level) {
58
- // pino level is numeric (info = 30)
59
- if (level === 30) {
60
- const msg =
61
- typeof args[0] === 'string'
62
- ? args[0]
63
- : typeof args[1] === 'string'
64
- ? args[1]
65
- : '';
66
- if (shouldSuppressInfo(msg)) {
67
- return;
11
+ const TESTLENS_X_WORKFLOW_AUTH =
12
+ 'd5f8f1d1a4546e9b49dcedbe51f483aca4bb0e2357b513082b0ce49e59583d38';
13
+
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
+ private pinoLogger: pino.Logger;
23
+ private logLevel: 'info' | 'debug';
24
+ private runId?: string;
25
+
26
+ constructor(logLevel: 'info' | 'debug' = 'info') {
27
+ this.logLevel = logLevel;
28
+
29
+ // Create base pino logger with CloudWatch-compatible settings
30
+ this.pinoLogger = pino({
31
+ level: 'debug', // Always log at debug level, we filter manually
32
+ base: null, // remove pid/hostname - CloudWatch provides this
33
+ timestamp: false, // CloudWatch adds timestamps
34
+ formatters: {
35
+ level: (label) => {
36
+ return { level: label }; // Include level for CloudWatch filtering
68
37
  }
69
38
  }
70
- return method.apply(this, args as any);
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Set run ID context for all subsequent logs
44
+ * This allows correlating all logs to a specific test run in CloudWatch
45
+ */
46
+ setRunId(runId: string): void {
47
+ this.runId = runId;
48
+
49
+ // Recreate logger with runId context for CloudWatch querying
50
+ this.pinoLogger = this.pinoLogger.child({ runId });
51
+ }
52
+
53
+ /**
54
+ * Check if we're in debug mode
55
+ */
56
+ isDebug(): boolean {
57
+ return this.logLevel === 'debug';
58
+ }
59
+
60
+ /**
61
+ * Debug level logging - only shown when logLevel is 'debug'
62
+ */
63
+ debug(msg: string, ...args: any[]): void {
64
+ if (this.logLevel === 'debug') {
65
+ this.pinoLogger.debug(msg, ...args);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Debug level logging with object
71
+ */
72
+ debugObj(obj: Record<string, any>, msg?: string): void {
73
+ if (this.logLevel === 'debug') {
74
+ this.pinoLogger.debug(obj, msg);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Info level logging - shown for both 'info' and 'debug' levels
80
+ */
81
+ info(msg: string, ...args: any[]): void {
82
+ this.pinoLogger.info(msg, ...args);
83
+ }
84
+
85
+ /**
86
+ * Info level logging with object
87
+ */
88
+ infoObj(obj: Record<string, any>, msg?: string): void {
89
+ this.pinoLogger.info(obj, msg);
90
+ }
91
+
92
+ /**
93
+ * Warn level logging - shown for both 'info' and 'debug' levels
94
+ */
95
+ warn(msg: string, ...args: any[]): void {
96
+ this.pinoLogger.warn(msg, ...args);
97
+ }
98
+
99
+ /**
100
+ * Error level logging - always shown regardless of log level
101
+ */
102
+ error(msg: string | Record<string, any>, ...args: any[]): void {
103
+ if (typeof msg === 'string') {
104
+ this.pinoLogger.error(msg, ...args);
105
+ } else {
106
+ this.pinoLogger.error(msg, ...args);
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Log test case start - debug level only
112
+ * In info mode, we only show logs when there are issues
113
+ */
114
+ logTestStart(testName: string): void {
115
+ this.debug(`[TEST START] ${testName}`);
116
+ }
117
+
118
+ /**
119
+ * Log test case completion - debug level only
120
+ * In info mode, we only show logs when there are issues
121
+ */
122
+ logTestEnd(testName: string, status: string, duration: number): void {
123
+ const statusEmoji = status === 'passed' ? '✓' : status === 'failed' ? '✗' : '○';
124
+ const durationStr = `${duration}ms`;
125
+ this.debug(`[TEST END] ${statusEmoji} ${testName} - ${status} (${durationStr})`);
126
+ }
127
+
128
+ /**
129
+ * Log artifact status - info level only for failures
130
+ * Success is logged at debug level
131
+ */
132
+ logArtifactStatus(testName: string, artifactType: string, status: 'uploaded' | 'skipped' | 'failed'): void {
133
+ if (status === 'failed') {
134
+ this.error(`[ARTIFACT] ✗ ${artifactType} for "${testName}" - failed`);
135
+ } else {
136
+ this.debug(`[ARTIFACT] ${status === 'uploaded' ? '✓' : '○'} ${artifactType} for "${testName}" - ${status}`);
71
137
  }
72
138
  }
73
- });
139
+
140
+ /**
141
+ * Log run summary - debug level only
142
+ * In info mode, we only show logs when there are issues
143
+ */
144
+ logRunSummary(passed: number, failed: number, skipped: number, timedOut: number): void {
145
+ this.debug(`[RUN SUMMARY] ${passed} passed, ${failed} failed (${timedOut} timeouts), ${skipped} skipped`);
146
+ }
147
+ }
148
+
149
+ // Legacy logger for backward compatibility during transition
150
+ // Will be replaced with Logger instance per reporter
151
+ let globalLogger = new Logger('info');
152
+ const logger = {
153
+ get debug() { return globalLogger.debug.bind(globalLogger); },
154
+ get info() { return globalLogger.info.bind(globalLogger); },
155
+ get warn() { return globalLogger.warn.bind(globalLogger); },
156
+ get error() { return globalLogger.error.bind(globalLogger); },
157
+ get levelVal() { return globalLogger.isDebug() ? 20 : 30; }
158
+ };
74
159
 
75
160
  // Lazy-load mime module to support ESM
76
161
  let mimeModule: any = null;
@@ -114,6 +199,12 @@ export interface TestLensReporterConfig {
114
199
  customMetadata?: Record<string, string | string[]>;
115
200
  /** Execution ID for one run per pipeline (e.g. from TESTLENS_EXECUTION_ID or CI build UUID). When set, used as runId so multiple steps share one run. */
116
201
  executionId?: string;
202
+ /**
203
+ * Log level for the reporter.
204
+ * - 'info' (default): Shows test start/completion with status, artifact status, and errors
205
+ * - 'debug': Shows all logs including detailed internal operations
206
+ */
207
+ logLevel?: 'info' | 'debug';
117
208
  }
118
209
 
119
210
  export interface TestLensReporterOptions {
@@ -147,6 +238,12 @@ export interface TestLensReporterOptions {
147
238
  customMetadata?: Record<string, string | string[]>;
148
239
  /** Execution ID for one run per pipeline (e.g. from TESTLENS_EXECUTION_ID or CI build UUID). When set, used as runId so multiple steps share one run. */
149
240
  executionId?: string;
241
+ /**
242
+ * Log level for the reporter.
243
+ * - 'info' (default): Shows test start/completion with status, artifact status, and errors
244
+ * - 'debug': Shows all logs including detailed internal operations
245
+ */
246
+ logLevel?: 'info' | 'debug';
150
247
  }
151
248
 
152
249
  export interface GitInfo {
@@ -256,13 +353,24 @@ export class TestLensReporter implements Reporter {
256
353
  private runCreationFailed: boolean = false; // Track if run creation failed due to limits
257
354
  private cliArgs: Record<string, any> = {}; // Store CLI args separately
258
355
  private pendingUploads: Set<Promise<any>> = new Set(); // Track pending artifact uploads
259
- private traceNetworkRows: unknown[] = []; // Network requests/responses from trace zip for current test
260
356
  private artifactStats = {
261
357
  uploaded: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
262
358
  skipped: { screenshot: 0, video: 0, trace: 0, attachment: 0 },
263
359
  failed: { screenshot: 0, video: 0, trace: 0, attachment: 0 }
264
360
  };
265
361
  private artifactsSeen: number = 0;
362
+ private logger: Logger; // Instance logger with proper context
363
+
364
+ /**
365
+ * Get environment variable value with case-insensitive key match (e.g. TL_BUILDNAME, tl_buildname).
366
+ */
367
+ private static getEnvCaseInsensitive(name: string): string | undefined {
368
+ const upper = name.toUpperCase();
369
+ for (const key of Object.keys(process.env)) {
370
+ if (key.toUpperCase() === upper) return process.env[key];
371
+ }
372
+ return undefined;
373
+ }
266
374
 
267
375
  /**
268
376
  * Parse custom metadata from environment variables
@@ -273,11 +381,12 @@ export class TestLensReporter implements Reporter {
273
381
 
274
382
  // Common environment variable names for build metadata
275
383
  const envVarMappings: Record<string, string[]> = {
276
- // Support both TestLens-specific names (recommended) and common CI names
277
- 'testlensBuildTag': ['testlensBuildTag', 'TESTLENS_BUILD_TAG', 'TESTLENS_BUILDTAG', 'BUILDTAG', 'BUILD_TAG', 'TestlensBuildTag', 'TestLensBuildTag'],
278
- 'testlensBuildName': ['testlensBuildName', 'TESTLENS_BUILD_NAME', 'TESTLENS_BUILDNAME', 'BUILDNAME', 'BUILD_NAME', 'TestlensBuildName', 'TestLensBuildName'],
384
+ // Support both TestLens-specific names (recommended) and common CI names; TL_* are short aliases; keys matched case-insensitively
385
+ 'testlensBuildTag': ['TL_BUILDTAG', 'testlensBuildTag', 'TESTLENS_BUILD_TAG', 'TESTLENS_BUILDTAG', 'BUILDTAG', 'BUILD_TAG', 'TestlensBuildTag', 'TestLensBuildTag'],
386
+ 'testlensBuildName': ['TL_BUILDNAME', 'testlensBuildName', 'TESTLENS_BUILD_NAME', 'TESTLENS_BUILDNAME', 'BUILDNAME', 'BUILD_NAME', 'TestlensBuildName', 'TestLensBuildName'],
279
387
  // Execution ID for one run per pipeline (checked in order; prefer TESTLENS_EXECUTION_ID, then CI-specific UUIDs)
280
388
  'executionId': [
389
+ 'TL_EXECUTIONID',
281
390
  'TESTLENS_EXECUTION_ID',
282
391
  'TestlensExecutionId',
283
392
  'TestLensExecutionId',
@@ -300,10 +409,10 @@ export class TestLensReporter implements Reporter {
300
409
  'customvalue': ['CUSTOMVALUE', 'CUSTOM_VALUE']
301
410
  };
302
411
 
303
- // Check for each metadata key
412
+ // Check for each metadata key (case-insensitive env lookup)
304
413
  Object.entries(envVarMappings).forEach(([key, envVars]) => {
305
414
  for (const envVar of envVars) {
306
- const value = process.env[envVar];
415
+ const value = TestLensReporter.getEnvCaseInsensitive(envVar);
307
416
  if (value) {
308
417
  // For testlensBuildTag, support comma-separated values
309
418
  if (key === 'testlensBuildTag' && value.includes(',')) {
@@ -322,6 +431,16 @@ export class TestLensReporter implements Reporter {
322
431
  }
323
432
 
324
433
  constructor(options: TestLensReporterOptions) {
434
+ // Initialize logger first with user-configured log level
435
+ const logLevel = options.logLevel ||
436
+ (process.env.TESTLENS_LOG_LEVEL as 'info' | 'debug') ||
437
+ (process.env.LOG_LEVEL === 'debug' ? 'debug' : 'info') ||
438
+ 'info';
439
+ this.logger = new Logger(logLevel);
440
+
441
+ // Update global logger reference for any legacy code
442
+ globalLogger = this.logger;
443
+
325
444
  // Parse custom CLI arguments
326
445
  const customArgs = TestLensReporter.parseCustomArgs();
327
446
  this.cliArgs = customArgs; // Store CLI args separately for later use
@@ -354,7 +473,8 @@ export class TestLensReporter implements Reporter {
354
473
  rejectUnauthorized: options.rejectUnauthorized,
355
474
  ignoreSslErrors: options.ignoreSslErrors,
356
475
  customMetadata: { ...options.customMetadata, ...customArgs }, // Config metadata first, then CLI args override
357
- executionId: options.executionId
476
+ executionId: options.executionId,
477
+ logLevel: logLevel
358
478
  } as Required<TestLensReporterConfig>;
359
479
 
360
480
  if (!this.config.apiKey) {
@@ -362,7 +482,7 @@ export class TestLensReporter implements Reporter {
362
482
  }
363
483
 
364
484
  if (apiKey !== options.apiKey) {
365
- logger.debug('✓ Using API key from environment variable');
485
+ this.logger.debug('✓ Using API key from environment variable');
366
486
  }
367
487
 
368
488
  // Default environment to allow self-signed certs unless explicitly set
@@ -376,15 +496,15 @@ export class TestLensReporter implements Reporter {
376
496
  // Check various ways SSL validation can be disabled or enforced (in order of precedence)
377
497
  if (this.config.ignoreSslErrors === true) {
378
498
  rejectUnauthorized = false;
379
- logger.debug('[DEBUG] SSL certificate validation disabled via ignoreSslErrors option');
499
+ this.logger.debug('[DEBUG] SSL certificate validation disabled via ignoreSslErrors option');
380
500
  } else if (this.config.rejectUnauthorized === false) {
381
501
  rejectUnauthorized = false;
382
- logger.debug('[DEBUG] SSL certificate validation disabled via rejectUnauthorized option');
502
+ this.logger.debug('[DEBUG] SSL certificate validation disabled via rejectUnauthorized option');
383
503
  } else if (this.config.rejectUnauthorized === true) {
384
504
  rejectUnauthorized = true;
385
505
  } else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
386
506
  rejectUnauthorized = false;
387
- logger.debug('[DEBUG] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
507
+ this.logger.debug('[DEBUG] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
388
508
  } else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1') {
389
509
  rejectUnauthorized = true;
390
510
  }
@@ -398,6 +518,7 @@ export class TestLensReporter implements Reporter {
398
518
  timeout: this.config.timeout,
399
519
  headers: {
400
520
  'Content-Type': 'application/json',
521
+ 'x-workflow-auth': TESTLENS_X_WORKFLOW_AUTH,
401
522
  ...(this.config.apiKey && { 'X-API-Key': this.config.apiKey }),
402
523
  },
403
524
  // Enhanced SSL handling with flexible TLS configuration
@@ -444,6 +565,10 @@ export class TestLensReporter implements Reporter {
444
565
  const executionId = typeof executionIdRaw === 'string' && executionIdRaw.trim() ? String(executionIdRaw).trim() : undefined;
445
566
  this.runId = executionId || randomUUID();
446
567
  this.usedExecutionId = !!executionId;
568
+
569
+ // Set runId in logger context for CloudWatch correlation
570
+ this.logger.setRunId(this.runId);
571
+
447
572
  this.runMetadata = this.initializeRunMetadata();
448
573
  this.specMap = new Map<string, SpecData>();
449
574
  this.testMap = new Map<string, TestData>();
@@ -451,11 +576,11 @@ export class TestLensReporter implements Reporter {
451
576
 
452
577
  // Log custom metadata if any
453
578
  if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
454
- logger.debug('\n[METADATA] Custom Metadata Detected:');
579
+ this.logger.debug('\n[METADATA] Custom Metadata Detected:');
455
580
  Object.entries(this.config.customMetadata).forEach(([key, value]) => {
456
- logger.debug(` ${key}: ${value}`);
581
+ this.logger.debug(` ${key}: ${value}`);
457
582
  });
458
- logger.debug('');
583
+ this.logger.debug('');
459
584
  }
460
585
  }
461
586
 
@@ -525,25 +650,20 @@ export class TestLensReporter implements Reporter {
525
650
  }
526
651
 
527
652
  async onBegin(config: FullConfig, suite: Suite): Promise<void> {
528
- logger.debug(`[REPORTER] source=${__filename} enableArtifacts=${this.config.enableArtifacts}`);
529
- // Show Build Name if provided, otherwise show Run ID
530
- if (this.runMetadata.testlensBuildName) {
531
- logger.debug(`TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
532
- logger.debug(` Run ID: ${this.runId}`);
533
- } else {
534
- logger.debug(`TestLens Reporter starting - Run ID: ${this.runId}`);
535
- }
653
+ this.logger.debug(`[REPORTER] source=${__filename} enableArtifacts=${this.config.enableArtifacts}`);
654
+ // Only show startup info in debug mode - in info mode we only log issues
655
+ this.logger.debug(`TestLens Reporter starting - Run ID: ${this.runId}`);
536
656
 
537
657
  // Collect Git information if enabled
538
658
  if (this.config.enableGitInfo) {
539
659
  this.runMetadata.gitInfo = await this.collectGitInfo();
540
660
  if (this.runMetadata.gitInfo) {
541
- logger.debug(`Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
661
+ this.logger.debug(`Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
542
662
  } else {
543
- logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
663
+ this.logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
544
664
  }
545
665
  } else {
546
- logger.debug(`[INFO] Git info collection disabled (enableGitInfo: false)`);
666
+ this.logger.debug(`[INFO] Git info collection disabled (enableGitInfo: false)`);
547
667
  }
548
668
 
549
669
  // Add shard information if available
@@ -564,8 +684,8 @@ export class TestLensReporter implements Reporter {
564
684
  }
565
685
 
566
686
  async onTestBegin(test: TestCase, result: TestResult): Promise<void> {
567
- // Log which test is starting
568
- logger.debug(`[TEST] Running test: ${test.title}`);
687
+ // Log which test is starting (info level for test start)
688
+ this.logger.logTestStart(test.title);
569
689
 
570
690
  const specPath = test.location.file;
571
691
  const specKey = `${specPath}-${test.parent.title}`;
@@ -643,30 +763,16 @@ export class TestLensReporter implements Reporter {
643
763
  }
644
764
 
645
765
  async onTestEnd(test: TestCase, result: TestResult): Promise<void> {
646
- this.traceNetworkRows = []; // Reset at start of each test
766
+ this.logger.debug(`[ARTIFACTS] attachments=${result.attachments?.length ?? 0}`);
767
+
647
768
  const testId = this.getTestId(test);
648
769
  let testCaseId = '';
770
+ const localTraceNetworkRows: unknown[] = [];
649
771
  let testData = this.testMap.get(testId);
650
772
 
651
- logger.debug(`[ARTIFACTS] attachments=${result.attachments?.length ?? 0}`);
652
-
653
- if (result.attachments && result.attachments.length > 0) {
654
- for (const attachment of result.attachments) {
655
- const artifactType = this.getArtifactType(attachment.name);
656
- this.artifactsSeen += 1;
657
- if (attachment.path) {
658
- this.bumpArtifactStat('uploaded', artifactType);
659
- } else {
660
- this.bumpArtifactStat('skipped', artifactType);
661
- }
662
- }
663
- }
664
-
665
- logger.debug(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
666
-
667
773
  // For skipped tests, onTestBegin might not be called, so we need to create the test data here
668
774
  if (!testData) {
669
- logger.debug(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
775
+ this.logger.debug(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
670
776
  // Create spec data if not exists (skipped tests might not have spec data either)
671
777
  const specPath = test.location.file;
672
778
  const specKey = `${specPath}-${test.parent.title}`;
@@ -832,7 +938,7 @@ export class TestLensReporter implements Reporter {
832
938
 
833
939
  // Send testEnd event for all tests, regardless of status
834
940
  // This ensures tests that are interrupted or have unexpected statuses are properly recorded
835
- logger.debug(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
941
+ this.logger.debug(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
836
942
  // Send test end event to API and get response
837
943
  const testEndResponse = await this.sendToApi({
838
944
  type: 'testEnd',
@@ -842,14 +948,17 @@ export class TestLensReporter implements Reporter {
842
948
  });
843
949
  testCaseId = testEndResponse?.testCaseId;
844
950
 
951
+ // Log test completion with status (info level)
952
+ this.logger.logTestEnd(test.title, testData.status, testData.duration);
953
+
845
954
  // Handle artifacts (test case is now guaranteed to be in database)
846
955
  if (this.config.enableArtifacts) {
847
956
  // Pass test case DB ID if available for faster lookups; pass status/endTime so backend
848
957
  // can fix up test case status if testEnd failed but artifact request succeeds
849
- await this.processArtifacts(testId, result, testEndResponse?.testCaseId, {
958
+ await this.processArtifacts(testId, test.title, result, testEndResponse?.testCaseId, {
850
959
  status: testData.status,
851
960
  endTime: testData.endTime
852
- });
961
+ }, localTraceNetworkRows);
853
962
  } else if (result.attachments && result.attachments.length > 0) {
854
963
  for (const attachment of result.attachments) {
855
964
  const artifactType = this.getArtifactType(attachment.name);
@@ -922,7 +1031,7 @@ export class TestLensReporter implements Reporter {
922
1031
  });
923
1032
 
924
1033
  // Send spec code blocks to API
925
- await this.sendSpecCodeBlocks(specPath, test.title, testData?.errors || [], this.runId, testId, testCaseId);
1034
+ await this.sendSpecCodeBlocks(specPath, test.title, testData?.errors || [], this.runId, testId, testCaseId, localTraceNetworkRows);
926
1035
  }
927
1036
  }
928
1037
  }
@@ -933,29 +1042,36 @@ export class TestLensReporter implements Reporter {
933
1042
 
934
1043
  // Wait for all pending artifact uploads to complete before sending runEnd
935
1044
  if (this.pendingUploads.size > 0) {
936
- logger.debug(`Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
1045
+ this.logger.debug(`Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
937
1046
  try {
938
1047
  await Promise.all(Array.from(this.pendingUploads));
939
- logger.debug(`[OK] All artifact uploads completed`);
1048
+ this.logger.debug(`[OK] All artifact uploads completed`);
940
1049
  } catch (error) {
941
- logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
1050
+ this.logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
942
1051
  }
943
1052
  }
944
1053
 
945
1054
  const uploaded = this.artifactStats.uploaded;
946
1055
  const skipped = this.artifactStats.skipped;
947
1056
  const failed = this.artifactStats.failed;
948
- // Always show summary so users can confirm artifact settings
1057
+
1058
+ // Calculate total uploaded/failed for info level summary
1059
+ const totalUploaded = uploaded.screenshot + uploaded.video + uploaded.trace + uploaded.attachment;
1060
+ const totalFailed = failed.screenshot + failed.video + failed.trace + failed.attachment;
1061
+ const totalSkipped = skipped.screenshot + skipped.video + skipped.trace + skipped.attachment;
1062
+
1063
+ // Info level: only show artifact summary if there are failures
1064
+ if (totalFailed > 0) {
1065
+ this.logger.error(`[ARTIFACTS] ${totalUploaded} uploaded, ${totalFailed} failed, ${totalSkipped} skipped`);
1066
+ }
1067
+
1068
+ // Debug level: show detailed breakdown
949
1069
  const summary =
950
1070
  `[ARTIFACTS] seen=${this.artifactsSeen} | ` +
951
1071
  `uploaded screenshot=${uploaded.screenshot}, video=${uploaded.video}, trace=${uploaded.trace}, attachment=${uploaded.attachment} | ` +
952
1072
  `skipped screenshot=${skipped.screenshot}, video=${skipped.video}, trace=${skipped.trace}, attachment=${skipped.attachment} | ` +
953
1073
  `failed screenshot=${failed.screenshot}, video=${failed.video}, trace=${failed.trace}, attachment=${failed.attachment}`;
954
- if (logger.levelVal > 30) {
955
- console.log(summary);
956
- } else {
957
- logger.debug(summary);
958
- }
1074
+ this.logger.debug(summary);
959
1075
 
960
1076
  // Calculate final stats
961
1077
  const totalTests = Array.from(this.testMap.values()).length;
@@ -987,14 +1103,11 @@ export class TestLensReporter implements Reporter {
987
1103
  }
988
1104
  });
989
1105
 
990
- // Show Build Name if provided, otherwise show Run ID
991
- if (this.runMetadata.testlensBuildName) {
992
- logger.debug(`[COMPLETE] TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
993
- logger.debug(` Run ID: ${this.runId}`);
994
- } else {
995
- logger.debug(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
996
- }
997
- logger.debug(`[RESULTS] ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
1106
+ // Only show completion info in debug mode - in info mode we only log issues
1107
+ this.logger.debug(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
1108
+
1109
+ // Log run summary at debug level
1110
+ this.logger.logRunSummary(passedTests, failedTests, skippedTests, timedOutTests);
998
1111
  }
999
1112
 
1000
1113
  private async sendToApi(payload: any): Promise<any> {
@@ -1010,8 +1123,9 @@ export class TestLensReporter implements Reporter {
1010
1123
  }
1011
1124
  });
1012
1125
  if (this.config.enableRealTimeStream) {
1013
- logger.debug(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
1126
+ this.logger.debug(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
1014
1127
  }
1128
+
1015
1129
  // Return response data for caller to use
1016
1130
  return response.data;
1017
1131
  } catch (error: any) {
@@ -1025,25 +1139,25 @@ export class TestLensReporter implements Reporter {
1025
1139
  this.runCreationFailed = true;
1026
1140
  }
1027
1141
 
1028
- logger.error('\n' + '='.repeat(80));
1142
+ this.logger.error('\n' + '='.repeat(80));
1029
1143
  if (errorData?.limit_type === 'test_cases') {
1030
- logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
1144
+ this.logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
1031
1145
  } else if (errorData?.limit_type === 'test_runs') {
1032
- logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
1146
+ this.logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
1033
1147
  } else {
1034
- logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
1148
+ this.logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
1035
1149
  }
1036
- logger.error('='.repeat(80));
1037
- logger.error('');
1038
- logger.error(errorData?.message || 'You have reached your plan limit.');
1039
- logger.error('');
1040
- logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
1041
- logger.error('');
1042
- logger.error('To continue, please upgrade your plan.');
1043
- logger.error('Contact: support@alternative-path.com');
1044
- logger.error('');
1045
- logger.error('='.repeat(80));
1046
- logger.error('');
1150
+ this.logger.error('='.repeat(80));
1151
+ this.logger.error('');
1152
+ this.logger.error(errorData?.message || 'You have reached your plan limit.');
1153
+ this.logger.error('');
1154
+ this.logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
1155
+ this.logger.error('');
1156
+ this.logger.error('To continue, please upgrade your plan.');
1157
+ this.logger.error('Contact: support@alternative-path.com');
1158
+ this.logger.error('');
1159
+ this.logger.error('='.repeat(80));
1160
+ this.logger.error('');
1047
1161
  return; // Don't log the full error object for limit errors
1048
1162
  }
1049
1163
 
@@ -1051,35 +1165,35 @@ export class TestLensReporter implements Reporter {
1051
1165
  if (status === 401) {
1052
1166
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
1053
1167
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
1054
- logger.error('\n' + '='.repeat(80));
1168
+ this.logger.error('\n' + '='.repeat(80));
1055
1169
 
1056
1170
  if (errorData?.error === 'test_cases_limit_reached') {
1057
- logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
1171
+ this.logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
1058
1172
  } else if (errorData?.error === 'test_runs_limit_reached') {
1059
- logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
1173
+ this.logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
1060
1174
  } else {
1061
- logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
1175
+ this.logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
1062
1176
  }
1063
1177
 
1064
- logger.error('='.repeat(80));
1065
- logger.error('');
1066
- logger.error(errorData?.message || 'Your trial period has expired.');
1067
- logger.error('');
1068
- logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
1069
- logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1070
- logger.error('');
1178
+ this.logger.error('='.repeat(80));
1179
+ this.logger.error('');
1180
+ this.logger.error(errorData?.message || 'Your trial period has expired.');
1181
+ this.logger.error('');
1182
+ this.logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
1183
+ this.logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1184
+ this.logger.error('');
1071
1185
  if (errorData?.trial_end_date) {
1072
- logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1073
- logger.error('');
1186
+ this.logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1187
+ this.logger.error('');
1074
1188
  }
1075
- logger.error('='.repeat(80));
1076
- logger.error('');
1189
+ this.logger.error('='.repeat(80));
1190
+ this.logger.error('');
1077
1191
  } else {
1078
- logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
1192
+ this.logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
1079
1193
  }
1080
1194
  } else if (status !== 403) {
1081
1195
  // Log other errors (but not 403 which we handled above)
1082
- logger.error({
1196
+ this.logger.error({
1083
1197
  message: `[ERROR] Failed to send ${payload.type} event to TestLens`,
1084
1198
  error: error?.message || 'Unknown error',
1085
1199
  status: status,
@@ -1097,9 +1211,11 @@ export class TestLensReporter implements Reporter {
1097
1211
 
1098
1212
  private async processArtifacts(
1099
1213
  testId: string,
1214
+ testName: string,
1100
1215
  result: TestResult,
1101
1216
  testCaseDbId?: string,
1102
- testEndPayload?: { status: string; endTime: string }
1217
+ testEndPayload?: { status: string; endTime: string },
1218
+ traceNetworkRows?: unknown[]
1103
1219
  ): Promise<void> {
1104
1220
  // Skip artifact processing if run creation failed
1105
1221
  if (this.runCreationFailed) {
@@ -1110,6 +1226,7 @@ export class TestLensReporter implements Reporter {
1110
1226
 
1111
1227
  for (const attachment of attachments) {
1112
1228
  const artifactType = this.getArtifactType(attachment.name);
1229
+ this.artifactsSeen += 1;
1113
1230
  if (attachment.path) {
1114
1231
  // Check if attachment should be processed based on config
1115
1232
  const isVideo = artifactType === 'video' || attachment.contentType?.startsWith('video/');
@@ -1117,14 +1234,14 @@ export class TestLensReporter implements Reporter {
1117
1234
 
1118
1235
  // Skip video if disabled in config
1119
1236
  if (isVideo && !this.config.enableVideo) {
1120
- logger.debug(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
1237
+ this.logger.debug(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
1121
1238
  this.bumpArtifactStat('skipped', artifactType);
1122
1239
  continue;
1123
1240
  }
1124
1241
 
1125
1242
  // Skip screenshot if disabled in config
1126
1243
  if (isScreenshot && !this.config.enableScreenshot) {
1127
- logger.debug(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
1244
+ this.logger.debug(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
1128
1245
  this.bumpArtifactStat('skipped', artifactType);
1129
1246
  continue;
1130
1247
  }
@@ -1182,14 +1299,11 @@ export class TestLensReporter implements Reporter {
1182
1299
  }
1183
1300
  }
1184
1301
  const durationMs = Date.now() - traceStart;
1185
- if (networkRows.length > 0) {
1186
- this.traceNetworkRows = networkRows;
1187
- } else {
1188
- this.traceNetworkRows = [];
1302
+ if (networkRows.length > 0 && traceNetworkRows) {
1303
+ traceNetworkRows.push(...networkRows);
1189
1304
  }
1190
1305
  } catch (e) {
1191
- logger.warn('[TRACE] Could not read trace zip: ' + (e && (e as Error).message));
1192
- this.traceNetworkRows = [];
1306
+ this.logger.warn('[TRACE] Could not read trace zip: ' + (e && (e as Error).message));
1193
1307
  }
1194
1308
  }
1195
1309
 
@@ -1229,7 +1343,7 @@ export class TestLensReporter implements Reporter {
1229
1343
  const uploadPromise = Promise.resolve().then(async () => {
1230
1344
  try {
1231
1345
  if (!attachment.path) {
1232
- logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
1346
+ this.logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
1233
1347
  this.bumpArtifactStat('skipped', artifactType);
1234
1348
  return;
1235
1349
  }
@@ -1238,7 +1352,7 @@ export class TestLensReporter implements Reporter {
1238
1352
 
1239
1353
  // Skip if upload failed or file was too large
1240
1354
  if (!s3Data) {
1241
- logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
1355
+ this.logger.debug(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
1242
1356
  this.bumpArtifactStat('failed', artifactType);
1243
1357
  return;
1244
1358
  }
@@ -1268,9 +1382,14 @@ export class TestLensReporter implements Reporter {
1268
1382
  artifact: artifactData
1269
1383
  });
1270
1384
 
1271
- logger.debug(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
1385
+ // Log artifact upload success at info level
1386
+ this.logger.logArtifactStatus(testName, artifactType, 'uploaded');
1387
+ this.bumpArtifactStat('uploaded', artifactType);
1388
+ this.logger.debug(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
1272
1389
  } catch (error) {
1273
- logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${(error as Error).message}`);
1390
+ // Log artifact upload failure at error level
1391
+ this.logger.logArtifactStatus(testName, artifactType, 'failed');
1392
+ this.logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${(error as Error).message}`);
1274
1393
  this.bumpArtifactStat('failed', artifactType);
1275
1394
  }
1276
1395
  });
@@ -1284,7 +1403,7 @@ export class TestLensReporter implements Reporter {
1284
1403
  // Don't await here - let uploads happen in parallel
1285
1404
  // They will be awaited in onEnd
1286
1405
  } catch (error) {
1287
- logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${(error as Error).message}`);
1406
+ this.logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${(error as Error).message}`);
1288
1407
  this.bumpArtifactStat('failed', artifactType);
1289
1408
  }
1290
1409
  } else {
@@ -1293,7 +1412,7 @@ export class TestLensReporter implements Reporter {
1293
1412
  }
1294
1413
  }
1295
1414
 
1296
- private async sendSpecCodeBlocks(specPath: string, testName: string, errors: unknown[], runId: string, test_id: string, testCaseId: string): Promise<void> {
1415
+ private async sendSpecCodeBlocks(specPath: string, testName: string, errors: unknown[], runId: string, test_id: string, testCaseId: string, traceNetworkRows: unknown[] = []): Promise<void> {
1297
1416
  try {
1298
1417
  // Extract code blocks using built-in parser
1299
1418
  const testBlocks = this.extractTestBlocks(specPath);
@@ -1320,24 +1439,24 @@ export class TestLensReporter implements Reporter {
1320
1439
  filePath: path.relative(process.cwd(), specPath),
1321
1440
  codeBlocks,
1322
1441
  errors,
1323
- traceNetworkRows: this.traceNetworkRows,
1442
+ traceNetworkRows: traceNetworkRows,
1324
1443
  testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, ''),
1325
1444
  runId,
1326
1445
  test_id,
1327
1446
  testCaseId
1328
1447
  });
1329
1448
 
1330
- logger.debug(`Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
1449
+ this.logger.debug(`Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
1331
1450
  } catch (error: any) {
1332
1451
  const errorData = error?.response?.data;
1333
1452
 
1334
1453
  // Handle duplicate spec code blocks gracefully (when re-running tests)
1335
1454
  if (errorData?.error && errorData.error.includes('duplicate key value violates unique constraint')) {
1336
- logger.debug(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
1455
+ this.logger.debug(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
1337
1456
  return;
1338
1457
  }
1339
1458
 
1340
- logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
1459
+ this.logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
1341
1460
  }
1342
1461
  }
1343
1462
 
@@ -1444,7 +1563,7 @@ export class TestLensReporter implements Reporter {
1444
1563
 
1445
1564
  return blocks;
1446
1565
  } catch (error: any) {
1447
- logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
1566
+ this.logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
1448
1567
  return [];
1449
1568
  }
1450
1569
  }
@@ -1468,7 +1587,7 @@ export class TestLensReporter implements Reporter {
1468
1587
  }
1469
1588
  } catch (e) {
1470
1589
  // Remote info is optional - handle gracefully
1471
- logger.debug('[INFO] No git remote configured, skipping remote info');
1590
+ this.logger.debug('[INFO] No git remote configured, skipping remote info');
1472
1591
  }
1473
1592
 
1474
1593
  const isDirty = execSync('git status --porcelain', { encoding: 'utf-8' }).trim().length > 0;
@@ -1556,7 +1675,7 @@ export class TestLensReporter implements Reporter {
1556
1675
  const fileSize = this.getFileSize(filePath);
1557
1676
  const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
1558
1677
 
1559
- logger.debug(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
1678
+ this.logger.debug(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
1560
1679
 
1561
1680
  const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
1562
1681
 
@@ -1587,7 +1706,7 @@ export class TestLensReporter implements Reporter {
1587
1706
  const { uploadUrl, s3Key, metadata } = presignedResponse.data;
1588
1707
 
1589
1708
  // Step 2: Upload directly to S3 using presigned URL
1590
- logger.debug(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1709
+ this.logger.debug(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1591
1710
 
1592
1711
  const fileBuffer = fs.readFileSync(filePath);
1593
1712
 
@@ -1609,7 +1728,7 @@ export class TestLensReporter implements Reporter {
1609
1728
  throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
1610
1729
  }
1611
1730
 
1612
- logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1731
+ this.logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1613
1732
 
1614
1733
  // Step 3: Confirm upload with server to save metadata
1615
1734
  const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
@@ -1634,7 +1753,7 @@ export class TestLensReporter implements Reporter {
1634
1753
 
1635
1754
  if (confirmResponse.status === 201 && confirmResponse.data.success) {
1636
1755
  const artifact = confirmResponse.data.artifact;
1637
- logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
1756
+ this.logger.debug(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
1638
1757
  return {
1639
1758
  key: s3Key,
1640
1759
  url: artifact.s3Url,
@@ -1652,29 +1771,29 @@ export class TestLensReporter implements Reporter {
1652
1771
 
1653
1772
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
1654
1773
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
1655
- logger.error('\n' + '='.repeat(80));
1774
+ this.logger.error('\n' + '='.repeat(80));
1656
1775
 
1657
1776
  if (errorData?.error === 'test_cases_limit_reached') {
1658
- logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
1777
+ this.logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
1659
1778
  } else if (errorData?.error === 'test_runs_limit_reached') {
1660
- logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
1779
+ this.logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
1661
1780
  } else {
1662
- logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
1781
+ this.logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
1663
1782
  }
1664
1783
 
1665
- logger.error('='.repeat(80));
1666
- logger.error('');
1667
- logger.error(errorData?.message || 'Your trial period has expired.');
1668
- logger.error('');
1669
- logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
1670
- logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1671
- logger.error('');
1784
+ this.logger.error('='.repeat(80));
1785
+ this.logger.error('');
1786
+ this.logger.error(errorData?.message || 'Your trial period has expired.');
1787
+ this.logger.error('');
1788
+ this.logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
1789
+ this.logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1790
+ this.logger.error('');
1672
1791
  if (errorData?.trial_end_date) {
1673
- logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1674
- logger.error('');
1792
+ this.logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1793
+ this.logger.error('');
1675
1794
  }
1676
- logger.error('='.repeat(80));
1677
- logger.error('');
1795
+ this.logger.error('='.repeat(80));
1796
+ this.logger.error('');
1678
1797
  return null;
1679
1798
  }
1680
1799
  }
@@ -1692,9 +1811,9 @@ export class TestLensReporter implements Reporter {
1692
1811
  errorMsg = `Access denied (403) - presigned URL may have expired`;
1693
1812
  }
1694
1813
 
1695
- logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
1814
+ this.logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
1696
1815
  if (error.response?.data) {
1697
- logger.error({ errorDetails: error.response.data }, 'Error details');
1816
+ this.logger.error({ errorDetails: error.response.data }, 'Error details');
1698
1817
  }
1699
1818
 
1700
1819
  // Don't throw, just return null to continue with other artifacts
@@ -1741,7 +1860,7 @@ export class TestLensReporter implements Reporter {
1741
1860
  const stats = fs.statSync(filePath);
1742
1861
  return stats.size;
1743
1862
  } catch (error) {
1744
- logger.warn(`Could not get file size for ${filePath}: ${(error as Error).message}`);
1863
+ this.logger.warn(`Could not get file size for ${filePath}: ${(error as Error).message}`);
1745
1864
  return 0;
1746
1865
  }
1747
1866
  }