@alternative-path/testlens-playwright-reporter 0.4.4 → 0.4.5

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 (4) hide show
  1. package/index.js +118 -101
  2. package/index.ts +119 -102
  3. package/package.json +2 -1
  4. package/postinstall.js +8 -3
package/index.js CHANGED
@@ -9,6 +9,11 @@ const fs = tslib_1.__importStar(require("fs"));
9
9
  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
+ const pino_1 = tslib_1.__importDefault(require("pino"));
13
+ // Create pino logger instance
14
+ const logger = (0, pino_1.default)({
15
+ level: process.env.LOG_LEVEL || 'info'
16
+ });
12
17
  // Lazy-load mime module to support ESM
13
18
  let mimeModule = null;
14
19
  async function getMime() {
@@ -45,11 +50,11 @@ class TestLensReporter {
45
50
  // For testlensBuildTag, support comma-separated values
46
51
  if (key === 'testlensBuildTag' && value.includes(',')) {
47
52
  customArgs[key] = value.split(',').map(tag => tag.trim()).filter(tag => tag);
48
- console.log(`✓ Found ${envVar}=${value} (mapped to '${key}' as array of ${customArgs[key].length} tags)`);
53
+ logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}' as array of ${customArgs[key].length} tags)`);
49
54
  }
50
55
  else {
51
56
  customArgs[key] = value;
52
- console.log(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
57
+ logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
53
58
  }
54
59
  break; // Use first match
55
60
  }
@@ -88,32 +93,43 @@ class TestLensReporter {
88
93
  flushInterval: options.flushInterval || 5000,
89
94
  retryAttempts: options.retryAttempts !== undefined ? options.retryAttempts : 0,
90
95
  timeout: options.timeout || 60000,
96
+ rejectUnauthorized: options.rejectUnauthorized,
97
+ ignoreSslErrors: options.ignoreSslErrors,
91
98
  customMetadata: { ...options.customMetadata, ...customArgs } // Config metadata first, then CLI args override
92
99
  };
93
100
  if (!this.config.apiKey) {
94
101
  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.');
95
102
  }
96
103
  if (apiKey !== options.apiKey) {
97
- console.log('✓ Using API key from environment variable');
104
+ logger.info('✓ Using API key from environment variable');
105
+ }
106
+ // Default environment to allow self-signed certs unless explicitly set
107
+ if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === undefined) {
108
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
98
109
  }
99
110
  // Determine SSL validation behavior
