@webex/plugin-meetings 3.3.1-next.2 → 3.3.1-next.21

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.
Files changed (56) hide show
  1. package/dist/breakouts/breakout.js +1 -1
  2. package/dist/breakouts/index.js +7 -2
  3. package/dist/breakouts/index.js.map +1 -1
  4. package/dist/interpretation/index.js +1 -1
  5. package/dist/interpretation/siLanguage.js +1 -1
  6. package/dist/media/MediaConnectionAwaiter.js +50 -13
  7. package/dist/media/MediaConnectionAwaiter.js.map +1 -1
  8. package/dist/mediaQualityMetrics/config.js +16 -6
  9. package/dist/mediaQualityMetrics/config.js.map +1 -1
  10. package/dist/meeting/connectionStateHandler.js +67 -0
  11. package/dist/meeting/connectionStateHandler.js.map +1 -0
  12. package/dist/meeting/index.js +98 -46
  13. package/dist/meeting/index.js.map +1 -1
  14. package/dist/metrics/constants.js +2 -1
  15. package/dist/metrics/constants.js.map +1 -1
  16. package/dist/metrics/index.js +57 -0
  17. package/dist/metrics/index.js.map +1 -1
  18. package/dist/reachability/clusterReachability.js +108 -53
  19. package/dist/reachability/clusterReachability.js.map +1 -1
  20. package/dist/reachability/index.js +415 -56
  21. package/dist/reachability/index.js.map +1 -1
  22. package/dist/statsAnalyzer/index.js +81 -27
  23. package/dist/statsAnalyzer/index.js.map +1 -1
  24. package/dist/statsAnalyzer/mqaUtil.js +36 -10
  25. package/dist/statsAnalyzer/mqaUtil.js.map +1 -1
  26. package/dist/types/media/MediaConnectionAwaiter.d.ts +18 -4
  27. package/dist/types/mediaQualityMetrics/config.d.ts +11 -0
  28. package/dist/types/meeting/connectionStateHandler.d.ts +30 -0
  29. package/dist/types/meeting/index.d.ts +2 -0
  30. package/dist/types/metrics/constants.d.ts +1 -0
  31. package/dist/types/metrics/index.d.ts +15 -0
  32. package/dist/types/reachability/clusterReachability.d.ts +31 -3
  33. package/dist/types/reachability/index.d.ts +93 -2
  34. package/dist/types/statsAnalyzer/index.d.ts +15 -6
  35. package/dist/types/statsAnalyzer/mqaUtil.d.ts +17 -4
  36. package/dist/webinar/index.js +1 -1
  37. package/package.json +23 -22
  38. package/src/breakouts/index.ts +7 -1
  39. package/src/media/MediaConnectionAwaiter.ts +66 -11
  40. package/src/mediaQualityMetrics/config.ts +14 -3
  41. package/src/meeting/connectionStateHandler.ts +65 -0
  42. package/src/meeting/index.ts +72 -14
  43. package/src/metrics/constants.ts +1 -0
  44. package/src/metrics/index.ts +44 -0
  45. package/src/reachability/clusterReachability.ts +86 -25
  46. package/src/reachability/index.ts +316 -27
  47. package/src/statsAnalyzer/index.ts +85 -24
  48. package/src/statsAnalyzer/mqaUtil.ts +55 -7
  49. package/test/unit/spec/breakouts/index.ts +51 -32
  50. package/test/unit/spec/media/MediaConnectionAwaiter.ts +90 -32
  51. package/test/unit/spec/meeting/connectionStateHandler.ts +102 -0
  52. package/test/unit/spec/meeting/index.js +158 -36
  53. package/test/unit/spec/metrics/index.js +126 -0
  54. package/test/unit/spec/reachability/clusterReachability.ts +116 -22
  55. package/test/unit/spec/reachability/index.ts +1153 -84
  56. package/test/unit/spec/stats-analyzer/index.js +647 -319
