@vapi-ai/web 2.5.0 → 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/vapi.ts CHANGED
@@ -98,6 +98,73 @@ interface CallStartFailedEvent {
98
98
  context: Record<string, any>;
99
99
  }
100
100
 
101
+ interface SerializedError {
102
+ message: string;
103
+ name?: string;
104
+ stack?: string;
105
+ code?: string | number;
106
+ cause?: string;
107
+ [key: string]: any;
108
+ }
109
+
110
+ /**
111
+ * Extracts error details into a plain object that serializes properly to JSON.
112
+ * Error objects don't serialize well because their properties (message, stack, name)
113
+ * are not enumerable and get lost when using JSON.stringify().
114
+ */
115
+ function serializeError(error: unknown): SerializedError {
116
+ if (error === null || error === undefined) {
117
+ return { message: 'Unknown error (null or undefined)' };
118
+ }
119
+
120
+ if (error instanceof Error) {
121
+ const serialized: SerializedError = {
122
+ message: error.message,
123
+ name: error.name,
124
+ stack: error.stack,
125
+ };
126
+
127
+ // Include any additional properties that might be on the error
128
+ const errorAsAny = error as any;
129
+ if (errorAsAny.code !== undefined) {
130
+ serialized.code = errorAsAny.code;
131
+ }
132
+ if (errorAsAny.cause !== undefined) {
133
+ serialized.cause = String(errorAsAny.cause);
134
+ }
135
+ if (errorAsAny.reason !== undefined) {
136
+ serialized.reason = errorAsAny.reason;
137
+ }
138
+ if (errorAsAny.details !== undefined) {
139
+ serialized.details = errorAsAny.details;
140
+ }
141
+ // Daily.co specific error properties
142
+ if (errorAsAny.errorMsg !== undefined) {
143
+ serialized.errorMsg = errorAsAny.errorMsg;
144
+ }
145
+ if (errorAsAny.error !== undefined && typeof errorAsAny.error === 'string') {
146
+ serialized.errorDetail = errorAsAny.error;
147
+ }
148
+
149
+ return serialized;
150
+ }
151
+
152
+ if (typeof error === 'string') {
153
+ return { message: error };
154
+ }
155
+
156
+ if (typeof error === 'object') {
157
+ // It's already a plain object, but let's ensure message exists
158
+ const errorObj = error as Record<string, any>;
159
+ return {
160
+ message: errorObj.message || errorObj.error || JSON.stringify(error),
161
+ ...errorObj
162
+ };
163
+ }
164
+
165
+ return { message: String(error) };
166
+ }
167
+
101
168
  type VapiEventListeners = {
102
169
  'call-end': () => void;
103
170
  'call-start': () => void;
@@ -298,7 +365,7 @@ export default class Vapi extends VapiEventEmitter {
298
365
  this.emit('error', {
299
366
  type: 'validation-error',
300
367
  stage: 'input-validation',
301
- message: error.message,
368
+ error: serializeError(error),
302
369
  timestamp: new Date().toISOString()
303
370
  });
304
371
  throw error;
@@ -410,17 +477,18 @@ export default class Vapi extends VapiEventEmitter {
410
477
  });
411
478
  } catch (error) {
412
479
  const dailyCallDuration = Date.now() - dailyCallStartTime;
480
+ const serializedError = serializeError(error);
413
481
  this.emit('call-start-progress', {
414
482
  stage: 'daily-call-object-creation',
415
483
  status: 'failed',
416
484
  duration: dailyCallDuration,
417
485
  timestamp: new Date().toISOString(),
418
- metadata: { error: error?.toString() }
486
+ metadata: { error: serializedError.message }
419
487
  });
420
488
  this.emit('error', {
421
489
  type: 'daily-call-object-creation-error',
422
490
  stage: 'daily-call-object-creation',
423
- error,
491
+ error: serializedError,
424
492
  timestamp: new Date().toISOString()
425
493
  });
426
494
  throw error;
@@ -445,14 +513,22 @@ export default class Vapi extends VapiEventEmitter {
445
513
  });
