@webex/plugin-meetings 3.0.0-beta.262 → 3.0.0-beta.263

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.
@@ -16,9 +16,20 @@ import ReachabilityRequest from './request';
16
16
  const DEFAULT_TIMEOUT = 3000;
17
17
  const VIDEO_MESH_TIMEOUT = 1000;
18
18
 
19
+ export type ReachabilityMetrics = {
20
+ reachability_public_udp_success: number;
21
+ reachability_public_udp_failed: number;
22
+ reachability_public_tcp_success: number;
23
+ reachability_public_tcp_failed: number;
24
+ reachability_vmn_udp_success: number;
25
+ reachability_vmn_udp_failed: number;
26
+ reachability_vmn_tcp_success: number;
27
+ reachability_vmn_tcp_failed: number;
28
+ };
29
+
19
30
  // result for a specific transport protocol (like udp or tcp)
20
31
  export type TransportResult = {
21
- reachable: 'true' | 'false';
32
+ reachable?: 'true' | 'false';
22
33
  latencyInMilliseconds?: string;
23
34
  clientMediaIPs?: string[];
24
35
  untested?: 'true';
@@ -138,6 +149,58 @@ export default class Reachability {
138
149
  }
139
150
  }
140
151
 
152
+ /**
153
+ * Returns statistics about last reachability results. The returned value is an object
154
+ * with a flat list of properties so that it can be easily sent with metrics
155
+ *
156
+ * @returns {Promise} Promise with metrics values, it never rejects/throws.
157
+ */
158
+ async getReachabilityMetrics(): Promise<ReachabilityMetrics> {
159
+ const stats: ReachabilityMetrics = {
160
+ reachability_public_udp_success: 0,
161
+ reachability_public_udp_failed: 0,
162
+ reachability_public_tcp_success: 0,
163
+ reachability_public_tcp_failed: 0,
164
+ reachability_vmn_udp_success: 0,
165
+ reachability_vmn_udp_failed: 0,
166
+ reachability_vmn_tcp_success: 0,
167
+ reachability_vmn_tcp_failed: 0,
168
+ };
169
+
170
+ const updateStats = (clusterType: 'public' | 'vmn', result: ReachabilityResult) => {
171
+ if (result.udp?.reachable) {
172
+ const outcome = result.udp.reachable === 'true' ? 'success' : 'failed';
173
+ stats[`reachability_${clusterType}_udp_${outcome}`] += 1;
174
+ }
175
+ if (result.tcp?.reachable) {
176
+ const outcome = result.tcp.reachable === 'true' ? 'success' : 'failed';
177
+ stats[`reachability_${clusterType}_tcp_${outcome}`] += 1;
178
+ }
179
+ };
180
+
181
+ try {
182
+ // @ts-ignore
183
+ const resultsJson = await this.webex.boundedStorage.get(
184
+ REACHABILITY.namespace,
185
+ REACHABILITY.localStorageResult
186
+ );
187
+
188
+ const internalResults: InternalReachabilityResults = JSON.parse(resultsJson);
189
+
190
+ Object.values(internalResults).forEach((result) => {
191
+ updateStats(result.isVideoMesh ? 'vmn' : 'public', result);
192
+ });
193
+ } catch (e) {
194
+ // empty storage, that's ok
195
+ LoggerProxy.logger.warn(
196
+ 'Roap:request#getReachabilityMetrics --> Error parsing reachability data: ',
197
+ e
198
+ );
199
+ }
200
+
201
+ return stats;
202
+ }
203
+
141
204
  /**
142
205
  * Reachability results as an object in the format that backend expects
143
206
  *
@@ -44,14 +44,12 @@ import {
44
44
  StreamEventNames,
45
45
  } from '@webex/media-helpers';
46
46
  import * as StatsAnalyzerModule from '@webex/plugin-meetings/src/statsAnalyzer';
47
- import * as MuteStateModule from '@webex/plugin-meetings/src/meeting/muteState';
48
47
  import EventsScope from '@webex/plugin-meetings/src/common/events/events-scope';
49
48
  import Meetings, {CONSTANTS} from '@webex/plugin-meetings';
50
49
  import Meeting from '@webex/plugin-meetings/src/meeting';
51
50
  import Members from '@webex/plugin-meetings/src/members';
52
51
  import * as MembersImport from '@webex/plugin-meetings/src/members';
53
52
  import Roap from '@webex/plugin-meetings/src/roap';
54
- import RoapRequest from '@webex/plugin-meetings/src/roap/request';
55
53
  import MeetingRequest from '@webex/plugin-meetings/src/meeting/request';
56
54
  import * as MeetingRequestImport from '@webex/plugin-meetings/src/meeting/request';
57
55
  import LocusInfo from '@webex/plugin-meetings/src/locus-info';
@@ -81,7 +79,6 @@ import {
81
79
  UserNotJoinedError,
82
80
  MeetingNotActiveError,
83
81
  UserInLobbyError,
84
- NoMediaEstablishedYetError,
85
82
  } from '../../../../src/common/errors/webex-errors';
86
83
  import WebExMeetingsErrors from '../../../../src/common/errors/webex-meetings-error';
87
84
  import ParameterError from '../../../../src/common/errors/parameter';
@@ -89,19 +86,14 @@ import PasswordError from '../../../../src/common/errors/password-error';
89
86
  import CaptchaError from '../../../../src/common/errors/captcha-error';
90
87
  import PermissionError from '../../../../src/common/errors/permission';
91
88
  import IntentToJoinError from '../../../../src/common/errors/intent-to-join';
92
- import DefaultSDKConfig from '../../../../src/config';
93
89
  import testUtils from '../../../utils/testUtils';
94
90
  import {
95
91
  MeetingInfoV2CaptchaError,
96
92
  MeetingInfoV2PasswordError,
97
93
  MeetingInfoV2PolicyError,
98
94
  } from '../../../../src/meeting-info/meeting-info-v2';
99
- import {ANNOTATION_POLICY} from '../../../../src/annotation/constants';
100
95
 
101
96
 
102
- // Non-stubbed function
103
- const {getDisplayMedia} = Media;
104
-
105
97
  describe('plugin-meetings', () => {
106
98
  const logger = {
107
99
  info: () => {},
@@ -228,6 +220,11 @@ describe('plugin-meetings', () => {
228
220
  webex.credentials.getOrgId = sinon.stub().returns('fake-org-id');
229
221
  webex.internal.metrics.submitClientMetrics = sinon.stub().returns(Promise.resolve());
230
222
  webex.meetings.uploadLogs = sinon.stub().returns(Promise.resolve());
223
+ webex.meetings.reachability = {
224
+ isAnyPublicClusterReachable: sinon.stub().resolves(true),
225
+ getReachabilityResults: sinon.stub().resolves(undefined),
226
+ getReachabilityMetrics: sinon.stub().resolves({}),
227
+ };
231
228
  webex.internal.llm.on = sinon.stub();
232
229
  membersSpy = sinon.spy(MembersImport, 'default');
233
230
  meetingRequestSpy = sinon.spy(MeetingRequestImport, 'default');
@@ -1170,8 +1167,15 @@ describe('plugin-meetings', () => {
1170
1167
  }
1171
1168
  });
1172
1169
 
1173
- it('should reset the statsAnalyzer to null if addMedia throws an error', async () => {
1170
+ it('should send metrics and reset the statsAnalyzer to null if addMedia throws an error', async () => {
1174
1171
  meeting.meetingState = 'ACTIVE';
1172
+ meeting.webex.meetings.reachability = {
1173
+ getReachabilityMetrics: sinon.stub().resolves({
1174
+ someReachabilityMetric1: 'some value1',
1175
+ someReachabilityMetric2: 'some value2'
1176
+ }),
1177
+ };
1178
+
1175
1179
  // setup the mock to return an incomplete object - this will cause addMedia to fail
1176
1180
  // because some methods (like on() or initiateOffer()) are missing
1177
1181
  Media.createMediaConnection = sinon.stub().returns({
@@ -1195,6 +1199,8 @@ describe('plugin-meetings', () => {
1195
1199
  signalingState: 'unknown',
1196
1200
  connectionState: 'unknown',
1197
1201
  iceConnectionState: 'unknown',
1202
+ someReachabilityMetric1: 'some value1',
1203
+ someReachabilityMetric2: 'some value2',
1198
1204
  });
1199
1205
  });
1200
1206
 
@@ -1546,6 +1552,12 @@ describe('plugin-meetings', () => {
1546
1552
 
1547
1553
  it('should send ADD_MEDIA_SUCCESS metrics', async () => {
1548
1554
  meeting.meetingState = 'ACTIVE';
1555
+ meeting.webex.meetings.reachability = {
1556
+ getReachabilityMetrics: sinon.stub().resolves({
1557
+ someReachabilityMetric1: 'some value1',
1558
+ someReachabilityMetric2: 'some value2'
1559
+ }),
1560
+ };
1549
1561
  await meeting.addMedia({
1550
1562
  mediaSettings: {},
1551
1563
  });
@@ -1556,6 +1568,8 @@ describe('plugin-meetings', () => {
1556
1568
  locus_id: meeting.locusUrl.split('/').pop(),
1557
1569
  connectionType: 'udp',
1558
1570
  isMultistream: false,
1571
+ someReachabilityMetric1: 'some value1',
1572
+ someReachabilityMetric2: 'some value2'
1559
1573
  });
1560
1574
 
1561
1575
  assert.called(webex.internal.newMetrics.submitClientEvent);
@@ -1792,10 +1806,6 @@ describe('plugin-meetings', () => {
1792
1806
  meeting.setMercuryListener = sinon.stub();
1793
1807
  meeting.locusInfo.onFullLocus = sinon.stub();
1794
1808
  meeting.webex.meetings.geoHintInfo = {regionCode: 'EU', countryCode: 'UK'};
1795
- meeting.webex.meetings.reachability = {
1796
- isAnyPublicClusterReachable: sinon.stub().resolves(true),
1797
- getReachabilityResults: sinon.stub().resolves(undefined),
1798
- };
1799
1809
  meeting.roap.doTurnDiscovery = sinon
1800
1810
  .stub()
1801
1811
  .resolves({turnServerInfo: {}, turnDiscoverySkippedReason: 'reachability'});
@@ -328,8 +328,6 @@ describe('getReachabilityResults', () => {
328
328
 
329
329
  beforeEach(() => {
330
330
  webex = new MockWebex();
331
-
332
- // sinon.stub(MeetingUtil, 'getIpVersion').returns(IP_VERSION.unknown);
333
331
  });
334
332
 
335
333
  afterEach(() => {
@@ -416,3 +414,211 @@ describe('getReachabilityResults', () => {
416
414
  );
417
415
  });
418
416
  });
417
+
418
+ describe('getReachabilityMetrics', () => {
419
+ let webex;
420
+
421
+ beforeEach(() => {
422
+ webex = new MockWebex();
423
+ });
424
+
425
+ afterEach(() => {
426
+ sinon.restore();
427
+ });
428
+
429
+ const runCheck = async (mockStorage: any, expectedResult: any) => {
430
+ if (mockStorage) {
431
+ await webex.boundedStorage.put(
432
+ 'Reachability',
433
+ 'reachability.result',
434
+ JSON.stringify(mockStorage)
435
+ );
436
+ }
437
+ const reachability = new Reachability(webex);
438
+ const result = await reachability.getReachabilityMetrics();
439
+
440
+ assert.deepEqual(result, expectedResult);
441
+ };
442
+
443
+ it('returns all zeros if reading from local storage fails', async () => {
444
+ sinon.stub(webex.boundedStorage, 'get').rejects(new Error('fake error'));
445
+
446
+ const reachability = new Reachability(webex);
447
+
448
+ const result = await reachability.getReachabilityMetrics();
449
+
450
+ assert.deepEqual(result, {
451
+ reachability_public_udp_success: 0,
452
+ reachability_public_udp_failed: 0,
453
+ reachability_public_tcp_success: 0,
454
+ reachability_public_tcp_failed: 0,
455
+ reachability_vmn_udp_success: 0,
456
+ reachability_vmn_udp_failed: 0,
457
+ reachability_vmn_tcp_success: 0,
458
+ reachability_vmn_tcp_failed: 0,
459
+ });
460
+ });
461
+
462
+ it('returns correct stats based on local storage results', async () => {
463
+ await runCheck(
464
+ // mock storage:
465
+ {
466
+ public1: {
467
+ udp: {reachable: 'true', latencyInMilliseconds: '100'},
468
+ tcp: {untested: 'true'},
469
+ xtls: {untested: 'true'},
470
+ },
471
+ vmn1: {
472
+ udp: {reachable: 'true', latencyInMilliseconds: '200'},
473
+ tcp: {reachable: 'false'},
474
+ xtls: {untested: 'true'},
475
+ isVideoMesh: true,
476
+ },
477
+ vmn2: {
478
+ udp: {untested: 'true'},
479
+ tcp: {reachable: 'true', latencyInMilliseconds: '100', clientMediaIPs: ['10.10.10.10']},
480
+ xtls: {untested: 'true'},
481
+ isVideoMesh: true,
482
+ someOtherField: 'any value',
483
+ },
484
+ public2: {
485
+ udp: {reachable: 'false', latencyInMilliseconds: '300'},
486
+ tcp: {reachable: 'false', untested: 'true'},
487
+ xtls: {untested: 'true'},
488
+ someOtherField: 'any value',
489
+ },
490
+ public3: {
491
+ udp: {reachable: 'true', latencyInMilliseconds: '400', clientMediaIPs: ['10.10.10.10']},
492
+ tcp: {reachable: 'true', latencyInMilliseconds: '100', clientMediaIPs: ['10.10.10.10']},
493
+ xtls: {untested: 'true'},
494
+ isVideoMesh: false,
495
+ someOtherField: 'any value',
496
+ },
497
+ public4: {
498
+ udp: {reachable: 'true', latencyInMilliseconds: '40', clientMediaIPs: ['10.10.10.11']},
499
+ tcp: {reachable: 'true', latencyInMilliseconds: '100', clientMediaIPs: ['10.10.10.11']},
500
+ xtls: {untested: 'true'},
501
+ isVideoMesh: false,
502
+ someOtherField: 'any value',
503
+ },
504
+ public5: {
505
+ udp: {reachable: 'false'},
506
+ tcp: {untested: 'true'},
507
+ xtls: {untested: 'true'},
508
+ isVideoMesh: false,
509
+ someOtherField: 'any value',
510
+ },
511
+ },
512
+ // expected result:
513
+ {
514
+ reachability_public_udp_success: 3,
515
+ reachability_public_udp_failed: 2,
516
+ reachability_public_tcp_success: 2,
517
+ reachability_public_tcp_failed: 1,
518
+ reachability_vmn_udp_success: 1,
519
+ reachability_vmn_udp_failed: 0,
520
+ reachability_vmn_tcp_success: 1,
521
+ reachability_vmn_tcp_failed: 1,
522
+ }
523
+ );
524
+ });
525
+
526
+ it('returns correct stats when only public nodes were tested', async () => {
527
+ await runCheck(
528
+ // mock storage:
529
+ {
530
+ public1: {
531
+ udp: {reachable: 'true', latencyInMilliseconds: '400', clientMediaIPs: ['10.10.10.10']},
532
+ tcp: {reachable: 'true', latencyInMilliseconds: '100', clientMediaIPs: ['10.10.10.10']},
533
+ xtls: {untested: 'true'},
534
+ isVideoMesh: false,
535
+ },
536
+ public2: {
537
+ udp: {reachable: 'true', latencyInMilliseconds: '100'},
538
+ tcp: {untested: 'true'},
539
+ xtls: {untested: 'true'},
540
+ },
541
+ public3: {
542
+ udp: {reachable: 'false', latencyInMilliseconds: '300'},
543
+ tcp: {reachable: 'false', untested: 'true'},
544
+ xtls: {untested: 'true'},
545
+ someOtherField: 'any value',
546
+ },
547
+ public4: {
548
+ udp: {untested: 'true'},
549
+ tcp: {reachable: 'false'},
550
+ xtls: {untested: 'true'},
551
+ isVideoMesh: false,
552
+ someOtherField: 'any value',
553
+ },
554
+ public5: {
555
+ udp: {reachable: 'true', latencyInMilliseconds: '400', clientMediaIPs: ['10.10.10.10']},
556
+ tcp: {untested: 'true'},
557
+ xtls: {untested: 'true'},
558
+ },
559
+ },
560
+ // expected result:
561
+ {
562
+ reachability_public_udp_success: 3,
563
+ reachability_public_udp_failed: 1,
564
+ reachability_public_tcp_success: 1,
565
+ reachability_public_tcp_failed: 2,
566
+ reachability_vmn_udp_success: 0,
567
+ reachability_vmn_udp_failed: 0,
568
+ reachability_vmn_tcp_success: 0,
569
+ reachability_vmn_tcp_failed: 0,
570
+ }
571
+ );
572
+ });
573
+
574
+ it('returns correct stats when only video mesh nodes were tested', async () => {
575
+ await runCheck(
576
+ // mock storage:
577
+ {
578
+ vmn1: {
579
+ udp: {reachable: 'false'},
580
+ tcp: {reachable: 'true', latencyInMilliseconds: '100', clientMediaIPs: ['10.10.10.10']},
581
+ xtls: {untested: 'true'},
582
+ isVideoMesh: true,
583
+ },
584
+ vmn2: {
585
+ udp: {reachable: 'true', latencyInMilliseconds: '200', clientMediaIPs: ['10.10.10.10']},
586
+ tcp: {untested: 'true'},
587
+ xtls: {untested: 'true'},
588
+ isVideoMesh: true,
589
+ },
590
+ vmn3: {
591
+ udp: {reachable: 'true', latencyInMilliseconds: '300', clientMediaIPs: ['10.10.10.10']},
592
+ tcp: {reachable: 'false', untested: 'true'},
593
+ xtls: {untested: 'true'},
594
+ isVideoMesh: true,
595
+ },
596
+ vmn4: {
597
+ udp: {untested: 'true'},
598
+ tcp: {reachable: 'false'},
599
+ xtls: {untested: 'true'},
600
+ isVideoMesh: true,
601
+ someOtherField: 'any value',
602
+ },
603
+ vmn5: {
604
+ udp: {reachable: 'true', latencyInMilliseconds: '200', clientMediaIPs: ['10.10.10.10']},
605
+ tcp: {reachable: 'false'},
606
+ xtls: {untested: 'true'},
607
+ isVideoMesh: true,
608
+ someOtherField: 'any value',
609
+ },
610
+ },
611
+ // expected result:
612
+ {
613
+ reachability_public_udp_success: 0,
614
+ reachability_public_udp_failed: 0,
615
+ reachability_public_tcp_success: 0,
616
+ reachability_public_tcp_failed: 0,
617
+ reachability_vmn_udp_success: 3,
618
+ reachability_vmn_udp_failed: 1,
619
+ reachability_vmn_tcp_success: 1,
620
+ reachability_vmn_tcp_failed: 3,
621
+ }
622
+ );
623
+ });
624
+ });