@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/api.ts +5291 -2120
- package/dist/api.d.ts +5111 -2591
- package/dist/api.js +231 -51
- package/dist/vapi.d.ts +21 -0
- package/dist/vapi.js +221 -24
- package/package.json +2 -2
- package/vapi.ts +242 -24
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
|
-
|
|
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:
|
|
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',
|
|
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',
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
767
|
-
errorStack:
|
|
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:
|
|
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',
|
|
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',
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
1338
|
-
errorStack:
|
|
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:
|
|
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
|
}
|