446
514
 
447
515
  this.call.on('error', (error: any) => {
448
- this.emit('error', error);
516
+ this.emit('error', {
517
+ type: 'daily-error',
518
+ error: serializeError(error),
519
+ timestamp: new Date().toISOString()
520
+ });
449
521
  if (isVideoRecordingEnabled) {
450
522
  this.call?.stopRecording();
451
523
  }
452
524
  });
453
525
 
454
526
  this.call.on('camera-error', (error: any) => {
455
- this.emit('camera-error', error);
527
+ this.emit('camera-error', {
528
+ type: 'camera-error',
529
+ error: serializeError(error),
530
+ timestamp: new Date().toISOString()
531
+ });
456
532
  });
457
533
 
458
534
  this.call.on('network-quality-change', (event: any) => {
@@ -560,17 +636,18 @@ export default class Vapi extends VapiEventEmitter {
560
636
  });
561
637
  } catch (error) {
562
638
  const joinDuration = Date.now() - joinStartTime;
639
+ const serializedError = serializeError(error);
563
640
  this.emit('call-start-progress', {
564
641
  stage: 'daily-call-join',
565
642
  status: 'failed',
566
643
  duration: joinDuration,
567
644
  timestamp: new Date().toISOString(),
568
- metadata: { error: error?.toString() }
645
+ metadata: { error: serializedError.message }
569
646
  });
570
647
  this.emit('error', {
571
648
  type: 'daily-call-join-error',
572
649
  stage: 'daily-call-join',
573
- error,
650
+ error: serializedError,
574
651
  duration: joinDuration,
575
652
  timestamp: new Date().toISOString()
576
653
  });
@@ -623,17 +700,18 @@ export default class Vapi extends VapiEventEmitter {
623
700
  });
624
701
  } catch (error) {
625
702
  const recordingSetupDuration = Date.now() - recordingStartTime;
703
+ const serializedError = serializeError(error);
626
704
  this.emit('call-start-progress', {
627
705
  stage: 'video-recording-setup',
628
706
  status: 'failed',
629
707
  duration: recordingSetupDuration,
630
708
  timestamp: new Date().toISOString(),
631
- metadata: { error: error?.toString() }
709
+ metadata: { error: serializedError.message }
632
710
  });
633
711
  this.emit('error', {
634
712
  type: 'video-recording-setup-error',
635
713
  stage: 'video-recording-setup',
636
- error,
714
+ error: serializedError,
637
715
  timestamp: new Date().toISOString()
638
716
  });
639
717
  // Don't throw here, video recording is optional
@@ -667,17 +745,18 @@ export default class Vapi extends VapiEventEmitter {
667
745
  });
668
746
  } catch (error) {
669
747
  const audioObserverDuration = Date.now() - audioObserverStartTime;
748
+ const serializedError = serializeError(error);
670
749
  this.emit('call-start-progress', {
671
750
  stage: 'audio-observer-setup',
672
751
  status: 'failed',
673
752
  duration: audioObserverDuration,
674
753
  timestamp: new Date().toISOString(),
675
- metadata: { error: error?.toString() }
754
+ metadata: { error: serializedError.message }
676
755
  });
677
756
  this.emit('error', {
678
757
  type: 'audio-observer-setup-error',
679
758
  stage: 'audio-observer-setup',
680
- error,
759
+ error: serializedError,
681
760
  timestamp: new Date().toISOString()
682
761
  });
683
762
  // Don't throw here, this is non-critical
@@ -733,17 +812,18 @@ export default class Vapi extends VapiEventEmitter {
733
812
  });
