@webex/internal-plugin-metrics 3.9.0-webinar5k.1 → 3.10.0-multi-llms.1

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.
@@ -264,6 +264,41 @@ describe('internal-plugin-metrics', () => {
264
264
  });
265
265
  });
266
266
 
267
+ it('should build origin correctly and vendorId can be passed in options', () => {
268
+ sinon.stub(CallDiagnosticUtils, 'anonymizeIPAddress').returns('1.1.1.1');
269
+
270
+ //@ts-ignore
271
+ const res = cd.getOrigin(
272
+ {
273
+ subClientType: 'WEB_APP',
274
+ clientType: 'TEAMS_CLIENT',
275
+ clientLaunchMethod: 'url-handler',
276
+ vendorId: 'GoogleMeet',
277
+ },
278
+ fakeMeeting.id
279
+ );
280
+
281
+ assert.deepEqual(res, {
282
+ clientInfo: {
283
+ browser: getBrowserName(),
284
+ browserVersion: getBrowserVersion(),
285
+ clientType: 'TEAMS_CLIENT',
286
+ clientVersion: 'webex-js-sdk/webex-version',
287
+ publicNetworkPrefix: '1.1.1.1',
288
+ localNetworkPrefix: '1.1.1.1',
289
+ os: getOSNameInternal(),
290
+ osVersion: getOSVersion() || 'unknown',
291
+ subClientType: 'WEB_APP',
292
+ clientLaunchMethod: 'url-handler',
293
+ vendorId: 'GoogleMeet',
294
+ },
295
+ environment: 'meeting_evn',
296
+ name: 'endpoint',
297
+ networkType: 'unknown',
298
+ userAgent,
299
+ });
300
+ });
301
+
267
302
  it('should build origin correctly with browserLaunchMethod', () => {
268
303
  sinon.stub(CallDiagnosticUtils, 'anonymizeIPAddress').returns('1.1.1.1');
269
304
 
@@ -2431,7 +2466,7 @@ describe('internal-plugin-metrics', () => {
2431
2466
  );
2432
2467
  });
2433
2468
 
2434
- it('should send behavioral event if meetingId provided but meeting is undefined', () => {
2469
+ it('should record failure metric when meetingId is provided but meeting is undefined', () => {
2435
2470
  webex.meetings.getBasicMeetingInformation = sinon.stub().returns(undefined);
2436
2471
 
2437
2472
  cd.submitClientEvent({name: 'client.alert.displayed', options: {meetingId: 'meetingId'}});
@@ -2465,6 +2500,228 @@ describe('internal-plugin-metrics', () => {
2465
2500
  assert.calledWith(cd.submitToCallDiagnosticsPreLogin, testEvent);
2466
2501
  assert.notCalled(cd.submitToCallDiagnostics);
2467
2502
  });
2503
+
2504
+ describe('Limiting repeated events', () => {
2505
+ beforeEach(() => {
2506
+ cd.clearEventLimits();
2507
+ });
2508
+
2509
+ const createEventLimitRegex = (eventName: string, eventType: string) => {
2510
+ const escapedEventName = eventName.replace(/\./g, '\\.');
2511
+ return new RegExp(`Event limit reached for ${escapedEventName} for ${eventType}`);
2512
+ };
2513
+
2514
+ it('should always send events that are not in the limiting switch cases', () => {
2515
+ const options = {
2516
+ meetingId: fakeMeeting.id,
2517
+ };
2518
+ const submitToCallDiagnosticsStub = sinon.stub(cd, 'submitToCallDiagnostics');
2519
+
2520
+ const baselineCallCount = webex.logger.log.callCount;
2521
+ cd.submitClientEvent({
2522
+ name: 'client.alert.displayed',
2523
+ options,
2524
+ });
2525
+
2526
+ cd.submitClientEvent({
2527
+ name: 'client.alert.displayed',
2528
+ options,
2529
+ });
2530
+
2531
+ cd.submitClientEvent({
2532
+ name: 'client.alert.displayed',
2533
+ options,
2534
+ });
2535
+
2536
+ assert.calledThrice(submitToCallDiagnosticsStub);
2537
+ });
2538
+
2539
+ ([
2540
+ ['client.media.render.start'],
2541
+ ['client.media.render.stop'],
2542
+ ['client.media.rx.start'],
2543
+ ['client.media.rx.stop'],
2544
+ ['client.media.tx.start'],
2545
+ ['client.media.tx.stop']
2546
+ ] as const).forEach(([name]) => {
2547
+ it(`should only send ${name} once per mediaType`, () => {
2548
+ const options = {
2549
+ meetingId: fakeMeeting.id,
2550
+ };
2551
+ const payload = {
2552
+ mediaType: 'video' as const,
2553
+ };
2554
+ const submitToCallDiagnosticsStub = sinon.stub(cd, 'submitToCallDiagnostics');
2555
+
2556
+ const baselineCallCount = webex.logger.log.callCount;
2557
+ // Send first event
2558
+ cd.submitClientEvent({
2559
+ name,
2560
+ payload,
2561
+ options,
2562
+ });
2563
+
2564
+ assert.calledOnce(submitToCallDiagnosticsStub);
2565
+ submitToCallDiagnosticsStub.resetHistory();
2566
+
2567
+ // Send second event of same type
2568
+ cd.submitClientEvent({
2569
+ name,
2570
+ payload,
2571
+ options,
2572
+ });
2573
+
2574
+ assert.notCalled(submitToCallDiagnosticsStub);
2575
+ assert.calledWith(
2576
+ webex.logger.log,
2577
+ 'call-diagnostic-events -> ',
2578
+ sinon.match(createEventLimitRegex(name, 'mediaType video'))
2579
+ );
2580
+ webex.logger.log.resetHistory();
2581
+
2582
+ // Send third event of same type
2583
+ cd.submitClientEvent({
2584
+ name,
2585
+ payload,
2586
+ options,
2587
+ });
2588
+
2589
+ assert.notCalled(submitToCallDiagnosticsStub);
2590
+ assert.neverCalledWithMatch(webex.logger.log,
2591
+ 'call-diagnostic-events -> ',
2592
+ sinon.match(createEventLimitRegex(name, 'mediaType video'))
2593
+ );
2594
+
2595
+ // Send fourth event with a different mediaType
2596
+ cd.submitClientEvent({
2597
+ name,
2598
+ payload: {mediaType: 'audio'},
2599
+ options,
2600
+ });
2601
+
2602
+ assert.calledOnce(submitToCallDiagnosticsStub);
2603
+ });
2604
+
2605
+ it(`should handle share media type with shareInstanceId correctly for ${name}`, () => {
2606
+ const options = {
2607
+ meetingId: fakeMeeting.id,
2608
+ };
2609
+ const payload = {
2610
+ mediaType: 'share' as const,
2611
+ shareInstanceId: 'instance-1',
2612
+ };
2613
+ const submitToCallDiagnosticsStub = sinon.stub(cd, 'submitToCallDiagnostics');
2614
+
2615
+ const baselineCallCount = webex.logger.log.callCount;
2616
+ // Send first event
2617
+ cd.submitClientEvent({
2618
+ name,
2619
+ payload,
2620
+ options,
2621
+ });
2622
+
2623
+ // Send second event with same shareInstanceId
2624
+ cd.submitClientEvent({
2625
+ name,
2626
+ payload,
2627
+ options,
2628
+ });
2629
+
2630
+ // Send event with different shareInstanceId
2631
+ cd.submitClientEvent({
2632
+ name,
2633
+ payload: { ...payload, shareInstanceId: 'instance-2' },
2634
+ options,
2635
+ });
2636
+
2637
+ assert.calledTwice(submitToCallDiagnosticsStub);
2638
+ });
2639
+ });
2640
+
2641
+ ([
2642
+ ['client.roap-message.received'],
2643
+ ['client.roap-message.sent']
2644
+ ] as const).forEach(([name]) => {
2645
+ it(`should not send third event of same type and not log warning again for ${name}`, () => {
2646
+ const options = {
2647
+ meetingId: fakeMeeting.id,
2648
+ };
2649
+ const payload = {
2650
+ roap: {
2651
+ messageType: 'OFFER' as const,
2652
+ },
2653
+ };
2654
+ const submitToCallDiagnosticsStub = sinon.stub(cd, 'submitToCallDiagnostics');
2655
+
2656
+ // Clear any existing call history to get accurate counts
2657
+ webex.logger.log.resetHistory();
2658
+
2659
+ // Send first event
2660
+ cd.submitClientEvent({
2661
+ name,
2662
+ payload,
2663
+ options,
2664
+ });
2665
+
2666
+ assert.calledOnce(submitToCallDiagnosticsStub);
2667
+ submitToCallDiagnosticsStub.resetHistory();
2668
+
2669
+ // Send second event (should trigger warning)
2670
+ cd.submitClientEvent({
2671
+ name,
2672
+ payload,
2673
+ options,
2674
+ });
2675
+
2676
+ assert.notCalled(submitToCallDiagnosticsStub);
2677
+ assert.calledWith(
2678
+ webex.logger.log,
2679
+ 'call-diagnostic-events -> ',
2680
+ sinon.match(createEventLimitRegex(name, 'ROAP type OFFER'))
2681
+ );
2682
+ webex.logger.log.resetHistory();
2683
+
2684
+ cd.submitClientEvent({
2685
+ name,
2686
+ payload,
2687
+ options,
2688
+ });
2689
+
2690
+ assert.notCalled(submitToCallDiagnosticsStub);
2691
+ assert.neverCalledWithMatch(
2692
+ webex.logger.log,
2693
+ 'call-diagnostic-events -> ',
2694
+ sinon.match(createEventLimitRegex(name, 'ROAP type OFFER'))
2695
+ );
2696
+ });
2697
+
2698
+ it(`should handle roap.type instead of roap.messageType for ${name}`, () => {
2699
+ const options = {
2700
+ meetingId: fakeMeeting.id,
2701
+ };
2702
+ const payload = {
2703
+ roap: {
2704
+ type: 'ANSWER' as const,
2705
+ },
2706
+ };
2707
+ const submitToCallDiagnosticsStub = sinon.stub(cd, 'submitToCallDiagnostics');
2708
+
2709
+ cd.submitClientEvent({
2710
+ name,
2711
+ payload,
2712
+ options,
2713
+ });
2714
+
2715
+ cd.submitClientEvent({
2716
+ name,
2717
+ payload,
2718
+ options,
2719
+ });
2720
+
2721
+ assert.calledOnce(submitToCallDiagnosticsStub);
2722
+ });
2723
+ });
2724
+ });
2468
2725
  });
2469
2726
 
2470
2727
  describe('#submitToCallDiagnostics', () => {
@@ -3649,7 +3906,7 @@ describe('internal-plugin-metrics', () => {
3649
3906
 
3650
3907
  assert.deepEqual(webexLoggerLogCalls[0].args, [
3651
3908
  'CallDiagnosticMetrics: @setDeviceInfo called',
3652
- device,
3909
+ {userId: 'userId', deviceId: 'deviceUrl', orgId: 'orgId'},
3653
3910
  ]);
3654
3911
 
3655
3912
  assert.deepEqual(cd.device, device);
@@ -4022,5 +4279,186 @@ describe('internal-plugin-metrics', () => {
4022
4279
  assert.notCalled(submitFeatureEventSpy);
4023
4280
  });
4024
4281
  });
4282
+
4283
+ describe('#clearEventLimitsForCorrelationId', () => {
4284
+ beforeEach(() => {
4285
+ cd.clearEventLimits();
4286
+ });
4287
+
4288
+ it('should clear event limits for specific correlationId only', () => {
4289
+ // Use the actual correlationIds from our fakeMeeting fixtures
4290
+ const correlationId1 = fakeMeeting.correlationId; // e.g. 'correlationId1'
4291
+ const correlationId2 = fakeMeeting2.correlationId; // e.g. 'correlationId2'
4292
+ const options1 = { meetingId: fakeMeeting.id };
4293
+ const options2 = { meetingId: fakeMeeting2.id };
4294
+ const payload = { mediaType: 'video' as const };
4295
+
4296
+ // Set up events for both correlations to trigger limits
4297
+ cd.submitClientEvent({ name: 'client.media.render.start', payload, options: options1 });
4298
+ cd.submitClientEvent({ name: 'client.media.render.start', payload, options: options2 });
4299
+ cd.submitClientEvent({ name: 'client.media.render.start', payload, options: options1 });
4300
+ cd.submitClientEvent({ name: 'client.media.render.start', payload, options: options2 });
4301
+ assert.isTrue(cd.eventLimitTracker.size > 0);
4302
+ assert.isTrue(cd.eventLimitWarningsLogged.size > 0);
4303
+
4304
+ // Clear limits for only correlationId1 (present)
4305
+ cd.clearEventLimitsForCorrelationId(correlationId1);
4306
+
4307
+ const remainingTrackerKeys = Array.from(cd.eventLimitTracker.keys());
4308
+ const remainingWarningKeys = Array.from(cd.eventLimitWarningsLogged.keys());
4309
+
4310
+ // Should have no keys with correlationId1
4311
+ assert.isFalse(remainingTrackerKeys.some(key => key.split(':')[1] === correlationId1));
4312
+ assert.isFalse(remainingWarningKeys.some(key => key.split(':')[1] === correlationId1));
4313
+
4314
+ // Should still have keys with correlationId2
4315
+ assert.isTrue(remainingTrackerKeys.some(key => key.split(':')[1] === correlationId2));
4316
+ assert.isTrue(remainingWarningKeys.some(key => key.split(':')[1] === correlationId2));
4317
+ });
4318
+
4319
+ it('should handle empty correlationId gracefully', () => {
4320
+ const options = { meetingId: fakeMeeting.id };
4321
+ const payload = { mediaType: 'video' as const };
4322
+
4323
+ // Set up some tracking data
4324
+ cd.submitClientEvent({
4325
+ name: 'client.media.render.start',
4326
+ payload,
4327
+ options,
4328
+ });
4329
+
4330
+ cd.submitClientEvent({
4331
+ name: 'client.media.render.start',
4332
+ payload,
4333
+ options,
4334
+ });
4335
+
4336
+ const initialTrackerSize = cd.eventLimitTracker.size;
4337
+ const initialWarningsSize = cd.eventLimitWarningsLogged.size;
4338
+
4339
+ // Should not clear anything for empty correlationId
4340
+ cd.clearEventLimitsForCorrelationId('');
4341
+ cd.clearEventLimitsForCorrelationId(null as any);
4342
+ cd.clearEventLimitsForCorrelationId(undefined as any);
4343
+
4344
+ assert.equal(cd.eventLimitTracker.size, initialTrackerSize);
4345
+ assert.equal(cd.eventLimitWarningsLogged.size, initialWarningsSize);
4346
+ });
4347
+
4348
+ it('should handle non-existent correlationId gracefully', () => {
4349
+ const options = { meetingId: fakeMeeting.id };
4350
+ const payload = { mediaType: 'video' as const };
4351
+
4352
+ // Set up some tracking data
4353
+ cd.submitClientEvent({
4354
+ name: 'client.media.render.start',
4355
+ payload,
4356
+ options,
4357
+ });
4358
+
4359
+ const initialTrackerSize = cd.eventLimitTracker.size;
4360
+ const initialWarningsSize = cd.eventLimitWarningsLogged.size;
4361
+
4362
+ // Should not clear anything for non-existent correlationId
4363
+ cd.clearEventLimitsForCorrelationId('nonExistentCorrelationId');
4364
+
4365
+ assert.equal(cd.eventLimitTracker.size, initialTrackerSize);
4366
+ assert.equal(cd.eventLimitWarningsLogged.size, initialWarningsSize);
4367
+ });
4368
+
4369
+ it('should clear multiple event types for the same correlationId', () => {
4370
+ const correlationId = fakeMeeting.correlationId;
4371
+ const options = { meetingId: fakeMeeting.id };
4372
+ const videoPayload = { mediaType: 'video' as const };
4373
+ const audioPayload = { mediaType: 'audio' as const };
4374
+ const roapPayload = { roap: { messageType: 'OFFER' as const } };
4375
+
4376
+ // Set up multiple event types for the same correlation
4377
+ cd.submitClientEvent({
4378
+ name: 'client.media.render.start',
4379
+ payload: videoPayload,
4380
+ options,
4381
+ });
4382
+
4383
+ cd.submitClientEvent({
4384
+ name: 'client.media.render.start',
4385
+ payload: audioPayload,
4386
+ options,
4387
+ });
4388
+
4389
+ cd.submitClientEvent({
4390
+ name: 'client.roap-message.sent',
4391
+ payload: roapPayload,
4392
+ options,
4393
+ });
4394
+
4395
+ // Trigger limits
4396
+ cd.submitClientEvent({
4397
+ name: 'client.media.render.start',
4398
+ payload: videoPayload,
4399
+ options,
4400
+ });
4401
+
4402
+ cd.submitClientEvent({
4403
+ name: 'client.media.render.start',
4404
+ payload: audioPayload,
4405
+ options,
4406
+ });
4407
+
4408
+ cd.submitClientEvent({
4409
+ name: 'client.roap-message.sent',
4410
+ payload: roapPayload,
4411
+ options,
4412
+ });
4413
+
4414
+ assert.isTrue(cd.eventLimitTracker.size > 0);
4415
+ assert.isTrue(cd.eventLimitWarningsLogged.size > 0);
4416
+
4417
+ // Clear all limits for this correlationId
4418
+ cd.clearEventLimitsForCorrelationId(correlationId);
4419
+
4420
+ // Should clear all tracking data for this correlationId
4421
+ assert.equal(cd.eventLimitTracker.size, 0);
4422
+ assert.equal(cd.eventLimitWarningsLogged.size, 0);
4423
+ });
4424
+
4425
+ it('should allow events to be sent again after clearing limits for correlationId', () => {
4426
+ const correlationId = fakeMeeting.correlationId;
4427
+ const options = { meetingId: fakeMeeting.id };
4428
+ const payload = { mediaType: 'video' as const };
4429
+ const submitToCallDiagnosticsStub = sinon.stub(cd, 'submitToCallDiagnostics');
4430
+
4431
+ // Send first event (should succeed)
4432
+ cd.submitClientEvent({
4433
+ name: 'client.media.render.start',
4434
+ payload,
4435
+ options,
4436
+ });
4437
+
4438
+ assert.calledOnce(submitToCallDiagnosticsStub);
4439
+ submitToCallDiagnosticsStub.resetHistory();
4440
+
4441
+ // Send second event (should be blocked)
4442
+ cd.submitClientEvent({
4443
+ name: 'client.media.render.start',
4444
+ payload,
4445
+ options,
4446
+ });
4447
+
4448
+ assert.notCalled(submitToCallDiagnosticsStub);
4449
+
4450
+ // Clear limits for this correlationId
4451
+ cd.clearEventLimitsForCorrelationId(correlationId);
4452
+
4453
+ // Send event again (should succeed after clearing)
4454
+ cd.submitClientEvent({
4455
+ name: 'client.media.render.start',
4456
+ payload,
4457
+ options,
4458
+ });
4459
+
4460
+ assert.calledOnce(submitToCallDiagnosticsStub);
4461
+ });
4462
+ });
4025
4463
  });
4026
4464
  });
@@ -17,7 +17,7 @@ describe('internal-plugin-metrics', () => {
17
17
  let webex;
18
18
  let clock;
19
19
  let now;
20
-
20
+ const deviceManagerStub = {getPairedDevice: sinon.stub()};
21
21
  const preLoginId = 'my_prelogin_id';
22
22
 
23
23
  beforeEach(() => {
@@ -30,6 +30,7 @@ describe('internal-plugin-metrics', () => {
30
30
  newMetrics: NewMetrics,
31
31
  },
32
32
  });
33
+ webex.devicemanager = deviceManagerStub;
33
34
 
34
35
  webex.request = (options) =>
35
36
  Promise.resolve({body: {items: []}, waitForServiceTimeout: 15, options});
@@ -217,9 +218,7 @@ describe('internal-plugin-metrics', () => {
217
218
  });
218
219
 
219
220
  assert.deepEqual(calls.args[0].type, ['diagnostic-event']);
220
-
221
221
  const prepareDiagnosticMetricItemCalls = prepareDiagnosticMetricItemSpy.getCalls();
222
-
223
222
  // second argument (item) also gets assigned a delay property but the key is a Symbol and haven't been able to test that..
224
223
  assert.deepEqual(prepareDiagnosticMetricItemCalls[0].args[0], webex);
225
224
  assert.deepEqual(prepareDiagnosticMetricItemCalls[0].args[1].eventPayload, {
@@ -232,6 +231,75 @@ describe('internal-plugin-metrics', () => {
232
231
  });
233
232
  assert.deepEqual(prepareDiagnosticMetricItemCalls[0].args[1].type, ['diagnostic-event']);
234
233
  });
234
+ it('adds the paired device to the metric payload if paired', async () => {
235
+ webex.internal.newMetrics.callDiagnosticMetrics.preLoginMetricsBatcher.prepareRequest = (
236
+ q
237
+ ) => Promise.resolve(q);
238
+ webex.devicemanager.getPairedDevice = sinon.stub().returns({
239
+ deviceInfo: {
240
+ id: 'my_device_id',
241
+ },
242
+ url: 'my_url',
243
+ mode: 'personal',
244
+ devices: [{productName: 'my_product_name'}],
245
+ });
246
+ webex.devicemanager.getPairedMethod = sinon.stub().returns("Manual");
247
+
248
+ const prepareItemSpy = sinon.spy(
249
+ webex.internal.newMetrics.callDiagnosticMetrics.preLoginMetricsBatcher,
250
+ 'prepareItem'
251
+ );
252
+ const prepareDiagnosticMetricItemSpy = sinon.spy(
253
+ CallDiagnosticUtils,
254
+ 'prepareDiagnosticMetricItem'
255
+ );
256
+
257
+ const promise =
258
+ webex.internal.newMetrics.callDiagnosticMetrics.submitToCallDiagnosticsPreLogin(
259
+ {
260
+ event: {name: 'client.interstitial-window.launched'},
261
+ },
262
+ preLoginId
263
+ );
264
+
265
+ await flushPromises();
266
+
267
+ clock.tick(config.metrics.batcherWait);
268
+
269
+ await promise;
270
+
271
+ const calls = prepareItemSpy.getCalls()[0];
272
+
273
+ assert.deepEqual(calls.args[0].eventPayload, {
274
+ event: {
275
+ joinTimes: {
276
+ meetingInfoReqResp: undefined,
277
+ clickToInterstitial: undefined,
278
+ clickToInterstitialWithUserDelay: undefined,
279
+ refreshCaptchaServiceReqResp: undefined,
280
+ downloadIntelligenceModelsReqResp: undefined,
281
+ },
282
+ name: 'client.interstitial-window.launched',
283
+ pairedDevice: {
284
+ deviceId: 'my_device_id',
285
+ deviceURL: 'my_url',
286
+ devicePairingType: 'Manual',
287
+ productName: 'my_product_name',
288
+ isPersonalDevice: true,
289
+ },
290
+ pairingState: 'paired',
291
+ },
292
+ origin: {
293
+ buildType: 'test',
294
+ networkType: 'unknown',
295
+ upgradeChannel: 'test',
296
+ },
297
+ });
298
+
299
+ assert.deepEqual(calls.args[0].type, ['diagnostic-event']);
300
+ assert.calledOnce(webex.devicemanager.getPairedDevice);
301
+
302
+ });
235
303
  });
236
304
 
237
305
  describe('savePreLoginId', () => {