@@ -2216,7 +2216,7 @@ describe('plugin-meetings', () => {
2216
2216
  turnDiscoverySkippedReason: undefined,
2217
2217
  });
2218
2218
  meeting.meetingState = 'ACTIVE';
2219
- meeting.mediaProperties.waitForMediaConnectionConnected.rejects(new Error('fake error'));
2219
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
2220
2220
 
2221
2221
  const forceRtcMetricsSend = sinon.stub().resolves();
2222
2222
  const closeMediaConnectionStub = sinon.stub();
@@ -2240,12 +2240,12 @@ describe('plugin-meetings', () => {
2240
2240
  assert.calledTwice(generateClientErrorCodeForIceFailureStub);
2241
2241
  assert.calledWith(generateClientErrorCodeForIceFailureStub, {
2242
2242
  signalingState: 'unknown',
2243
- iceConnectionState: 'unknown',
2243
+ iceConnected: false,
2244
2244
  turnServerUsed: false,
2245
2245
  });
2246
2246
  assert.calledWith(generateClientErrorCodeForIceFailureStub, {
2247
2247
  signalingState: 'unknown',
2248
- iceConnectionState: 'unknown',
2248
+ iceConnected: false,
2249
2249
  turnServerUsed: true,
2250
2250
  });
2251
2251
 
@@ -2440,7 +2440,7 @@ describe('plugin-meetings', () => {
2440
2440
  assert.calledOnce(generateClientErrorCodeForIceFailureStub);
2441
2441
  assert.calledWith(generateClientErrorCodeForIceFailureStub, {
2442
2442
  signalingState: 'unknown',
2443
- iceConnectionState: 'unknown',
2443
+ iceConnected: undefined,
2444
2444
  turnServerUsed: false,
2445
2445
  });
2446
2446
 
@@ -2715,7 +2715,7 @@ describe('plugin-meetings', () => {
2715
2715
  turnDiscoverySkippedReason: undefined,
2716
2716
  });
2717
2717
  meeting.meetingState = 'ACTIVE';
2718
- meeting.mediaProperties.waitForMediaConnectionConnected.rejects(new Error('fake error'));
2718
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
2719
2719
 
2720
2720
  const forceRtcMetricsSend = sinon.stub().resolves();
2721
2721
  const closeMediaConnectionStub = sinon.stub();
@@ -2762,6 +2762,66 @@ describe('plugin-meetings', () => {
2762
2762
  assert.isOk(errorThrown);
2763
2763
  });
2764
2764
 
2765
+ it('should send ICE_CANDIDATE_ERROR metric if media connection fails and ice candidate errors have been gathered', async () => {
2766
+ let errorThrown = undefined;
2767
+
2768
+ meeting.roap.doTurnDiscovery = sinon.stub().returns({
2769
+ turnServerInfo: undefined,
2770
+ turnDiscoverySkippedReason: undefined,
2771
+ });
2772
+ meeting.meetingState = 'ACTIVE';
2773
+ meeting.mediaProperties.waitForMediaConnectionConnected.rejects({iceConnected: false});
2774
+
2775
+ const forceRtcMetricsSend = sinon.stub().resolves();
2776
+ const closeMediaConnectionStub = sinon.stub();
2777
+ Media.createMediaConnection = sinon.stub().returns({
2778
+ close: closeMediaConnectionStub,
2779
+ forceRtcMetricsSend,
2780
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
2781
+ initiateOffer: sinon.stub().resolves({}),
2782
+ on: sinon.stub(),
2783
+ });
2784
+
2785
+ meeting.iceCandidateErrors.set('701_error', 2);
2786
+ meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1);
2787
+
2788
+ await meeting
2789
+ .addMedia({
2790
+ mediaSettings: {},
2791
+ })
2792
+ .catch((err) => {
2793
+ errorThrown = err;
2794
+ assert.instanceOf(err, AddMediaFailed);
2795
+ });
2796
+
2797
+ // Check that the only metric sent is ADD_MEDIA_FAILURE
2798
+ assert.calledOnceWithExactly(
2799
+ Metrics.sendBehavioralMetric,
2800
+ BEHAVIORAL_METRICS.ADD_MEDIA_FAILURE,
2801
+ {
2802
+ correlation_id: meeting.correlationId,
2803
+ locus_id: meeting.locusUrl.split('/').pop(),
2804
+ reason: errorThrown.message,
2805
+ stack: errorThrown.stack,
2806
+ code: errorThrown.code,
2807
+ turnDiscoverySkippedReason: undefined,
2808
+ turnServerUsed: true,
2809
+ retriedWithTurnServer: false,
2810
+ isMultistream: false,
2811
+ isJoinWithMediaRetry: false,
2812
+ signalingState: 'unknown',
2813
+ connectionState: 'unknown',
2814
+ iceConnectionState: 'unknown',
2815
+ selectedCandidatePairChanges: 2,
2816
+ numTransports: 1,
2817
+ '701_error': 2,
2818
+ '701_turn_host_lookup_received_error': 1
2819
+ }
2820
+ );
2821
+
2822
+ assert.isOk(errorThrown);
2823
+ });
2824
+
2765
2825
  describe('handles StatsAnalyzer events', () => {
2766
2826
  let prevConfigValue;
2767
2827
  let statsAnalyzerStub;
@@ -3058,7 +3118,7 @@ describe('plugin-meetings', () => {
3058
3118
 
3059
3119
  meeting.meetingState = 'ACTIVE';
3060
3120
  meeting.mediaProperties.waitForMediaConnectionConnected.rejects(
3061
- new Error('fake error')
3121
+ {iceConnected: false}
3062
3122
  );
3063
3123
 
3064
3124
  let errorThrown = false;
@@ -3073,7 +3133,7 @@ describe('plugin-meetings', () => {
3073
3133
 
3074
3134
  assert.calledOnceWithExactly(generateClientErrorCodeForIceFailureStub, {
3075
3135
  signalingState: 'unknown',
3076
- iceConnectionState: 'unknown',
3136
+ iceConnected: false,
3077
3137
  turnServerUsed: true,
3078
3138
  });
3079
3139
 
@@ -6275,7 +6335,7 @@ describe('plugin-meetings', () => {
6275
6335
  },
6276
6336
  'SELF_OBSERVING'
6277
6337
  );
6278
-
6338
+
6279
6339
 
6280
6340
  // Verify that the event handler behaves as expected
6281
6341
  expect(meeting.statsAnalyzer.stopAnalyzer.calledOnce).to.be.true;
@@ -7079,6 +7139,10 @@ describe('plugin-meetings', () => {
7079
7139
  id: 'stream',
7080
7140
  getTracks: () => [{id: 'track', addEventListener: sinon.stub()}],
7081
7141
  };
7142
+ const simulateConnectionStateChange = (newState) => {
7143
+ meeting.mediaProperties.webrtcMediaConnection.getConnectionState = sinon.stub().returns(newState);
7144
+ eventListeners[Event.PEER_CONNECTION_STATE_CHANGED]();
7145
+ }
7082
7146
 
7083
7147
  beforeEach(() => {
7084
7148
  eventListeners = {};
@@ -7088,6 +7152,7 @@ describe('plugin-meetings', () => {
7088
7152
  on: sinon.stub().callsFake((event, listener) => {
7089
7153
  eventListeners[event] = listener;
7090
7154
  }),
7155
+ getConnectionState: sinon.stub().returns(ConnectionState.New),
7091
7156
  };
7092
7157
  MediaUtil.createMediaStream.returns(fakeStream);
7093
7158
  });
@@ -7099,7 +7164,9 @@ describe('plugin-meetings', () => {
7099
7164
  assert.isFunction(eventListeners[Event.ROAP_FAILURE]);
7100
7165
  assert.isFunction(eventListeners[Event.ROAP_MESSAGE_TO_SEND]);
7101
7166
  assert.isFunction(eventListeners[Event.REMOTE_TRACK_ADDED]);
7102
- assert.isFunction(eventListeners[Event.CONNECTION_STATE_CHANGED]);
7167
+ assert.isFunction(eventListeners[Event.PEER_CONNECTION_STATE_CHANGED]);
7168
+ assert.isFunction(eventListeners[Event.ICE_CONNECTION_STATE_CHANGED]);
7169
+ assert.isFunction(eventListeners[Event.ICE_CANDIDATE_ERROR]);
7103
7170
  });
7104
7171
 
7105
7172
  it('should trigger a media:ready event when REMOTE_TRACK_ADDED is fired', () => {
@@ -7135,13 +7202,45 @@ describe('plugin-meetings', () => {
7135
7202
  });
7136
7203
  });
7137
7204
 
7205
+ describe('should react on a ICE_CANDIDATE_ERROR event', () => {
7206
+ beforeEach(() => {
7207
+ meeting.setupMediaConnectionListeners();
7208
+
7209
+ });
7210
+
7211
+ it('should not collect skipped ice candidates error', () => {
7212
+ eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 600, errorText: 'Address not associated with the desired network interface.' }});
7213
+
7214
+ assert.equal(meeting.iceCandidateErrors.size, 0);
7215
+ });
7216
+
7217
+ it('should collect valid ice candidates error', () => {
7218
+ eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: '' }});
7219
+
7220
+ assert.equal(meeting.iceCandidateErrors.size, 1);
7221
+ assert.equal(meeting.iceCandidateErrors.has('701_'), true);
7222
+ });
7223
+
7224
+ it('should increment counter if same valid ice candidates error collected', () => {
7225
+ eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: '' }});
7226
+
7227
+ eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: 'STUN host lookup received error.' }});
7228
+ eventListeners[Event.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: 'STUN host lookup received error.' }});
7229
+
7230
+ assert.equal(meeting.iceCandidateErrors.size, 2);
7231
+ assert.equal(meeting.iceCandidateErrors.has('701_'), true);
7232
+ assert.equal(meeting.iceCandidateErrors.get('701_'), 1);
7233
+ assert.equal(meeting.iceCandidateErrors.has('701_stun_host_lookup_received_error'), true);
7234
+ assert.equal(meeting.iceCandidateErrors.get('701_stun_host_lookup_received_error'), 2);
7235
+ });
7236
+ });
7237
+
7138
7238
  describe('CONNECTION_STATE_CHANGED event when state = "Connecting"', () => {
7139
7239
  it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = true', () => {
7140
7240
  meeting.hasMediaConnectionConnectedAtLeastOnce = true;
7141
7241
  meeting.setupMediaConnectionListeners();
7142
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7143
- state: 'Connecting',
7144
- });
7242
+
7243
+ simulateConnectionStateChange(ConnectionState.Connecting);
7145
7244
 