734
813
  } catch (error) {
735
814
  const audioProcessingDuration = Date.now() - audioProcessingStartTime;
815
+ const serializedError = serializeError(error);
736
816
  this.emit('call-start-progress', {
737
817
  stage: 'audio-processing-setup',
738
818
  status: 'failed',
739
819
  duration: audioProcessingDuration,
740
820
  timestamp: new Date().toISOString(),
741
- metadata: { error: error?.toString() }
821
+ metadata: { error: serializedError.message }
742
822
  });
743
823
  this.emit('error', {
744
824
  type: 'audio-processing-setup-error',
745
825
  stage: 'audio-processing-setup',
746
- error,
826
+ error: serializedError,
747
827
  timestamp: new Date().toISOString()
748
828
  });
749
829
  // Don't throw here, this is non-critical
@@ -759,12 +839,13 @@ export default class Vapi extends VapiEventEmitter {
759
839
  return webCall;
760
840
  } catch (e) {
761
841
  const totalDuration = Date.now() - startTime;
842
+ const serializedError = serializeError(e);
762
843
 
763
844
  this.emit('call-start-failed', {
764
845
  stage: 'unknown',
765
846
  totalDuration,
766
- error: e?.toString() || 'Unknown error occurred',
767
- errorStack: e instanceof Error ? e.stack : 'No stack trace available',
847
+ error: serializedError.message,
848
+ errorStack: serializedError.stack || 'No stack trace available',
768
849
  timestamp: new Date().toISOString(),
769
850
  context: {
770
851
  hasAssistant: !!assistant,
@@ -778,7 +859,7 @@ export default class Vapi extends VapiEventEmitter {
778
859
  this.emit('error', {
779
860
  type: 'start-method-error',
780
861
  stage: 'unknown',
781
- error: e,
862
+ error: serializedError,
782
863
  totalDuration,
783
864
  timestamp: new Date().toISOString(),
784
865
  context: {
@@ -1054,14 +1135,22 @@ export default class Vapi extends VapiEventEmitter {
1054
1135
  });
1055
1136
 
1056
1137
  this.call.on('error', (error: any) => {
1057
- this.emit('error', error);
1138
+ this.emit('error', {
1139
+ type: 'daily-error',
1140
+ error: serializeError(error),
1141
+ timestamp: new Date().toISOString()
1142
+ });
1058
1143
  if (isVideoRecordingEnabled) {
1059
1144
  this.call?.stopRecording();
1060
1145
  }
1061
1146
  });
1062
1147
 
1063
1148
  this.call.on('camera-error', (error: any) => {
1064
- this.emit('camera-error', error);
1149
+ this.emit('camera-error', {
1150
+ type: 'camera-error',
1151
+ error: serializeError(error),
1152
+ timestamp: new Date().toISOString()
1153
+ });
1065
1154
  });
1066
1155
 
1067
1156
  this.call.on('network-quality-change', (event: any) => {
@@ -1234,12 +1323,13 @@ export default class Vapi extends VapiEventEmitter {
1234
1323
  });
1235
1324
  } catch (error) {
1236
1325
  const recordingSetupDuration = Date.now() - recordingStartTime;
1326
+ const serializedError = serializeError(error);
1237
1327
  this.emit('call-start-progress', {
1238
1328
  stage: 'video-recording-setup',
1239
1329
  status: 'failed',
1240
1330
  duration: recordingSetupDuration,
1241
1331
  timestamp: new Date().toISOString(),
1242
- metadata: { error: error?.toString() }
1332
+ metadata: { error: serializedError.message }
1243
1333
  });
1244
1334
  // Don't throw here, video recording is optional
1245
1335
  }
@@ -1272,12 +1362,13 @@ export default class Vapi extends VapiEventEmitter {
1272
1362
  });
1273
1363
  } catch (error) {
1274
1364
  const audioObserverDuration = Date.now() - audioObserverStartTime;
1365
+ const serializedError = serializeError(error);
1275
1366
  this.emit('call-start-progress', {
1276
1367
  stage: 'audio-observer-setup',
1277
1368
  status: 'failed',
1278
1369
  duration: audioObserverDuration,
1279
1370
  timestamp: new Date().toISOString(),
1280
- metadata: { error: error?.toString() }
1371
+ metadata: { error: serializedError.message }
1281
1372
  });
1282
1373
  // Don't throw here, this is non-critical
1283
1374
  }
@@ -1309,12 +1400,13 @@ export default class Vapi extends VapiEventEmitter {
1309
1400
  });
1310
1401
  } catch (error) {
1311
1402
  const audioProcessingDuration = Date.now() - audioProcessingStartTime;
1403
+ const serializedError = serializeError(error);
1312
1404
  this.emit('call-start-progress', {
1313
1405
  stage: 'audio-processing-setup',
1314
1406
  status: 'failed',
1315
1407
  duration: audioProcessingDuration,
1316
1408
  timestamp: new Date().toISOString(),
1317
- metadata: { error: error?.toString() }
1409
+ metadata: { error: serializedError.message }
1318
1410
  });
1319
1411
  // Don't throw here, this is non-critical
1320
1412
  }
@@ -1330,12 +1422,13 @@ export default class Vapi extends VapiEventEmitter {
1330
1422
 
1331
1423
  } catch (e) {
1332
1424
  const totalDuration = Date.now() - startTime;
1425
+ const serializedError = serializeError(e);
1333
1426
 
1334
1427
  this.emit('call-start-failed', {
1335
1428
  stage: 'reconnect',
1336
1429
  totalDuration,
1337
- error: e?.toString() || 'Unknown error occurred',
1338
- errorStack: e instanceof Error ? e.stack : 'No stack trace available',
1430
+ error: serializedError.message,
1431
+ errorStack: serializedError.stack || 'No stack trace available',
1339
1432
  timestamp: new Date().toISOString(),
1340
1433
  context: {
1341
1434
  isReconnect: true,
@@ -1349,7 +1442,7 @@ export default class Vapi extends VapiEventEmitter {
1349
1442
  // Also emit the generic error event for backward compatibility
1350
1443
  this.emit('error', {
1351
1444
  type: 'reconnect-error',
1352
- error: e,
1445
+ error: serializedError,
1353
1446
  totalDuration,
1354
1447
  timestamp: new Date().toISOString(),
1355
1448
  context: {
@@ -1365,4 +1458,129 @@ export default class Vapi extends VapiEventEmitter {
1365
1458
  throw e;
1366
1459
  }
1367
1460
  }
1461
+
1462
+ /**
1463
+ * Runs all network connectivity tests for pre-call diagnostics.
1464
+ * Creates a temporary Daily call object for testing purposes.
1465
+ *
1466
+ * Tests performed:
1467
+ * 1. Network connectivity (TURN server) - Tests if traffic can be relayed through TURN servers
1468
+ * 2. Websocket connectivity - Tests if websocket connections can be established
1469
+ * 3. Call quality - Tests overall call quality metrics (if available in SDK version)
1470
+ *
1471
+ * @returns {Promise<Record<string, any>>} Test results object with status for each test
1472
+ *
1473
+ * @example
1474
+ * // Run pre-call network diagnostics
1475
+ * const results = await Vapi.runNetworkTestsStandalone();
1476
+ * if (results.networkConnectivity?.result === 'failed') {
1477
+ * console.warn('Network issues detected - calls may not work properly');
1478
+ * }
1479
+ *
1480
+ * @static
1481
+ */
1482
+ public static async runNetworkTestsStandalone(): Promise<Record<string, any>> {
1483
+ console.log('Starting standalone network connectivity tests...');
1484
+
1485
+ const results: Record<string, any> = {};
1486
+ let tempCall: DailyCall | null = null;
1487
+
1488
+ try {
1489
+ // Create a temporary call object for testing
1490
+ console.log('Creating temporary call object for testing...');
1491
+ tempCall = DailyIframe.createCallObject({
1492
+ audioSource: true,
1493
+ videoSource: true,
1494
+ });
1495
+
1496
+ // Test 1: Network Connectivity (TURN server test)
1497
+ console.log('\n1. Testing network connectivity (TURN server)...');
1498
+ let videoTrack: MediaStreamTrack | null = null;
1499
+
1500
+ try {
1501
+ // Create a dummy video track for the test
1502
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
1503
+ videoTrack = stream.getVideoTracks()[0];
1504
+
1505
+ const networkTest = await tempCall.testNetworkConnectivity(videoTrack);
1506
+ results.networkConnectivity = networkTest;
1507
+ console.log('Network connectivity test result:', networkTest);
1508
+ } catch (error) {
1509
+ results.networkConnectivity = { result: 'error', error: error?.toString() };
1510
+ console.error('Network connectivity test error:', error);
1511
+ } finally {
1512
+ // Clean up the video track
1513
+ if (videoTrack) {
1514
+ videoTrack.stop();
1515
+ }
1516
+ }
1517
+
1518
+ // Test 2: Websocket Connectivity
1519
+ console.log('\n2. Testing websocket connectivity...');
1520
+ try {
1521
+ const websocketTest = await tempCall.testWebsocketConnectivity();
1522
+ results.websocketConnectivity = websocketTest;
1523
+ console.log('Websocket connectivity test result:', websocketTest);
1524
+ } catch (error) {
1525
+ results.websocketConnectivity = { result: 'error', error: error?.toString() };
1526
+ console.error('Websocket connectivity test error:', error);
1527
+ }
1528
+
1529
+ // Test 3: Call Quality (if available)
1530
+ console.log('\n3. Testing call quality...');
1531
+ try {
1532
+ // Check if the method exists
1533
+ if (typeof tempCall.testCallQuality === 'function') {
1534
+ // Call quality test requires startCamera to initialize call state
1535
+ try {
1536
+ // Use startCamera to initialize the call state without needing a room
1537
+ console.log('Initializing call state with startCamera...');
1538
+ await tempCall.startCamera();
1539
+
1540
+ // Now run the call quality test
1541
+ const callQualityTest = await tempCall.testCallQuality();
1542
+ results.callQuality = callQualityTest;
1543
+ console.log('Call quality test result:', callQualityTest);
1544
+
1545
+ // Camera will be cleaned up when we destroy the call object
1546
+ } catch (startCameraError: any) {
1547
+ // If startCamera fails, it might be due to permissions or other issues
1548
+ console.error('Failed to start camera for call quality test:', startCameraError);
1549
+ results.callQuality = {
1550
+ result: 'error',
1551
+ error: startCameraError?.toString(),
1552
+ message: 'Failed to initialize camera for call quality test. Check camera permissions.'
1553
+ };
1554
+ }
1555
+ } else {
1556
+ results.callQuality = { result: 'not-available', message: 'testCallQuality method not available' };
1557
+ console.log('Call quality test not available in current Daily.co version');
1558
+ }
1559
+ } catch (error: any) {
1560
+ results.callQuality = { result: 'error', error: error?.toString() };
1561
+ console.error('Call quality test error:', error);
1562
+ }
1563
+
1564
+ } catch (error) {
1565
+ console.error('Failed to create temporary call object:', error);
1566
+ results.error = error?.toString();
1567
+ } finally {
1568
+ // Clean up the temporary call object
1569
+ if (tempCall) {
1570
+ try {
1571
+ console.log('Cleaning up temporary call object...');
1572
+ await tempCall.destroy();
1573
+ } catch (error) {
1574
+ console.error('Error destroying temporary call object:', error);
1575
+ }
1576
+ }
1577
+ }
1578
+
1579
+ // Summary
1580
+ console.log('\n=== Network Test Summary ===');
1581
+ console.log('Results:', JSON.stringify(results, null, 2));
1582
+
1583
+ return results;
1584
+ }
1585
+
1368
1586
  }