100
- let rejectUnauthorized = true; // Default to secure
101
- // Check various ways SSL validation can be disabled (in order of precedence)
102
- if (this.config.ignoreSslErrors) {
103
- // Explicit configuration option
111
+ let rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0'; // Default to secure unless explicitly disabled
112
+ // Check various ways SSL validation can be disabled or enforced (in order of precedence)
113
+ if (this.config.ignoreSslErrors === true) {
104
114
  rejectUnauthorized = false;
105
- console.log('⚠️ SSL certificate validation disabled via ignoreSslErrors option');
115
+ logger.warn('[WARN] SSL certificate validation disabled via ignoreSslErrors option');
106
116
  }
107
117
  else if (this.config.rejectUnauthorized === false) {
108
- // Explicit configuration option
109
118
  rejectUnauthorized = false;
110
- console.log('⚠️ SSL certificate validation disabled via rejectUnauthorized option');
119
+ logger.warn('[WARN] SSL certificate validation disabled via rejectUnauthorized option');
120
+ }
121
+ else if (this.config.rejectUnauthorized === true) {
122
+ rejectUnauthorized = true;
111
123
  }
112
124
  else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
113
- // Environment variable override
114
125
  rejectUnauthorized = false;
115
- console.log('⚠️ SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
126
+ logger.warn('[WARN] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
127
+ }
128
+ else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1') {
129
+ rejectUnauthorized = true;
116
130
  }
131
+ // Mirror the resolved value so all HTTPS requests in this process follow it
132
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = rejectUnauthorized ? '1' : '0';
117
133
  // Set up axios instance with retry logic and enhanced SSL handling
118
134
  this.axiosInstance = axios_1.default.create({
119
135
  baseURL: this.config.apiEndpoint,
@@ -152,11 +168,11 @@ class TestLensReporter {
152
168
  this.runCreationFailed = false;
153
169
  // Log custom metadata if any
154
170
  if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
155
- console.log('\n📋 Custom Metadata Detected:');
171
+ logger.info('\n[METADATA] Custom Metadata Detected:');
156
172
  Object.entries(this.config.customMetadata).forEach(([key, value]) => {
157
- console.log(` ${key}: ${value}`);
173
+ logger.info(` ${key}: ${value}`);
158
174
  });
159
- console.log('');
175
+ logger.info('');
160
176
  }
161
177
  }
162
178
  initializeRunMetadata() {
@@ -221,24 +237,24 @@ class TestLensReporter {
221
237
  async onBegin(config, suite) {
222
238
  // Show Build Name if provided, otherwise show Run ID
223
239
  if (this.runMetadata.testlensBuildName) {
224
- console.log(`🚀 TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
225
- console.log(` Run ID: ${this.runId}`);
240
+ logger.info(`🚀 TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
241
+ logger.info(` Run ID: ${this.runId}`);
226
242
  }
227
243
  else {
228
- console.log(`🚀 TestLens Reporter starting - Run ID: ${this.runId}`);
244
+ logger.info(`🚀 TestLens Reporter starting - Run ID: ${this.runId}`);
229
245
  }
230
246
  // Collect Git information if enabled
231
247
  if (this.config.enableGitInfo) {
232
248
  this.runMetadata.gitInfo = await this.collectGitInfo();
233
249
  if (this.runMetadata.gitInfo) {
234
- console.log(`📦 Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
250
+ logger.info(`📦 Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
235
251
  }
236
252
  else {
237
- console.log(`⚠️ Git info collection returned null - not in a git repository or git not available`);
253
+ logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
238
254
  }
239
255
  }
240
256
  else {
241
- console.log(`ℹ️ Git info collection disabled (enableGitInfo: false)`);
257
+ logger.info(`[INFO] Git info collection disabled (enableGitInfo: false)`);
242
258
  }
243
259
  // Add shard information if available
244
260
  if (config.shard) {
@@ -257,7 +273,7 @@ class TestLensReporter {
257
273
  }
258
274
  async onTestBegin(test, result) {
259
275
  // Log which test is starting
260
- console.log(`\n▶️ Running test: ${test.title}`);
276
+ logger.info(`\n[TEST] Running test: ${test.title}`);
261
277
  const specPath = test.location.file;
262
278
  const specKey = `${specPath}-${test.parent.title}`;
263
279
  // Create or update spec data
@@ -330,10 +346,10 @@ class TestLensReporter {
330
346
  async onTestEnd(test, result) {
331
347
  const testId = this.getTestId(test);
332
348
  let testData = this.testMap.get(testId);
333
- console.log(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
349
+ logger.info(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
334
350
  // For skipped tests, onTestBegin might not be called, so we need to create the test data here
335
351
  if (!testData) {
336
- console.log(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
352
+ logger.info(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
337
353
  // Create spec data if not exists (skipped tests might not have spec data either)
338
354
  const specPath = test.location.file;
339
355
  const specKey = `${specPath}-${test.parent.title}`;
@@ -478,7 +494,7 @@ class TestLensReporter {
478
494
  });
479
495
  // Send testEnd event for all tests, regardless of status
480
496
  // This ensures tests that are interrupted or have unexpected statuses are properly recorded
481
- console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
497
+ logger.info(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
482
498
  // Send test end event to API and get response
483
499
  const testEndResponse = await this.sendToApi({
484
500
  type: 'testEnd',
@@ -561,13 +577,13 @@ class TestLensReporter {
561
577
  this.runMetadata.duration = Date.now() - new Date(this.runMetadata.startTime).getTime();
562
578
  // Wait for all pending artifact uploads to complete before sending runEnd
563
579
  if (this.pendingUploads.size > 0) {
564
- console.log(`⏳ Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
580
+ logger.info(`⏳ Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
565
581
  try {
566
582
  await Promise.all(Array.from(this.pendingUploads));
567
- console.log(`✅ All artifact uploads completed`);
583
+ logger.info(`[OK] All artifact uploads completed`);
568
584
  }
569
585
  catch (error) {
570
- console.error(`⚠️ Some artifact uploads failed, continuing with runEnd`);
586
+ logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
571
587
  }
572
588
  }
573
589
  // Calculate final stats
@@ -598,13 +614,13 @@ class TestLensReporter {
598
614
  });
599
615
  // Show Build Name if provided, otherwise show Run ID
600
616
  if (this.runMetadata.testlensBuildName) {
601
- console.log(`📊 TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
602
- console.log(` Run ID: ${this.runId}`);
617
+ logger.info(`[COMPLETE] TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
618
+ logger.info(` Run ID: ${this.runId}`);
603
619
  }
604
620
  else {
605
- console.log(`📊 TestLens Report completed - Run ID: ${this.runId}`);
621
+ logger.info(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
606
622
  }
607
- console.log(`🎯 Results: ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
623
+ logger.info(`[RESULTS] ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
608
624
  }
609
625
  async sendToApi(payload) {
610
626
  // Skip sending if run creation already failed
@@ -618,7 +634,7 @@ class TestLensReporter {
618
634
  }
619
635
  });
620
636
  if (this.config.enableRealTimeStream) {
621
- console.log(`✅ Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
637
+ logger.info(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
622
638
  }
623
639
  // Return response data for caller to use
624
640
  return response.data;
@@ -632,65 +648,66 @@ class TestLensReporter {
632
648
  if (payload.type === 'runStart' && errorData?.limit_type === 'test_runs') {
633
649
  this.runCreationFailed = true;
634
650
  }
635
- console.error('\n' + '='.repeat(80));
651
+ logger.error('\n' + '='.repeat(80));
636
652
  if (errorData?.limit_type === 'test_cases') {
637
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
653
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
638
654
  }
639
655
  else if (errorData?.limit_type === 'test_runs') {
640
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
656
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
641
657
  }
642
658
  else {
643
- console.error(' TESTLENS ERROR: Plan Limit Reached');
659
+ logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
644
660
  }
645
- console.error('='.repeat(80));
646
- console.error('');
647
- console.error(errorData?.message || 'You have reached your plan limit.');
648
- console.error('');
649
- console.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
650
- console.error('');
651
- console.error('To continue, please upgrade your plan.');
652
- console.error('Contact: support@alternative-path.com');
653
- console.error('');
654
- console.error('='.repeat(80));
655
- console.error('');
661
+ logger.error('='.repeat(80));
662
+ logger.error('');
663
+ logger.error(errorData?.message || 'You have reached your plan limit.');
664
+ logger.error('');
665
+ logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
666
+ logger.error('');
667
+ logger.error('To continue, please upgrade your plan.');
668
+ logger.error('Contact: support@alternative-path.com');
669
+ logger.error('');
670
+ logger.error('='.repeat(80));
671
+ logger.error('');
656
672
  return; // Don't log the full error object for limit errors
657
673
  }
658
674
  // Check for trial expiration, subscription errors, or limit errors (401)
659
675
  if (status === 401) {
660
676
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
661
677
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
662
- console.error('\n' + '='.repeat(80));
678
+ logger.error('\n' + '='.repeat(80));
663
679
  if (errorData?.error === 'test_cases_limit_reached') {
664
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
680
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
665
681
  }
666
682
  else if (errorData?.error === 'test_runs_limit_reached') {
667
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
683
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
668
684
  }
669
685
  else {
670
- console.error(' TESTLENS ERROR: Your trial plan has ended');
686
+ logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
671
687
  }
672
- console.error('='.repeat(80));
673
- console.error('');
674
- console.error(errorData?.message || 'Your trial period has expired.');
675
- console.error('');
676
- console.error('To continue using TestLens, please upgrade to Enterprise plan.');
677
- console.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
678
- console.error('');
688
+ logger.error('='.repeat(80));
689
+ logger.error('');
690
+ logger.error(errorData?.message || 'Your trial period has expired.');
691
+ logger.error('');
692
+ logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
693
+ logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
694
+ logger.error('');
679
695
  if (errorData?.trial_end_date) {
680
- console.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
681
- console.error('');
696
+ logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
697
+ logger.error('');
682
698
  }
683
- console.error('='.repeat(80));
684
- console.error('');
699
+ logger.error('='.repeat(80));
700
+ logger.error('');
685
701
  }
686
702
  else {
687
- console.error(`❌ Authentication failed: ${errorData?.error || 'Invalid API key'}`);
703
+ logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
688
704
  }
689
705
  }
690
706
  else if (status !== 403) {
691
707
  // Log other errors (but not 403 which we handled above)
692
- console.error(`❌ Failed to send ${payload.type} event to TestLens:`, {
693
- message: error?.message || 'Unknown error',
708
+ logger.error({
709
+ message: `[ERROR] Failed to send ${payload.type} event to TestLens`,
710
+ error: error?.message || 'Unknown error',
694
711
  status: status,
695
712
  statusText: error?.response?.statusText,
696
713
  data: errorData,
@@ -716,12 +733,12 @@ class TestLensReporter {
716
733
  const isScreenshot = artifactType === 'screenshot' || attachment.contentType?.startsWith('image/');
717
734
  // Skip video if disabled in config
718
735
  if (isVideo && !this.config.enableVideo) {
719
- console.log(`⏭️ Skipping video artifact ${attachment.name} - video capture disabled in config`);
736
+ logger.info(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
720
737
  continue;
721
738
  }
722
739
  // Skip screenshot if disabled in config
723
740
  if (isScreenshot && !this.config.enableScreenshot) {
724
- console.log(`⏭️ Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
741
+ logger.info(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
725
742
  continue;
726
743
  }
727
744
  try {
@@ -759,13 +776,13 @@ class TestLensReporter {
759
776
  const uploadPromise = Promise.resolve().then(async () => {
760
777
  try {
761
778
  if (!attachment.path) {
762
- console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
779
+ logger.info(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
763
780
  return;
764
781
  }
765
782
  const s3Data = await this.uploadArtifactToS3(attachment.path, testId, fileName, testCaseDbId);
766
783
  // Skip if upload failed or file was too large
767
784
  if (!s3Data) {
768
- console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
785
+ logger.info(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
769
786
  return;
770
787
  }
771
788
  const artifactData = {
@@ -786,10 +803,10 @@ class TestLensReporter {
786
803
  timestamp: new Date().toISOString(),
787
804
  artifact: artifactData
788
805
  });
789
- console.log(`📎 [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
806
+ logger.info(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
790
807
  }
791
808
  catch (error) {
792
- console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}:`, error.message);
809
+ logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${error.message}`);
793
810
  }
794
811
  });
795
812
  // Track this upload and ensure cleanup on completion
@@ -801,7 +818,7 @@ class TestLensReporter {
801
818
  // They will be awaited in onEnd
802
819
  }
803
820
  catch (error) {
804
- console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}:`, error.message);
821
+ logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${error.message}`);
805
822
  }
806
823
  }
807
824
  }
@@ -831,16 +848,16 @@ class TestLensReporter {
831
848
  codeBlocks,
832
849
  testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, '')
833
850
  });
834
- console.log(`📝 Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
851
+ logger.info(`📝 Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
835
852
  }
836
853
  catch (error) {
837
854
  const errorData = error?.response?.data;
838
855
  // Handle duplicate spec code blocks gracefully (when re-running tests)
839
856
  if (errorData?.error && errorData.error.includes('duplicate key value violates unique constraint')) {
840
- console.log(`ℹ️ Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
857
+ logger.info(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
841
858
  return;
842
859
  }
843
- console.error('Failed to send spec code blocks:', errorData || error?.message || 'Unknown error');
860
+ logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
844
861
  }
845
862
  }
846
863
  extractTestBlocks(filePath) {
@@ -899,7 +916,7 @@ class TestLensReporter {
899
916
  return blocks;
900
917
  }
901
918
  catch (error) {
902
- console.error(`Failed to extract test blocks from ${filePath}:`, error.message);
919
+ logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
903
920
  return [];
904
921
  }
905
922
  }
@@ -922,7 +939,7 @@ class TestLensReporter {
922
939
  }
923
940
  catch (e) {
924
941
  // Remote info is optional - handle gracefully
925
- console.log('ℹ️ No git remote configured, skipping remote info');
942
+ logger.info('[INFO] No git remote configured, skipping remote info');
926
943
  }
927
944
  const isDirty = (0, child_process_1.execSync)('git status --porcelain', { encoding: 'utf-8' }).trim().length > 0;
928
945
  return {
@@ -994,7 +1011,7 @@ class TestLensReporter {
994
1011
  // Check file size first
995
1012
  const fileSize = this.getFileSize(filePath);
996
1013
  const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
997
- console.log(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
1014
+ logger.info(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
998
1015
  const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
999
1016
  // Step 1: Request pre-signed URL from server
1000
1017
  const presignedUrlEndpoint = `${baseUrl}/api/v1/artifacts/public/presigned-url`;
@@ -1019,7 +1036,7 @@ class TestLensReporter {
1019
1036
  }
1020
1037
  const { uploadUrl, s3Key, metadata } = presignedResponse.data;
1021
1038
  // Step 2: Upload directly to S3 using presigned URL
1022
- console.log(`⬆️ [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1039
+ logger.info(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1023
1040
  const fileBuffer = fs.readFileSync(filePath);
1024
1041
  // IMPORTANT: When using presigned URLs, we MUST include exactly the headers that were signed
1025
1042
  // The backend signs with ServerSideEncryption:'AES256', so we must send that header
@@ -1037,7 +1054,7 @@ class TestLensReporter {
1037
1054
  if (uploadResponse.status !== 200) {
1038
1055
  throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
1039
1056
  }
1040
- console.log(`✅ [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1057
+ logger.info(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1041
1058
  // Step 3: Confirm upload with server to save metadata
1042
1059
  const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
1043
1060
  const confirmBody = {
@@ -1059,7 +1076,7 @@ class TestLensReporter {
1059
1076
  });
1060
1077
  if (confirmResponse.status === 201 && confirmResponse.data.success) {
1061
1078
  const artifact = confirmResponse.data.artifact;
1062
- console.log(`✅ [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
1079
+ logger.info(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
1063
1080
  return {
1064
1081
  key: s3Key,
1065
1082
  url: artifact.s3Url,
@@ -1078,29 +1095,29 @@ class TestLensReporter {
1078
1095
  const errorData = error?.response?.data;
1079
1096
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
1080
1097
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
1081
- console.error('\n' + '='.repeat(80));
1098
+ logger.error('\n' + '='.repeat(80));
1082
1099
  if (errorData?.error === 'test_cases_limit_reached') {
1083
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
1100
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
1084
1101
  }
1085
1102
  else if (errorData?.error === 'test_runs_limit_reached') {
1086
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
1103
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
1087
1104
  }
1088
1105
  else {
1089
- console.error(' TESTLENS ERROR: Your trial plan has ended');
1106
+ logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
1090
1107
  }
1091
- console.error('='.repeat(80));
1092
- console.error('');
1093
- console.error(errorData?.message || 'Your trial period has expired.');
1094
- console.error('');
1095
- console.error('To continue using TestLens, please upgrade to Enterprise plan.');
1096
- console.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1097
- console.error('');
1108
+ logger.error('='.repeat(80));
1109
+ logger.error('');
1110
+ logger.error(errorData?.message || 'Your trial period has expired.');
1111
+ logger.error('');
1112
+ logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
1113
+ logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1114
+ logger.error('');
1098
1115
  if (errorData?.trial_end_date) {
1099
- console.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1100
- console.error('');
1116
+ logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1117
+ logger.error('');
1101
1118
  }
1102
- console.error('='.repeat(80));
1103
- console.error('');
1119
+ logger.error('='.repeat(80));
1120
+ logger.error('');
1104
1121
  return null;
1105
1122
  }
1106
1123
  }
@@ -1118,9 +1135,9 @@ class TestLensReporter {
1118
1135
  else if (error.response?.status === 403) {
1119
1136
  errorMsg = `Access denied (403) - presigned URL may have expired`;
1120
1137
  }
1121
- console.error(`❌ Failed to upload ${fileName} to S3:`, errorMsg);
1138
+ logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
1122
1139
  if (error.response?.data) {
1123
- console.error('Error details:', error.response.data);
1140
+ logger.error({ errorDetails: error.response.data }, 'Error details');
1124
1141
  }
1125
1142
  // Don't throw, just return null to continue with other artifacts
1126
1143
  return null;
@@ -1138,7 +1155,7 @@ class TestLensReporter {
1138
1155
  }
1139
1156
  }
1140
1157
  catch (error) {
1141
- console.warn(`Failed to get MIME type for ${fileName}:`, error.message);
1158
+ logger.warn(`Failed to get MIME type for ${fileName}: ${error.message}`);
1142
1159
  }
1143
1160
  // Fallback to basic content type mapping
1144
1161
  const contentTypes = {
@@ -1165,7 +1182,7 @@ class TestLensReporter {
1165
1182
  return stats.size;
1166
1183
  }
1167
1184
  catch (error) {
1168
- console.warn(`Could not get file size for ${filePath}:`, error.message);
1185
+ logger.warn(`Could not get file size for ${filePath}: ${error.message}`);
1169
1186
  return 0;
1170
1187
  }
1171
1188
  }
package/index.ts CHANGED
@@ -6,7 +6,12 @@ import * as https from 'https';
6
6
  import axios, { AxiosInstance } from 'axios';
7
7
  import type { Reporter, TestCase, TestResult, FullConfig, Suite } from '@playwright/test/reporter';
8
8
  import { execSync } from 'child_process';
9
- import FormData from 'form-data';
9
+ import pino from 'pino';
10
+
11
+ // Create pino logger instance
12
+ const logger = pino({
13
+ level: process.env.LOG_LEVEL || 'info'
14
+ });
10
15
 
11
16
  // Lazy-load mime module to support ESM
12
17
  let mimeModule: any = null;
@@ -214,10 +219,10 @@ export class TestLensReporter implements Reporter {
214
219
  // For testlensBuildTag, support comma-separated values
215
220
  if (key === 'testlensBuildTag' && value.includes(',')) {
216
221
  customArgs[key] = value.split(',').map(tag => tag.trim()).filter(tag => tag);
217
- console.log(`✓ Found ${envVar}=${value} (mapped to '${key}' as array of ${customArgs[key].length} tags)`);
222
+ logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}' as array of ${customArgs[key].length} tags)`);
218
223
  } else {
219
224
  customArgs[key] = value;
220
- console.log(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
225
+ logger.info(`✓ Found ${envVar}=${value} (mapped to '${key}')`);
221
226
  }
222
227
  break; // Use first match
223
228
  }
@@ -257,6 +262,8 @@ export class TestLensReporter implements Reporter {
257
262
  flushInterval: options.flushInterval || 5000,
258
263
  retryAttempts: options.retryAttempts !== undefined ? options.retryAttempts : 0,
259
264
  timeout: options.timeout || 60000,
265
+ rejectUnauthorized: options.rejectUnauthorized,
266
+ ignoreSslErrors: options.ignoreSslErrors,
260
267
  customMetadata: { ...options.customMetadata, ...customArgs } // Config metadata first, then CLI args override
261
268
  } as Required<TestLensReporterConfig>;
262
269
 
@@ -265,27 +272,36 @@ export class TestLensReporter implements Reporter {
265
272
  }
266
273
 
267
274
  if (apiKey !== options.apiKey) {
268
- console.log('✓ Using API key from environment variable');
275
+ logger.info('✓ Using API key from environment variable');
276
+ }
277
+
278
+ // Default environment to allow self-signed certs unless explicitly set
279
+ if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === undefined) {
280
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
269
281
  }
270
282
 
271
283
  // Determine SSL validation behavior
272
- let rejectUnauthorized = true; // Default to secure
284
+ let rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0'; // Default to secure unless explicitly disabled
273
285
 
274
- // Check various ways SSL validation can be disabled (in order of precedence)
275
- if (this.config.ignoreSslErrors) {
276
- // Explicit configuration option
286
+ // Check various ways SSL validation can be disabled or enforced (in order of precedence)
287
+ if (this.config.ignoreSslErrors === true) {
277
288
  rejectUnauthorized = false;
278
- console.log('⚠️ SSL certificate validation disabled via ignoreSslErrors option');
289
+ logger.warn('[WARN] SSL certificate validation disabled via ignoreSslErrors option');
279
290
  } else if (this.config.rejectUnauthorized === false) {
280
- // Explicit configuration option
281
291
  rejectUnauthorized = false;
282
- console.log('⚠️ SSL certificate validation disabled via rejectUnauthorized option');
292
+ logger.warn('[WARN] SSL certificate validation disabled via rejectUnauthorized option');
293
+ } else if (this.config.rejectUnauthorized === true) {
294
+ rejectUnauthorized = true;
283
295
  } else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '0') {
284
- // Environment variable override
285
296
  rejectUnauthorized = false;
286
- console.log('⚠️ SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
297
+ logger.warn('[WARN] SSL certificate validation disabled via NODE_TLS_REJECT_UNAUTHORIZED environment variable');
298
+ } else if (process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1') {
299
+ rejectUnauthorized = true;
287
300
  }
288
301
 
302
+ // Mirror the resolved value so all HTTPS requests in this process follow it
303
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = rejectUnauthorized ? '1' : '0';
304
+
289
305
  // Set up axios instance with retry logic and enhanced SSL handling
290
306
  this.axiosInstance = axios.create({
291
307
  baseURL: this.config.apiEndpoint,
@@ -333,11 +349,11 @@ export class TestLensReporter implements Reporter {
333
349
 
334
350
  // Log custom metadata if any
335
351
  if (this.config.customMetadata && Object.keys(this.config.customMetadata).length > 0) {
336
- console.log('\n📋 Custom Metadata Detected:');
352
+ logger.info('\n[METADATA] Custom Metadata Detected:');
337
353
  Object.entries(this.config.customMetadata).forEach(([key, value]) => {
338
- console.log(` ${key}: ${value}`);
354
+ logger.info(` ${key}: ${value}`);
339
355
  });
340
- console.log('');
356
+ logger.info('');
341
357
  }
342
358
  }
343
359
 
@@ -409,22 +425,22 @@ export class TestLensReporter implements Reporter {
409
425
  async onBegin(config: FullConfig, suite: Suite): Promise<void> {
410
426
  // Show Build Name if provided, otherwise show Run ID
411
427
  if (this.runMetadata.testlensBuildName) {
412
- console.log(`🚀 TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
413
- console.log(` Run ID: ${this.runId}`);
428
+ logger.info(`🚀 TestLens Reporter starting - Build: ${this.runMetadata.testlensBuildName}`);
429
+ logger.info(` Run ID: ${this.runId}`);
414
430
  } else {
415
- console.log(`🚀 TestLens Reporter starting - Run ID: ${this.runId}`);
431
+ logger.info(`🚀 TestLens Reporter starting - Run ID: ${this.runId}`);
416
432
  }
417
433
 
418
434
  // Collect Git information if enabled
419
435
  if (this.config.enableGitInfo) {
420
436
  this.runMetadata.gitInfo = await this.collectGitInfo();
421
437
  if (this.runMetadata.gitInfo) {
422
- console.log(`📦 Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
438
+ logger.info(`📦 Git info collected: branch=${this.runMetadata.gitInfo.branch}, commit=${this.runMetadata.gitInfo.shortCommit}, author=${this.runMetadata.gitInfo.author}`);
423
439
  } else {
424
- console.log(`⚠️ Git info collection returned null - not in a git repository or git not available`);
440
+ logger.warn(`[WARN] Git info collection returned null - not in a git repository or git not available`);
425
441
  }
426
442
  } else {
427
- console.log(`ℹ️ Git info collection disabled (enableGitInfo: false)`);
443
+ logger.info(`[INFO] Git info collection disabled (enableGitInfo: false)`);
428
444
  }
429
445
 
430
446
  // Add shard information if available
@@ -446,7 +462,7 @@ export class TestLensReporter implements Reporter {
446
462
 
447
463
  async onTestBegin(test: TestCase, result: TestResult): Promise<void> {
448
464
  // Log which test is starting
449
- console.log(`\n▶️ Running test: ${test.title}`);
465
+ logger.info(`\n[TEST] Running test: ${test.title}`);
450
466
 
451
467
  const specPath = test.location.file;
452
468
  const specKey = `${specPath}-${test.parent.title}`;
@@ -527,11 +543,11 @@ export class TestLensReporter implements Reporter {
527
543
  const testId = this.getTestId(test);
528
544
  let testData = this.testMap.get(testId);
529
545
 
530
- console.log(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
546
+ logger.info(`[TestLens] onTestEnd called for test: ${test.title}, status: ${result.status}, testData exists: ${!!testData}`);
531
547
 
532
548
  // For skipped tests, onTestBegin might not be called, so we need to create the test data here
533
549
  if (!testData) {
534
- console.log(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
550
+ logger.info(`[TestLens] Creating test data for skipped/uncreated test: ${test.title}`);
535
551
  // Create spec data if not exists (skipped tests might not have spec data either)
536
552
  const specPath = test.location.file;
537
553
  const specKey = `${specPath}-${test.parent.title}`;
@@ -697,7 +713,7 @@ export class TestLensReporter implements Reporter {
697
713
 
698
714
  // Send testEnd event for all tests, regardless of status
699
715
  // This ensures tests that are interrupted or have unexpected statuses are properly recorded
700
- console.log(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
716
+ logger.info(`[TestLens] Sending testEnd - testId: ${testData.id}, status: ${testData.status}, originalStatus: ${testData.originalStatus}`);
701
717
  // Send test end event to API and get response
702
718
  const testEndResponse = await this.sendToApi({
703
719
  type: 'testEnd',
@@ -787,12 +803,12 @@ export class TestLensReporter implements Reporter {
787
803
 
788
804
  // Wait for all pending artifact uploads to complete before sending runEnd
789
805
  if (this.pendingUploads.size > 0) {
790
- console.log(`⏳ Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
806
+ logger.info(`⏳ Waiting for ${this.pendingUploads.size} artifact upload(s) to complete...`);
791
807
  try {
792
808
  await Promise.all(Array.from(this.pendingUploads));
793
- console.log(`✅ All artifact uploads completed`);
809
+ logger.info(`[OK] All artifact uploads completed`);
794
810
  } catch (error) {
795
- console.error(`⚠️ Some artifact uploads failed, continuing with runEnd`);
811
+ logger.error(`[WARN] Some artifact uploads failed, continuing with runEnd`);
796
812
  }
797
813
  }
798
814
 
@@ -827,12 +843,12 @@ export class TestLensReporter implements Reporter {
827
843
 
828
844
  // Show Build Name if provided, otherwise show Run ID
829
845
  if (this.runMetadata.testlensBuildName) {
830
- console.log(`📊 TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
831
- console.log(` Run ID: ${this.runId}`);
846
+ logger.info(`[COMPLETE] TestLens Report completed - Build: ${this.runMetadata.testlensBuildName}`);
847
+ logger.info(` Run ID: ${this.runId}`);
832
848
  } else {
833
- console.log(`📊 TestLens Report completed - Run ID: ${this.runId}`);
849
+ logger.info(`[COMPLETE] TestLens Report completed - Run ID: ${this.runId}`);
834
850
  }
835
- console.log(`🎯 Results: ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
851
+ logger.info(`[RESULTS] ${passedTests} passed, ${failedTests} failed (${timedOutTests} timeouts), ${skippedTests} skipped`);
836
852
  }
837
853
 
838
854
  private async sendToApi(payload: any): Promise<any> {
@@ -848,7 +864,7 @@ export class TestLensReporter implements Reporter {
848
864
  }
849
865
  });
850
866
  if (this.config.enableRealTimeStream) {
851
- console.log(`✅ Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
867
+ logger.info(`[OK] Sent ${payload.type} event to TestLens (HTTP ${response.status})`);
852
868
  }
853
869
  // Return response data for caller to use
854
870
  return response.data;
@@ -863,25 +879,25 @@ export class TestLensReporter implements Reporter {
863
879
  this.runCreationFailed = true;
864
880
  }
865
881
 
866
- console.error('\n' + '='.repeat(80));
882
+ logger.error('\n' + '='.repeat(80));
867
883
  if (errorData?.limit_type === 'test_cases') {
868
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
884
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
869
885
  } else if (errorData?.limit_type === 'test_runs') {
870
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
886
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
871
887
  } else {
872
- console.error(' TESTLENS ERROR: Plan Limit Reached');
888
+ logger.error('[ERROR] TESTLENS ERROR: Plan Limit Reached');
873
889
  }
874
- console.error('='.repeat(80));
875
- console.error('');
876
- console.error(errorData?.message || 'You have reached your plan limit.');
877
- console.error('');
878
- console.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
879
- console.error('');
880
- console.error('To continue, please upgrade your plan.');
881
- console.error('Contact: support@alternative-path.com');
882
- console.error('');
883
- console.error('='.repeat(80));
884
- console.error('');
890
+ logger.error('='.repeat(80));
891
+ logger.error('');
892
+ logger.error(errorData?.message || 'You have reached your plan limit.');
893
+ logger.error('');
894
+ logger.error(`Current usage: ${errorData?.current_usage || 'N/A'} / ${errorData?.limit || 'N/A'}`);
895
+ logger.error('');
896
+ logger.error('To continue, please upgrade your plan.');
897
+ logger.error('Contact: support@alternative-path.com');
898
+ logger.error('');
899
+ logger.error('='.repeat(80));
900
+ logger.error('');
885
901
  return; // Don't log the full error object for limit errors
886
902
  }
887
903
 
@@ -889,36 +905,37 @@ export class TestLensReporter implements Reporter {
889
905
  if (status === 401) {
890
906
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
891
907
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
892
- console.error('\n' + '='.repeat(80));
908
+ logger.error('\n' + '='.repeat(80));
893
909
 
894
910
  if (errorData?.error === 'test_cases_limit_reached') {
895
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
911
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
896
912
  } else if (errorData?.error === 'test_runs_limit_reached') {
897
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
913
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
898
914
  } else {
899
- console.error(' TESTLENS ERROR: Your trial plan has ended');
915
+ logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
900
916
  }
901
917
 
902
- console.error('='.repeat(80));
903
- console.error('');
904
- console.error(errorData?.message || 'Your trial period has expired.');
905
- console.error('');
906
- console.error('To continue using TestLens, please upgrade to Enterprise plan.');
907
- console.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
908
- console.error('');
918
+ logger.error('='.repeat(80));
919
+ logger.error('');
920
+ logger.error(errorData?.message || 'Your trial period has expired.');
921
+ logger.error('');
922
+ logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
923
+ logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
924
+ logger.error('');
909
925
  if (errorData?.trial_end_date) {
910
- console.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
911
- console.error('');
926
+ logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
927
+ logger.error('');
912
928
  }
913
- console.error('='.repeat(80));
914
- console.error('');
929
+ logger.error('='.repeat(80));
930
+ logger.error('');
915
931
  } else {
916
- console.error(`❌ Authentication failed: ${errorData?.error || 'Invalid API key'}`);
932
+ logger.error(`[ERROR] Authentication failed: ${errorData?.error || 'Invalid API key'}`);
917
933
  }
918
934
  } else if (status !== 403) {
919
935
  // Log other errors (but not 403 which we handled above)
920
- console.error(`❌ Failed to send ${payload.type} event to TestLens:`, {
921
- message: error?.message || 'Unknown error',
936
+ logger.error({
937
+ message: `[ERROR] Failed to send ${payload.type} event to TestLens`,
938
+ error: error?.message || 'Unknown error',
922
939
  status: status,
923
940
  statusText: error?.response?.statusText,
924
941
  data: errorData,
@@ -949,13 +966,13 @@ export class TestLensReporter implements Reporter {
949
966
 
950
967
  // Skip video if disabled in config
951
968
  if (isVideo && !this.config.enableVideo) {
952
- console.log(`⏭️ Skipping video artifact ${attachment.name} - video capture disabled in config`);
969
+ logger.info(`[SKIP] Skipping video artifact ${attachment.name} - video capture disabled in config`);
953
970
  continue;
954
971
  }
955
972
 
956
973
  // Skip screenshot if disabled in config
957
974
  if (isScreenshot && !this.config.enableScreenshot) {
958
- console.log(`⏭️ Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
975
+ logger.info(`[SKIP] Skipping screenshot artifact ${attachment.name} - screenshot capture disabled in config`);
959
976
  continue;
960
977
  }
961
978
 
@@ -995,7 +1012,7 @@ export class TestLensReporter implements Reporter {
995
1012
  const uploadPromise = Promise.resolve().then(async () => {
996
1013
  try {
997
1014
  if (!attachment.path) {
998
- console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
1015
+ logger.info(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - no file path`);
999
1016
  return;
1000
1017
  }
1001
1018
 
@@ -1003,7 +1020,7 @@ export class TestLensReporter implements Reporter {
1003
1020
 
1004
1021
  // Skip if upload failed or file was too large
1005
1022
  if (!s3Data) {
1006
- console.log(`⏭️ [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
1023
+ logger.info(`[SKIP] [Test: ${testId.substring(0, 8)}...] Skipping artifact ${attachment.name} - upload failed or file too large`);
1007
1024
  return;
1008
1025
  }
1009
1026
 
@@ -1027,9 +1044,9 @@ export class TestLensReporter implements Reporter {
1027
1044
  artifact: artifactData
1028
1045
  });
1029
1046
 
1030
- console.log(`📎 [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
1047
+ logger.info(`[ARTIFACT] [Test: ${testId.substring(0, 8)}...] Processed artifact: ${fileName}`);
1031
1048
  } catch (error) {
1032
- console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}:`, (error as Error).message);
1049
+ logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to process artifact ${attachment.name}: ${(error as Error).message}`);
1033
1050
  }
1034
1051
  });
1035
1052
 
@@ -1042,7 +1059,7 @@ export class TestLensReporter implements Reporter {
1042
1059
  // Don't await here - let uploads happen in parallel
1043
1060
  // They will be awaited in onEnd
1044
1061
  } catch (error) {
1045
- console.error(`❌ [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}:`, (error as Error).message);
1062
+ logger.error(`[ERROR] [Test: ${testId.substring(0, 8)}...] Failed to setup artifact upload ${attachment.name}: ${(error as Error).message}`);
1046
1063
  }
1047
1064
  }
1048
1065
  }
@@ -1077,17 +1094,17 @@ export class TestLensReporter implements Reporter {
1077
1094
  testSuiteName: path.basename(specPath).replace(/\.(spec|test)\.(js|ts)$/, '')
1078
1095
  });
1079
1096
 
1080
- console.log(`📝 Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
1097
+ logger.info(`📝 Sent ${codeBlocks.length} code blocks for: ${path.basename(specPath)}`);
1081
1098
  } catch (error: any) {
1082
1099
  const errorData = error?.response?.data;
1083
1100
 
1084
1101
  // Handle duplicate spec code blocks gracefully (when re-running tests)
1085
1102
  if (errorData?.error && errorData.error.includes('duplicate key value violates unique constraint')) {
1086
- console.log(`ℹ️ Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
1103
+ logger.info(`[INFO] Spec code blocks already exist for: ${path.basename(specPath)} (skipped)`);
1087
1104
  return;
1088
1105
  }
1089
1106
 
1090
- console.error('Failed to send spec code blocks:', errorData || error?.message || 'Unknown error');
1107
+ logger.error(`Failed to send spec code blocks: ${errorData || error?.message || 'Unknown error'}`);
1091
1108
  }
1092
1109
  }
1093
1110
 
@@ -1153,7 +1170,7 @@ export class TestLensReporter implements Reporter {
1153
1170
 
1154
1171
  return blocks;
1155
1172
  } catch (error: any) {
1156
- console.error(`Failed to extract test blocks from ${filePath}:`, error.message);
1173
+ logger.error(`Failed to extract test blocks from ${filePath}: ${error.message}`);
1157
1174
  return [];
1158
1175
  }
1159
1176
  }
@@ -1177,7 +1194,7 @@ export class TestLensReporter implements Reporter {
1177
1194
  }
1178
1195
  } catch (e) {
1179
1196
  // Remote info is optional - handle gracefully
1180
- console.log('ℹ️ No git remote configured, skipping remote info');
1197
+ logger.info('[INFO] No git remote configured, skipping remote info');
1181
1198
  }
1182
1199
 
1183
1200
  const isDirty = execSync('git status --porcelain', { encoding: 'utf-8' }).trim().length > 0;
@@ -1258,7 +1275,7 @@ export class TestLensReporter implements Reporter {
1258
1275
  const fileSize = this.getFileSize(filePath);
1259
1276
  const fileSizeMB = (fileSize / (1024 * 1024)).toFixed(2);
1260
1277
 
1261
- console.log(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
1278
+ logger.info(`📤 Uploading ${fileName} (${fileSizeMB}MB) directly to S3...`);
1262
1279
 
1263
1280
  const baseUrl = this.config.apiEndpoint.replace('/api/v1/webhook/playwright', '');
1264
1281
 
@@ -1290,7 +1307,7 @@ export class TestLensReporter implements Reporter {
1290
1307
  const { uploadUrl, s3Key, metadata } = presignedResponse.data;
1291
1308
 
1292
1309
  // Step 2: Upload directly to S3 using presigned URL
1293
- console.log(`⬆️ [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1310
+ logger.info(`[UPLOAD] [Test: ${testId.substring(0, 8)}...] Uploading ${fileName} directly to S3...`);
1294
1311
 
1295
1312
  const fileBuffer = fs.readFileSync(filePath);
1296
1313
 
@@ -1312,7 +1329,7 @@ export class TestLensReporter implements Reporter {
1312
1329
  throw new Error(`S3 upload failed with status ${uploadResponse.status}`);
1313
1330
  }
1314
1331
 
1315
- console.log(`✅ [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1332
+ logger.info(`[OK] [Test: ${testId.substring(0, 8)}...] S3 upload completed for ${fileName}`);
1316
1333
 
1317
1334
  // Step 3: Confirm upload with server to save metadata
1318
1335
  const confirmEndpoint = `${baseUrl}/api/v1/artifacts/public/confirm-upload`;
@@ -1338,7 +1355,7 @@ export class TestLensReporter implements Reporter {
1338
1355
 
1339
1356
  if (confirmResponse.status === 201 && confirmResponse.data.success) {
1340
1357
  const artifact = confirmResponse.data.artifact;
1341
- console.log(`✅ [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
1358
+ logger.info(`[OK] [Test: ${testId.substring(0, 8)}...] Upload confirmed${testCaseDbId ? ' (fast path)' : ' (fallback)'}`);
1342
1359
  return {
1343
1360
  key: s3Key,
1344
1361
  url: artifact.s3Url,
@@ -1356,29 +1373,29 @@ export class TestLensReporter implements Reporter {
1356
1373
 
1357
1374
  if (errorData?.error === 'trial_expired' || errorData?.error === 'subscription_inactive' ||
1358
1375
  errorData?.error === 'test_cases_limit_reached' || errorData?.error === 'test_runs_limit_reached') {
1359
- console.error('\n' + '='.repeat(80));
1376
+ logger.error('\n' + '='.repeat(80));
1360
1377
 
1361
1378
  if (errorData?.error === 'test_cases_limit_reached') {
1362
- console.error(' TESTLENS ERROR: Test Cases Limit Reached');
1379
+ logger.error('[ERROR] TESTLENS ERROR: Test Cases Limit Reached');
1363
1380
  } else if (errorData?.error === 'test_runs_limit_reached') {
1364
- console.error(' TESTLENS ERROR: Test Runs Limit Reached');
1381
+ logger.error('[ERROR] TESTLENS ERROR: Test Runs Limit Reached');
1365
1382
  } else {
1366
- console.error(' TESTLENS ERROR: Your trial plan has ended');
1383
+ logger.error('[ERROR] TESTLENS ERROR: Your trial plan has ended');
1367
1384
  }
1368
1385
 
1369
- console.error('='.repeat(80));
1370
- console.error('');
1371
- console.error(errorData?.message || 'Your trial period has expired.');
1372
- console.error('');
1373
- console.error('To continue using TestLens, please upgrade to Enterprise plan.');
1374
- console.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1375
- console.error('');
1386
+ logger.error('='.repeat(80));
1387
+ logger.error('');
1388
+ logger.error(errorData?.message || 'Your trial period has expired.');
1389
+ logger.error('');
1390
+ logger.error('To continue using TestLens, please upgrade to Enterprise plan.');
1391
+ logger.error('Contact: ' + (errorData?.contactEmail || 'support@alternative-path.com'));
1392
+ logger.error('');
1376
1393
  if (errorData?.trial_end_date) {
1377
- console.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1378
- console.error('');
1394
+ logger.error(`Trial ended: ${new Date(errorData.trial_end_date).toLocaleDateString()}`);
1395
+ logger.error('');
1379
1396
  }
1380
- console.error('='.repeat(80));
1381
- console.error('');
1397
+ logger.error('='.repeat(80));
1398
+ logger.error('');
1382
1399
  return null;
1383
1400
  }
1384
1401
  }
@@ -1396,9 +1413,9 @@ export class TestLensReporter implements Reporter {
1396
1413
  errorMsg = `Access denied (403) - presigned URL may have expired`;
1397
1414
  }
1398
1415
 
1399
- console.error(`❌ Failed to upload ${fileName} to S3:`, errorMsg);
1416
+ logger.error(`[ERROR] Failed to upload ${fileName} to S3: ${errorMsg}`);
1400
1417
  if (error.response?.data) {
1401
- console.error('Error details:', error.response.data);
1418
+ logger.error({ errorDetails: error.response.data }, 'Error details');
1402
1419
  }
1403
1420
 
1404
1421
  // Don't throw, just return null to continue with other artifacts
@@ -1417,7 +1434,7 @@ export class TestLensReporter implements Reporter {
1417
1434
  return mimeType || 'application/octet-stream';
1418
1435
  }
1419
1436
  } catch (error: any) {
1420
- console.warn(`Failed to get MIME type for ${fileName}:`, error.message);
1437
+ logger.warn(`Failed to get MIME type for ${fileName}: ${error.message}`);
1421
1438
  }
1422
1439
  // Fallback to basic content type mapping
1423
1440
  const contentTypes: Record<string, string> = {
@@ -1445,7 +1462,7 @@ export class TestLensReporter implements Reporter {
1445
1462
  const stats = fs.statSync(filePath);
1446
1463
  return stats.size;
1447
1464
  } catch (error) {
1448
- console.warn(`Could not get file size for ${filePath}:`, (error as Error).message);
1465
+ logger.warn(`Could not get file size for ${filePath}: ${(error as Error).message}`);
1449
1466
  return 0;
1450
1467
  }
1451
1468
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alternative-path/testlens-playwright-reporter",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Universal Playwright reporter for TestLens - works with both TypeScript and JavaScript projects",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -56,6 +56,7 @@
56
56
  "dotenv": "^16.4.5",
57
57
  "form-data": "^4.0.1",
58
58
  "mime": "^4.0.4",
59
+ "pino": "^9.0.0",
59
60
  "tslib": "^2.8.1"
60
61
  },
61
62
  "engines": {
package/postinstall.js CHANGED
@@ -1,5 +1,10 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const pino = require('pino');
4
+
5
+ const logger = pino({
6
+ level: process.env.LOG_LEVEL || 'info'
7
+ });
3
8
 
4
9
  try {
5
10
  // Get the parent project's node_modules/.bin directory
@@ -10,7 +15,7 @@ try {
10
15
 
11
16
  // Check if parent bin directory exists
12
17
  if (!fs.existsSync(parentBinDir)) {
13
- console.log('Parent .bin directory not found, skipping cross-env setup');
18
+ logger.info('Parent .bin directory not found, skipping cross-env setup');
14
19
  process.exit(0);
15
20
  }
16
21
 
@@ -25,7 +30,7 @@ try {
25
30
 
26
31
  // Copy the file
27
32
  fs.copyFileSync(sourceCmdPath, targetCmdPath);
28
- console.log('✓ cross-env binary installed');
33
+ logger.info('✓ cross-env binary installed');
29
34
 
30
35
  // Also copy PowerShell script on Windows
31
36
  if (process.platform === 'win32' && fs.existsSync(sourcePs1Path)) {
@@ -35,5 +40,5 @@ try {
35
40
  }
36
41
  } catch (error) {
37
42
  // Don't fail installation if this doesn't work
38
- console.log('Note: Could not setup cross-env automatically:', error.message);
43
+ logger.info('Note: Could not setup cross-env automatically:', error.message);
39
44
  }