7146
7245
  assert.notCalled(webex.internal.newMetrics.submitClientEvent);
7147
7246
  });
@@ -7149,9 +7248,8 @@ describe('plugin-meetings', () => {
7149
7248
  it('sends client.ice.start correctly when hasMediaConnectionConnectedAtLeastOnce = false', () => {
7150
7249
  meeting.hasMediaConnectionConnectedAtLeastOnce = false;
7151
7250
  meeting.setupMediaConnectionListeners();
7152
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7153
- state: 'Connecting',
7154
- });
7251
+
7252
+ simulateConnectionStateChange(ConnectionState.Connecting);
7155
7253
 
7156
7254
  assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
7157
7255
  assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
@@ -7177,6 +7275,7 @@ describe('plugin-meetings', () => {
7177
7275
  on: sinon.stub().callsFake((event, listener) => {
7178
7276
  eventListeners[event] = listener;
7179
7277
  }),
7278
+ getConnectionState: sinon.stub().returns(ConnectionState.Connected),
7180
7279
  };
7181
7280
  };
7182
7281
 
@@ -7230,9 +7329,7 @@ describe('plugin-meetings', () => {
7230
7329
  assert.equal(meeting.hasMediaConnectionConnectedAtLeastOnce, false);
7231
7330
 
7232
7331
  // simulate first connection success
7233
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7234
- state: 'Connected',
7235
- });
7332
+ simulateConnectionStateChange(ConnectionState.Connected);
7236
7333
  checkExpectedSpies({
7237
7334
  icePhase: 'JOIN_MEETING_FINAL',
7238
7335
  setNetworkStatusCallParams: [NETWORK_STATUS.CONNECTED],
@@ -7242,12 +7339,9 @@ describe('plugin-meetings', () => {
7242
7339
  // now simulate short connection loss, client.ice.end is not sent a second time as hasMediaConnectionConnectedAtLeastOnce = true
7243
7340
  resetSpies();
7244
7341
 
7245
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7246
- state: 'Disconnected',
7247
- });
7248
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7249
- state: 'Connected',
7250
- });
7342
+ simulateConnectionStateChange(ConnectionState.Disconnected);
7343
+
7344
+ simulateConnectionStateChange(ConnectionState.Connected);
7251
7345
 
7252
7346
  checkExpectedSpies({
7253
7347
  setNetworkStatusCallParams: [NETWORK_STATUS.DISCONNECTED, NETWORK_STATUS.CONNECTED],
@@ -7255,12 +7349,9 @@ describe('plugin-meetings', () => {
7255
7349
 
7256
7350
  resetSpies();
7257
7351
 
7258
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7259
- state: 'Disconnected',
7260
- });
7261
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7262
- state: 'Connected',
7263
- });
7352
+ simulateConnectionStateChange(ConnectionState.Disconnected);
7353
+
7354
+ simulateConnectionStateChange(ConnectionState.Connected);
7264
7355
  });
