@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.
- package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js +35 -15
- package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js.map +1 -1
- package/dist/call-diagnostic/call-diagnostic-metrics.js +157 -27
- package/dist/call-diagnostic/call-diagnostic-metrics.js.map +1 -1
- package/dist/metrics.js +1 -1
- package/dist/types/call-diagnostic/call-diagnostic-metrics-latencies.d.ts +4 -1
- package/dist/types/call-diagnostic/call-diagnostic-metrics.d.ts +32 -1
- package/package.json +2 -2
- package/src/call-diagnostic/call-diagnostic-metrics-latencies.ts +37 -15
- package/src/call-diagnostic/call-diagnostic-metrics.ts +144 -0
- package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-latencies.ts +76 -0
- package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +404 -1
|
@@ -148,15 +148,27 @@ export default class CallDiagnosticLatencies extends WebexPlugin {
|
|
|
148
148
|
* @param b end
|
|
149
149
|
* @returns latency
|
|
150
150
|
*/
|
|
151
|
-
public getDiffBetweenTimestamps(
|
|
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
|
|
156
|
-
return
|
|
159
|
+
if (typeof start !== 'number' || typeof end !== 'number') {
|
|
160
|
+
return undefined;
|
|
157
161
|
}
|
|
158
162
|
|
|
159
|
-
|
|
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(
|
|
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
|
-
|
|
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});
|