@webex/internal-plugin-metrics 3.8.1-next.12 → 3.8.1-next.14

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.
@@ -148,15 +148,27 @@ export default class CallDiagnosticLatencies extends WebexPlugin {
148
148
  * @param b end
149
149
  * @returns latency
150
150
  */
151
- public getDiffBetweenTimestamps(a: MetricEventNames, b: MetricEventNames) {
151
+ public getDiffBetweenTimestamps(
152
+ a: MetricEventNames,
153
+ b: MetricEventNames,
154
+ clampValues?: {minimum?: number; maximum?: number}
155
+ ) {
152
156
  const start = this.latencyTimestamps.get(a);
153
157
  const end = this.latencyTimestamps.get(b);
154
158
 
155
- if (typeof start === 'number' && typeof end === 'number') {
156
- return end - start;
159
+ if (typeof start !== 'number' || typeof end !== 'number') {
160
+ return undefined;
157
161
  }
158
162
 
159
- return undefined;
163
+ const diff = end - start;
164
+
165
+ if (!clampValues) {
166
+ return diff;
167
+ }
168
+
169
+ const {minimum = 0, maximum} = clampValues;
170
+
171
+ return Math.min(maximum ?? Infinity, Math.max(diff, minimum));
160
172
  }
161
173
 
162
174
  /**
@@ -172,7 +184,8 @@ export default class CallDiagnosticLatencies extends WebexPlugin {
172
184
  public getMeetingInfoReqResp() {
173
185
  return this.getDiffBetweenTimestamps(
174
186
  'internal.client.meetinginfo.request',
175
- 'internal.client.meetinginfo.response'
187
+ 'internal.client.meetinginfo.response',
188
+ {maximum: 1200000}
176
189
  );
177
190
  }
178
191
 
@@ -215,7 +228,8 @@ export default class CallDiagnosticLatencies extends WebexPlugin {
215
228
  public getCallInitJoinReq() {
216
229
  return this.getDiffBetweenTimestamps(
217
230
  'internal.client.interstitial-window.click.joinbutton',
218
- 'client.locus.join.request'
231
+ 'client.locus.join.request',
232
+ {maximum: 1200000}
219
233
  );
220
234
  }
221
235
 
@@ -224,7 +238,11 @@ export default class CallDiagnosticLatencies extends WebexPlugin {
224
238
  * @returns - latency
225
239
  */
226
240
  public getJoinReqResp() {
227
- return this.getDiffBetweenTimestamps('client.locus.join.request', 'client.locus.join.response');
241
+ return this.getDiffBetweenTimestamps(
242
+ 'client.locus.join.request',
243
+ 'client.locus.join.response',
244
+ {maximum: 1200000}
245
+ );
228
246
  }
229
247
 
230
248
  /**
@@ -245,7 +263,8 @@ export default class CallDiagnosticLatencies extends WebexPlugin {
245
263
  public getLocalSDPGenRemoteSDPRecv() {
246
264
  return this.getDiffBetweenTimestamps(
247
265
  'client.media-engine.local-sdp-generated',
248
- 'client.media-engine.remote-sdp-received'
266
+ 'client.media-engine.remote-sdp-received',
267
+ {maximum: 1200000}
249
268
  );
250
269
  }
251
270
 
@@ -254,7 +273,7 @@ export default class CallDiagnosticLatencies extends WebexPlugin {
254
273
  * @returns - latency
255
274
  */
256
275
  public getICESetupTime() {
257
- return this.getDiffBetweenTimestamps('client.ice.start', 'client.ice.end');
276
+ return this.getDiffBetweenTimestamps('client.ice.start', 'client.ice.end', {maximum: 1200000});
258
277
  }
259
278
 
260
279
  /**
@@ -378,7 +397,8 @@ export default class CallDiagnosticLatencies extends WebexPlugin {
378
397
  public getCallInitMediaEngineReady() {
379
398
  return this.getDiffBetweenTimestamps(
380
399
  'internal.client.interstitial-window.click.joinbutton',
381
- 'client.media-engine.ready'
400
+ 'client.media-engine.ready',
401
+ {maximum: 1200000}
382
402
  );
383
403
  }
384
404
 
@@ -398,7 +418,9 @@ export default class CallDiagnosticLatencies extends WebexPlugin {
398
418
  const lobbyTime = typeof lobbyTimeLatency === 'number' ? lobbyTimeLatency : 0;
399
419
 
400
420
  if (interstitialJoinClickTimestamp && connectedMedia) {
401
- return connectedMedia - interstitialJoinClickTimestamp - lobbyTime;
421
+ const interstitialToMediaOKJmt = connectedMedia - interstitialJoinClickTimestamp - lobbyTime;
422
+
423
+ return Math.max(0, interstitialToMediaOKJmt);
402
424
  }
403
425
 
404
426
  return undefined;
@@ -463,12 +485,12 @@ export default class CallDiagnosticLatencies extends WebexPlugin {
463
485
  const lobbyTime = this.getStayLobbyTime();
464
486
 
465
487
  if (clickToInterstitial && interstitialToJoinOk && joinConfJMT) {
466
- const totalMediaJMT = clickToInterstitial + interstitialToJoinOk + joinConfJMT;
488
+ const totalMediaJMT = Math.max(0, clickToInterstitial + interstitialToJoinOk + joinConfJMT);
467
489
  if (this.getMeeting()?.allowMediaInLobby) {
468
490
  return totalMediaJMT;
469
491
  }
470
492
 
471
- return totalMediaJMT - lobbyTime;
493
+ return Math.max(0, totalMediaJMT - lobbyTime);
472
494
  }
473
495
 
474
496
  return undefined;
@@ -484,7 +506,7 @@ export default class CallDiagnosticLatencies extends WebexPlugin {
484
506
  const joinConfJMT = this.getJoinConfJMT();
485
507
 
486
508
  if (clickToInterstitialWithUserDelay && interstitialToJoinOk && joinConfJMT) {
487
- return clickToInterstitialWithUserDelay + interstitialToJoinOk + joinConfJMT;
509
+ return Math.max(0, clickToInterstitialWithUserDelay + interstitialToJoinOk + joinConfJMT);
488
510
  }
489
511
 
490
512
  return undefined;
@@ -499,7 +521,7 @@ export default class CallDiagnosticLatencies extends WebexPlugin {
499
521
  const joinConfJMT = this.getJoinConfJMT();
500
522
 
501
523
  if (typeof interstitialToJoinOk === 'number' && typeof joinConfJMT === 'number') {
502
- return interstitialToJoinOk - joinConfJMT;
524
+ return Math.max(0, interstitialToJoinOk - joinConfJMT);
503
525
  }
504
526
 
505
527
  return undefined;
@@ -105,6 +105,8 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
105
105
  private delayedClientFeatureEvents: DelayedClientFeatureEvent[] = [];
106
106
  private eventErrorCache: WeakMap<any, any> = new WeakMap();
107
107
  private isMercuryConnected = false;
108
+ private eventLimitTracker: Map<string, number> = new Map();
109
+ private eventLimitWarningsLogged: Set<string> = new Set();
108
110
 
109
111
  // the default validator before piping an event to the batcher
110
112
  // this function can be overridden by the user
@@ -665,6 +667,144 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
665
667
  this.eventErrorCache = new WeakMap();
666
668
  }
667
669
 
670
+ /**
671
+ * Checks if an event should be limited based on criteria defined in the event dictionary.
672
+ * Returns true if the event should be sent, false if it has reached its limit.
673
+ * @param event - The diagnostic event object
674
+ * @returns boolean indicating whether the event should be sent
675
+ */
676
+ private shouldSendEvent({event}: Event): boolean {
677
+ const eventName = event?.name as string;
678
+ const correlationId = event?.identifiers?.correlationId;
679
+
680
+ if (!correlationId || correlationId === 'unknown') {
681
+ return true;
682
+ }
683
+
684
+ const limitKeyPrefix = `${eventName}:${correlationId}`;
685
+
686
+ switch (eventName) {
687
+ case 'client.media.render.start':
688
+ case 'client.media.render.stop':
689
+ case 'client.media.rx.start':
690
+ case 'client.media.rx.stop':
691
+ case 'client.media.tx.start':
692
+ case 'client.media.tx.stop': {
693
+ // Send only once per mediaType-correlationId pair (or mediaType-correlationId-shareInstanceId for share/share_audio)
694
+ const mediaType = event?.mediaType;
695
+ if (mediaType) {
696
+ if (mediaType === 'share' || mediaType === 'share_audio') {
697
+ const shareInstanceId = event?.shareInstanceId;
698
+ if (shareInstanceId) {
699
+ const limitKey = `${limitKeyPrefix}:${mediaType}:${shareInstanceId}`;
700
+
701
+ return this.checkAndIncrementEventCount(
702
+ limitKey,
703
+ 1,
704
+ `${eventName} for ${mediaType} instance ${shareInstanceId}`
705
+ );
706
+ }
707
+ } else {
708
+ const limitKey = `${limitKeyPrefix}:${mediaType}`;
709
+
710
+ return this.checkAndIncrementEventCount(
711
+ limitKey,
712
+ 1,
713
+ `${eventName} for mediaType ${mediaType}`
714
+ );
715
+ }
716
+ }
717
+ break;
718
+ }
719
+
720
+ case 'client.roap-message.received':
721
+ case 'client.roap-message.sent': {
722
+ // Send only once per correlationId and roap.messageType/roap.type
723
+ const roapMessageType = event?.roap?.messageType || event?.roap?.type;
724
+ if (roapMessageType) {
725
+ const limitKey = `${limitKeyPrefix}:${roapMessageType}`;
726
+
727
+ return this.checkAndIncrementEventCount(
728
+ limitKey,
729
+ 1,
730
+ `${eventName} for ROAP type ${roapMessageType}`
731
+ );
732
+ }
733
+ break;
734
+ }
735
+
736
+ default:
737
+ return true;
738
+ }
739
+
740
+ return true;
741
+ }
742
+
743
+ /**
744
+ * Checks the current count for a limit key and increments if under limit.
745
+ * @param limitKey - The unique key for this limit combination
746
+ * @param maxCount - Maximum allowed count
747
+ * @param eventDescription - Description for logging
748
+ * @returns true if under limit and incremented, false if at/over limit
749
+ */
750
+ private checkAndIncrementEventCount(
751
+ limitKey: string,
752
+ maxCount: number,
753
+ eventDescription: string
754
+ ): boolean {
755
+ const currentCount = this.eventLimitTracker.get(limitKey) || 0;
756
+
757
+ if (currentCount >= maxCount) {
758
+ // Log warning only once per limit key
759
+ if (!this.eventLimitWarningsLogged.has(limitKey)) {
760
+ this.logger.log(
761
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
762
+ `CallDiagnosticMetrics: Event limit reached for ${eventDescription}. ` +
763
+ `Max count ${maxCount} exceeded. Event will not be sent.`,
764
+ `limitKey: ${limitKey}`
765
+ );
766
+ this.eventLimitWarningsLogged.add(limitKey);
767
+ }
768
+
769
+ return false;
770
+ }
771
+
772
+ // Increment count and allow event
773
+ this.eventLimitTracker.set(limitKey, currentCount + 1);
774
+
775
+ return true;
776
+ }
777
+
778
+ /**
779
+ * Clears event limit tracking
780
+ */
781
+ public clearEventLimits(): void {
782
+ this.eventLimitTracker.clear();
783
+ this.eventLimitWarningsLogged.clear();
784
+ }
785
+
786
+ /**
787
+ * Clears event limit tracking for a specific correlationId only.
788
+ * Keeps limits for other meetings intact.
789
+ */
790
+ public clearEventLimitsForCorrelationId(correlationId: string): void {
791
+ if (!correlationId) {
792
+ return;
793
+ }
794
+ // Keys are formatted as "eventName:correlationId:..." across all limiters.
795
+ const hasCorrIdAtSecondToken = (key: string) => key.split(':')[1] === correlationId;
796
+ for (const key of Array.from(this.eventLimitTracker.keys())) {
797
+ if (hasCorrIdAtSecondToken(key)) {
798
+ this.eventLimitTracker.delete(key);
799
+ }
800
+ }
801
+ for (const key of Array.from(this.eventLimitWarningsLogged.values())) {
802
+ if (hasCorrIdAtSecondToken(key)) {
803
+ this.eventLimitWarningsLogged.delete(key);
804
+ }
805
+ }
806
+ }
807
+
668
808
  /**
669
809
  * Generate error payload for Client Event
670
810
  * @param rawError
@@ -1099,6 +1239,10 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
1099
1239
  );
1100
1240
  const diagnosticEvent = this.prepareClientEvent({name, payload, options});
1101
1241
 
1242
+ if (!this.shouldSendEvent(diagnosticEvent)) {
1243
+ return Promise.resolve();
1244
+ }
1245
+
1102
1246
  if (options?.preLoginId) {
1103
1247
  return this.submitToCallDiagnosticsPreLogin(diagnosticEvent, options?.preLoginId);
1104
1248
  }
@@ -130,6 +130,82 @@ describe('internal-plugin-metrics', () => {
130
130
  assert.deepEqual(res2, undefined);
131
131
  });
132
132
 
133
+ describe('getDiffBetweenTimestamps with clamping', () => {
134
+ it('should return diff without clamping when no clampValues provided', () => {
135
+ cdl.saveTimestamp({key: 'client.alert.displayed', value: 10});
136
+ cdl.saveTimestamp({key: 'client.alert.removed', value: 50});
137
+ const res = cdl.getDiffBetweenTimestamps('client.alert.displayed', 'client.alert.removed');
138
+ assert.deepEqual(res, 40);
139
+ });
140
+
141
+ it('should return diff without clamping when value is within range', () => {
142
+ cdl.saveTimestamp({key: 'client.alert.displayed', value: 10});
143
+ cdl.saveTimestamp({key: 'client.alert.removed', value: 50});
144
+ const res = cdl.getDiffBetweenTimestamps('client.alert.displayed', 'client.alert.removed', {
145
+ minimum: 0,
146
+ maximum: 100
147
+ });
148
+ assert.deepEqual(res, 40);
149
+ });
150
+
151
+ it('should clamp to minimum when diff is below minimum', () => {
152
+ cdl.saveTimestamp({key: 'client.alert.displayed', value: 50});
153
+ cdl.saveTimestamp({key: 'client.alert.removed', value: 45});
154
+ const res = cdl.getDiffBetweenTimestamps('client.alert.displayed', 'client.alert.removed', {
155
+ minimum: 10,
156
+ maximum: 100
157
+ });
158
+ assert.deepEqual(res, 10);
159
+ });
160
+
161
+ it('should clamp to maximum when diff is above maximum', () => {
162
+ cdl.saveTimestamp({key: 'client.alert.displayed', value: 10});
163
+ cdl.saveTimestamp({key: 'client.alert.removed', value: 210});
164
+ const res = cdl.getDiffBetweenTimestamps('client.alert.displayed', 'client.alert.removed', {
165
+ minimum: 0,
166
+ maximum: 100
167
+ });
168
+ assert.deepEqual(res, 100);
169
+ });
170
+
171
+ it('should use default minimum of 0 when only maximum is specified', () => {
172
+ cdl.saveTimestamp({key: 'client.alert.displayed', value: 50});
173
+ cdl.saveTimestamp({key: 'client.alert.removed', value: 45});
174
+ const res = cdl.getDiffBetweenTimestamps('client.alert.displayed', 'client.alert.removed', {
175
+ maximum: 100
176
+ });
177
+ assert.deepEqual(res, 0);
178
+ });
179
+
180
+ it('should not clamp maximum when maximum is undefined', () => {
181
+ cdl.saveTimestamp({key: 'client.alert.displayed', value: 10});
182
+ cdl.saveTimestamp({key: 'client.alert.removed', value: 2000});
183
+ const res = cdl.getDiffBetweenTimestamps('client.alert.displayed', 'client.alert.removed', {
184
+ minimum: 5
185
+ });
186
+ assert.deepEqual(res, 1990);
187
+ });
188
+
189
+ it('should handle negative differences correctly with clamping', () => {
190
+ cdl.saveTimestamp({key: 'client.alert.displayed', value: 100});
191
+ cdl.saveTimestamp({key: 'client.alert.removed', value: 50});
192
+ const res = cdl.getDiffBetweenTimestamps('client.alert.displayed', 'client.alert.removed', {
193
+ minimum: 10,
194
+ maximum: 1000
195
+ });
196
+ assert.deepEqual(res, 10);
197
+ });
198
+
199
+ it('should return undefined when timestamps are missing even with clamping', () => {
200
+ cdl.saveTimestamp({key: 'client.alert.displayed', value: 10});
201
+ const res = cdl.getDiffBetweenTimestamps('client.alert.displayed', 'client.alert.removed', {
202
+ minimum: 0,
203
+ maximum: 100
204
+ });
205
+ assert.deepEqual(res, undefined);
206
+ });
207
+ });
208
+
133
209
  it('calculates getMeetingInfoReqResp correctly', () => {
134
210
  cdl.saveTimestamp({key: 'internal.client.meetinginfo.request', value: 10});
135
211
  cdl.saveTimestamp({key: 'internal.client.meetinginfo.response', value: 20});