7265
7356
  });
7266
7357
 
@@ -7282,9 +7373,8 @@ describe('plugin-meetings', () => {
7282
7373
 
7283
7374
  const mockDisconnectedEvent = () => {
7284
7375
  meeting.setupMediaConnectionListeners();
7285
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7286
- state: 'Disconnected',
7287
- });
7376
+
7377
+ simulateConnectionStateChange(ConnectionState.Disconnected);
7288
7378
  };
7289
7379
 
7290
7380
  const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
@@ -7348,9 +7438,8 @@ describe('plugin-meetings', () => {
7348
7438
  describe('CONNECTION_STATE_CHANGED event when state = "Failed"', () => {
7349
7439
  const mockFailedEvent = () => {
7350
7440
  meeting.setupMediaConnectionListeners();
7351
- eventListeners[Event.CONNECTION_STATE_CHANGED]({
7352
- state: 'Failed',
7353
- });
7441
+
7442
+ simulateConnectionStateChange(ConnectionState.Failed);
7354
7443
  };
7355
7444
 
7356
7445
  const checkBehavioralMetricSent = (hasMediaConnectionConnectedAtLeastOnce = false) => {
@@ -9781,6 +9870,7 @@ describe('plugin-meetings', () => {
9781
9870
  beforeEach(() => {
9782
9871
  webex.internal.llm.isConnected = sinon.stub().returns(false);
9783
9872
  webex.internal.llm.getLocusUrl = sinon.stub();
9873
+ webex.internal.llm.getDatachannelUrl = sinon.stub();
9784
9874
  webex.internal.llm.registerAndConnect = sinon
9785
9875
  .stub()
9786
9876
  .returns(Promise.resolve('something'));
@@ -9808,6 +9898,7 @@ describe('plugin-meetings', () => {
9808
9898
  meeting.joinedWith = {state: 'JOINED'};
9809
9899
  webex.internal.llm.isConnected.returns(true);
9810
9900
  webex.internal.llm.getLocusUrl.returns('a url');
9901
+ webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
9811
9902
 
9812
9903
  meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a datachannel url'}};
9813
9904
 
@@ -9844,6 +9935,7 @@ describe('plugin-meetings', () => {
9844
9935
  meeting.joinedWith = {state: 'JOINED'};
9845
9936
  webex.internal.llm.isConnected.returns(true);
9846
9937
  webex.internal.llm.getLocusUrl.returns('a url');
9938
+ webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
9847
9939
 
9848
9940
  meeting.locusInfo = {url: 'a different url', info: {datachannelUrl: 'a datachannel url'}};
9849
9941
 
@@ -9869,6 +9961,36 @@ describe('plugin-meetings', () => {
9869
9961
  );
9870
9962
  });
9871
9963
 
9964
+ it('disconnects if first if the data channel url has changed', async () => {
9965
+ meeting.joinedWith = {state: 'JOINED'};
9966
+ webex.internal.llm.isConnected.returns(true);
9967
+ webex.internal.llm.getLocusUrl.returns('a url');
9968
+ webex.internal.llm.getDatachannelUrl.returns('a datachannel url');
9969
+
9970
+ meeting.locusInfo = {url: 'a url', info: {datachannelUrl: 'a different datachannel url'}};
9971
+
9972
+ const result = await meeting.updateLLMConnection();
9973
+
9974
+ assert.calledWith(webex.internal.llm.disconnectLLM);
9975
+ assert.calledWith(
9976
+ webex.internal.llm.registerAndConnect,
9977
+ 'a url',
9978
+ 'a different datachannel url'
9979
+ );
9980
+ assert.equal(result, 'something');
9981
+ assert.calledWithExactly(
9982
+ meeting.webex.internal.llm.off,
9983
+ 'event:relay.event',
9984
+ meeting.processRelayEvent
9985
+ );
9986
+ assert.calledTwice(meeting.webex.internal.llm.off);
9987
+ assert.calledOnceWithExactly(
9988
+ meeting.webex.internal.llm.on,
9989
+ 'event:relay.event',
9990
+ meeting.processRelayEvent
9991
+ );
9992
+ });
9993
+
9872
9994
  it('disconnects when the state is not JOINED', async () => {
9873
9995
  meeting.joinedWith = {state: 'any other state'};
9874
9996
  webex.internal.llm.isConnected.returns(true);
@@ -58,4 +58,130 @@ describe('Meeting metrics', () => {
58
58
  });
59
59
  });
60
60
  });
61
+
62
+ describe('prepareMetricFields', () => {
63
+ it('handles empty objects correctly', () => {
64
+ const result = metrics.prepareMetricFields({});
65
+
66
+ assert.deepEqual(result, {});
67
+ });
68
+
69
+ it('handles literal values correctly', () => {
70
+ assert.deepEqual(metrics.prepareMetricFields('any string'), {value: 'any string'});
71
+ assert.deepEqual(metrics.prepareMetricFields(999), {value: 999});
72
+ assert.deepEqual(metrics.prepareMetricFields(null), {value: null});
73
+ assert.deepEqual(metrics.prepareMetricFields(true), {value: true});
74
+ assert.deepEqual(metrics.prepareMetricFields(false), {value: false});
75
+ });
76
+
77
+ it('handles simple objects correctly', () => {
78
+ const result = metrics.prepareMetricFields({
79
+ someStringProp: 'some string',
80
+ numberProp: 100,
81
+ booleanPropFalse: false,
82
+ booleanPropTrue: true,
83
+ });
84
+
85
+ assert.deepEqual(result, {
86
+ someStringProp: 'some string',
87
+ numberProp: 100,
88
+ booleanPropFalse: false,
89
+ booleanPropTrue: true,
90
+ });
91
+ });
92
+
93
+ it('handles nested objects correctly', () => {
94
+ const result = metrics.prepareMetricFields({
95
+ someStringProp: 'some string',
96
+ nestedObject: {
97
+ stringProp: 'another string',
98
+ numberProp: 1234,
99
+ deepObject: {
100
+ oneMoreString: 'deep nested string',
101
+ someBoolean: true,
102
+ oneMoreNumber: 10,
103
+ },
104
+ },
105
+ });
106
+
107
+ assert.deepEqual(result, {
108
+ someStringProp: 'some string',
109
+ nestedObject_stringProp: 'another string',
110
+ nestedObject_numberProp: 1234,
111
+ nestedObject_deepObject_oneMoreString: 'deep nested string',
112
+ nestedObject_deepObject_someBoolean: true,
113
+ nestedObject_deepObject_oneMoreNumber: 10,
114
+ });
115
+ });
116
+
117
+ it('handles arrays correctly', () => {
118
+ const result = metrics.prepareMetricFields(['a string', 10, true, false, {id: 'object id'}]);
119
+
120
+ assert.deepEqual(result, {
121
+ 0: 'a string',
122
+ 1: 10,
123
+ 2: true,
124
+ 3: false,
125
+ '4_id': 'object id',
126
+ });
127
+ });
128
+
129
+ it('handles arrays nested in objects correctly', () => {
130
+ const result = metrics.prepareMetricFields({
131
+ something: 10,
132
+ anObject: {
133
+ someArray: ['a string', 10, true, false, {id: 'object id'}],
134
+ },
135
+ });
136
+
137
+ assert.deepEqual(result, {
138
+ something: 10,
139
+ anObject_someArray_0: 'a string',
140
+ anObject_someArray_1: 10,
141
+ anObject_someArray_2: true,
142
+ anObject_someArray_3: false,
143
+ anObject_someArray_4_id: 'object id',
144
+ });
145
+ });
146
+
147
+ it('handles arrays nested in arrays correctly', () => {
148
+ const result = metrics.prepareMetricFields({
149
+ something: 10,
150
+ topLevelArray: [
151
+ [1, 2, 'three', {prop1: '1st prop of object 1', prop2: '2nd prop of object 1'}],
152
+ [10, 20, 'thirty', {prop1: '1st prop of object 2', prop2: '2nd prop of object 2'}],
153
+ ],
154
+ });
155
+
156
+ assert.deepEqual(result, {
157
+ something: 10,
158
+ topLevelArray_0_0: 1,
159
+ topLevelArray_0_1: 2,
160
+ topLevelArray_0_2: 'three',
161
+ topLevelArray_0_3_prop1: '1st prop of object 1',
162
+ topLevelArray_0_3_prop2: '2nd prop of object 1',
163
+ topLevelArray_1_0: 10,
164
+ topLevelArray_1_1: 20,
165
+ topLevelArray_1_2: 'thirty',
166
+ topLevelArray_1_3_prop1: '1st prop of object 2',
167
+ topLevelArray_1_3_prop2: '2nd prop of object 2',
168
+ });
169
+ });
170
+
171
+ it('prepends the prefix', () => {
172
+ const result = metrics.prepareMetricFields({
173
+ someStringProp: 'a string',
174
+ numberProp: 111,
175
+ booleanPropFalse: false,
176
+ booleanPropTrue: true,
177
+ }, 'testPrefix');
178
+
179
+ assert.deepEqual(result, {
180
+ testPrefix_someStringProp: 'a string',
181
+ testPrefix_numberProp: 111,
182
+ testPrefix_booleanPropFalse: false,
183
+ testPrefix_booleanPropTrue: true,
184
+ });
185
+ })
186
+ });
61
187
  });