From 63020949ebbe18decd22ca4d4d7e34a477404835 Mon Sep 17 00:00:00 2001 From: Filip Nowakowski Date: Wed, 4 Sep 2024 15:06:22 +0200 Subject: [PATCH 01/48] fix(plugin-meetings): clientSignallingProtocol stat (#3803) Co-authored-by: Filip Nowakowski --- .../src/call-diagnostic/call-diagnostic-metrics.ts | 3 +++ .../unit/spec/call-diagnostic/call-diagnostic-metrics.ts | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.ts b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.ts index 2da21fa487a..d1f0e1721eb 100644 --- a/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.ts +++ b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.ts @@ -455,6 +455,9 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin { }, intervals: payload.intervals, callingServiceType: 'LOCUS', + meetingJoinInfo: { + clientSignallingProtocol: 'WebRTC', + }, sourceMetadata: { applicationSoftwareType: CLIENT_NAME, // @ts-ignore diff --git a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts index ea534d02cca..dd2bcebb648 100644 --- a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +++ b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts @@ -1454,6 +1454,9 @@ describe('internal-plugin-metrics', () => { eventData: {webClientDomain: 'whatever'}, intervals: [{}], callingServiceType: 'LOCUS', + meetingJoinInfo: { + clientSignallingProtocol: 'WebRTC', + }, sourceMetadata: { applicationSoftwareType: 'webex-js-sdk', applicationSoftwareVersion: 'webex-version', @@ -1490,6 +1493,9 @@ describe('internal-plugin-metrics', () => { eventData: {webClientDomain: 'whatever'}, intervals: [{}], callingServiceType: 'LOCUS', + meetingJoinInfo: { + clientSignallingProtocol: 'WebRTC', + }, sourceMetadata: { applicationSoftwareType: 'webex-js-sdk', applicationSoftwareVersion: 'webex-version', @@ -1524,6 +1530,9 @@ describe('internal-plugin-metrics', () => { eventData: {webClientDomain: 'whatever'}, intervals: [{}], callingServiceType: 'LOCUS', + meetingJoinInfo: { + clientSignallingProtocol: 'WebRTC', + }, sourceMetadata: { applicationSoftwareType: 'webex-js-sdk', applicationSoftwareVersion: 'webex-version', From d354100b91c0f9dbf84b2ba65f8e762c05648b63 Mon Sep 17 00:00:00 2001 From: Charles Burkett Date: Wed, 4 Sep 2024 13:01:18 -0400 Subject: [PATCH 02/48] fix(audio-mute): fix sometimes out-of-sync remote audio mute (#3797) --- .../src/locus-info/selfUtils.ts | 5 -- .../plugin-meetings/src/meeting/muteState.ts | 7 ++- .../test/unit/spec/locus-info/selfUtils.js | 48 ++++++++++--------- .../test/unit/spec/meeting/muteState.js | 24 ++++++++++ 4 files changed, 55 insertions(+), 29 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/locus-info/selfUtils.ts b/packages/@webex/plugin-meetings/src/locus-info/selfUtils.ts index 27079d8221b..0ba3e4d8178 100644 --- a/packages/@webex/plugin-meetings/src/locus-info/selfUtils.ts +++ b/packages/@webex/plugin-meetings/src/locus-info/selfUtils.ts @@ -428,11 +428,6 @@ SelfUtils.mutedByOthersChanged = (oldSelf, changedSelf) => { return false; } - // there is no need to trigger user update if no one muted user - if (changedSelf.selfIdentity === changedSelf.modifiedBy) { - return false; - } - return ( changedSelf.remoteMuted !== null && (oldSelf.remoteMuted !== changedSelf.remoteMuted || diff --git a/packages/@webex/plugin-meetings/src/meeting/muteState.ts b/packages/@webex/plugin-meetings/src/meeting/muteState.ts index 1c3ed6ccbc2..e834514e732 100644 --- a/packages/@webex/plugin-meetings/src/meeting/muteState.ts +++ b/packages/@webex/plugin-meetings/src/meeting/muteState.ts @@ -379,7 +379,12 @@ export class MuteState { } if (muted !== undefined) { this.state.server.remoteMute = muted; - this.muteLocalStream(meeting, muted, 'remotelyMuted'); + + // We never want to unmute the local stream from a server remote mute update. + // Moderated unmute is handled by a different function. + if (muted) { + this.muteLocalStream(meeting, muted, 'remotelyMuted'); + } } } diff --git a/packages/@webex/plugin-meetings/test/unit/spec/locus-info/selfUtils.js b/packages/@webex/plugin-meetings/test/unit/spec/locus-info/selfUtils.js index c0656946711..9ad22a69acb 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/locus-info/selfUtils.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/locus-info/selfUtils.js @@ -345,37 +345,39 @@ describe('plugin-meetings', () => { }); describe('mutedByOthersChanged', () => { - it('throws an error if changedSelf is not provided', function() { - assert.throws(() => SelfUtils.mutedByOthersChanged({}, null), 'New self must be defined to determine if self was muted by others.'); - }); - - it('return false when oldSelf is not defined', function() { - assert.equal(SelfUtils.mutedByOthersChanged(null, { remoteMuted: false }), false); + it('throws an error if changedSelf is not provided', function () { + assert.throws( + () => SelfUtils.mutedByOthersChanged({}, null), + 'New self must be defined to determine if self was muted by others.' + ); }); - it('should return true when remoteMuted is true on entry', function() { - assert.equal(SelfUtils.mutedByOthersChanged(null, { remoteMuted: true }), true); + it('return false when oldSelf is not defined', function () { + assert.equal(SelfUtils.mutedByOthersChanged(null, {remoteMuted: false}), false); }); - it('should return false when selfIdentity and modifiedBy are the same', function() { - assert.equal(SelfUtils.mutedByOthersChanged( - { remoteMuted: false }, - { remoteMuted: true, selfIdentity: 'user1', modifiedBy: 'user1' } - ), false); + it('should return true when remoteMuted is true on entry', function () { + assert.equal(SelfUtils.mutedByOthersChanged(null, {remoteMuted: true}), true); }); - it('should return true when remoteMuted values are different', function() { - assert.equal(SelfUtils.mutedByOthersChanged( - { remoteMuted: false }, - { remoteMuted: true, selfIdentity: 'user1', modifiedBy: 'user2' } - ), true); + it('should return true when remoteMuted values are different', function () { + assert.equal( + SelfUtils.mutedByOthersChanged( + {remoteMuted: false}, + {remoteMuted: true, selfIdentity: 'user1', modifiedBy: 'user2'} + ), + true + ); }); - it('should return true when remoteMuted is true and unmuteAllowed has changed', function() { - assert.equal(SelfUtils.mutedByOthersChanged( - { remoteMuted: true, unmuteAllowed: false }, - { remoteMuted: true, unmuteAllowed: true, selfIdentity: 'user1', modifiedBy: 'user2' } - ), true); + it('should return true when remoteMuted is true and unmuteAllowed has changed', function () { + assert.equal( + SelfUtils.mutedByOthersChanged( + {remoteMuted: true, unmuteAllowed: false}, + {remoteMuted: true, unmuteAllowed: true, selfIdentity: 'user1', modifiedBy: 'user2'} + ), + true + ); }); }); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/muteState.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/muteState.js index 217a265a708..7af0df9e88b 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/muteState.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/muteState.js @@ -113,6 +113,30 @@ describe('plugin-meetings', () => { assert.isTrue(audio.isRemotelyMuted()); }); + it('does not locally unmute on a server unmute', async () => { + const setServerMutedSpy = meeting.mediaProperties.audioStream.setServerMuted; + + // simulate remote mute + audio.handleServerRemoteMuteUpdate(meeting, true, true); + + assert.isTrue(audio.isRemotelyMuted()); + assert.isTrue(audio.isLocallyMuted()); + + // mutes local + assert.calledOnceWithExactly(setServerMutedSpy, true, 'remotelyMuted'); + + setServerMutedSpy.resetHistory(); + + // simulate remote unmute + audio.handleServerRemoteMuteUpdate(meeting, false, true); + + assert.isFalse(audio.isRemotelyMuted()); + assert.isTrue(audio.isLocallyMuted()); + + // does not unmute local + assert.notCalled(setServerMutedSpy); + }); + it('does local audio unmute if localAudioUnmuteRequired is received', async () => { // first we need to have the local stream user muted meeting.mediaProperties.audioStream.userMuted = true; From 73e75b76d046f18d805ae1d5652abb23431ea0a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edmond=20Vuji=C4=87i?= <67634227+edvujic@users.noreply.github.com> Date: Thu, 5 Sep 2024 09:50:01 +0200 Subject: [PATCH 03/48] fix: update internal media core version (#3806) Co-authored-by: evujici --- packages/@webex/media-helpers/package.json | 2 +- packages/@webex/plugin-meetings/package.json | 2 +- packages/calling/package.json | 2 +- yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/@webex/media-helpers/package.json b/packages/@webex/media-helpers/package.json index b95c6dd1c99..512e8e6fe42 100644 --- a/packages/@webex/media-helpers/package.json +++ b/packages/@webex/media-helpers/package.json @@ -22,7 +22,7 @@ "deploy:npm": "yarn npm publish" }, "dependencies": { - "@webex/internal-media-core": "2.9.1", + "@webex/internal-media-core": "2.9.2", "@webex/ts-events": "^1.1.0", "@webex/web-media-effects": "2.18.0" }, diff --git a/packages/@webex/plugin-meetings/package.json b/packages/@webex/plugin-meetings/package.json index 02c7f99e073..31a709e3185 100644 --- a/packages/@webex/plugin-meetings/package.json +++ b/packages/@webex/plugin-meetings/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@webex/common": "workspace:*", - "@webex/internal-media-core": "2.9.1", + "@webex/internal-media-core": "2.9.2", "@webex/internal-plugin-conversation": "workspace:*", "@webex/internal-plugin-device": "workspace:*", "@webex/internal-plugin-llm": "workspace:*", diff --git a/packages/calling/package.json b/packages/calling/package.json index d149f8f9b62..9ab50e04dff 100644 --- a/packages/calling/package.json +++ b/packages/calling/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "@types/platform": "1.3.4", - "@webex/internal-media-core": "2.9.1", + "@webex/internal-media-core": "2.9.2", "@webex/media-helpers": "workspace:*", "async-mutex": "0.4.0", "buffer": "6.0.3", diff --git a/yarn.lock b/yarn.lock index e2d039cfc0c..f789ccf87b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7419,7 +7419,7 @@ __metadata: "@typescript-eslint/eslint-plugin": 5.38.1 "@typescript-eslint/parser": 5.38.1 "@web/dev-server": 0.4.5 - "@webex/internal-media-core": 2.9.1 + "@webex/internal-media-core": 2.9.2 "@webex/media-helpers": "workspace:*" async-mutex: 0.4.0 buffer: 6.0.3 @@ -7710,9 +7710,9 @@ __metadata: languageName: unknown linkType: soft -"@webex/internal-media-core@npm:2.9.1": - version: 2.9.1 - resolution: "@webex/internal-media-core@npm:2.9.1" +"@webex/internal-media-core@npm:2.9.2": + version: 2.9.2 + resolution: "@webex/internal-media-core@npm:2.9.2" dependencies: "@babel/runtime": ^7.18.9 "@babel/runtime-corejs2": ^7.25.0 @@ -7724,7 +7724,7 @@ __metadata: uuid: ^8.3.2 webrtc-adapter: ^8.1.2 xstate: ^4.30.6 - checksum: 358a8b43ae55d6272fa07ca6e075cceb07a29d464c7e494ec130cd918848595eecd8e52ff12407790051af92b5e7d9612fdb4d7c6cf7a11231ce3bb390bd10f3 + checksum: 429ccc2a73ae226fb721f6fcd72f50653ded0fd54f8682542321d2b3dd7504a79d46c8746e2abf24d83c17401c0302c82bde4c73d232036afbbbcb544fd6639e languageName: node linkType: hard @@ -8483,7 +8483,7 @@ __metadata: "@babel/preset-typescript": 7.22.11 "@webex/babel-config-legacy": "workspace:*" "@webex/eslint-config-legacy": "workspace:*" - "@webex/internal-media-core": 2.9.1 + "@webex/internal-media-core": 2.9.2 "@webex/jest-config-legacy": "workspace:*" "@webex/legacy-tools": "workspace:*" "@webex/test-helper-chai": "workspace:*" @@ -8719,7 +8719,7 @@ __metadata: "@webex/babel-config-legacy": "workspace:*" "@webex/common": "workspace:*" "@webex/eslint-config-legacy": "workspace:*" - "@webex/internal-media-core": 2.9.1 + "@webex/internal-media-core": 2.9.2 "@webex/internal-plugin-conversation": "workspace:*" "@webex/internal-plugin-device": "workspace:*" "@webex/internal-plugin-llm": "workspace:*" From 90cc9c20116ddf29b9ca68c2c4554d28b259ae64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Wa=C5=9Bniowski?= Date: Thu, 5 Sep 2024 10:58:54 +0200 Subject: [PATCH 04/48] fix(plugin-meetings): add to peer connection bare turn tcp server (#3808) --- .../@webex/plugin-meetings/src/media/index.ts | 13 + .../test/unit/spec/media/index.ts | 52 ++-- .../test/unit/spec/meeting/index.js | 222 ++++++++++++------ 3 files changed, 196 insertions(+), 91 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/media/index.ts b/packages/@webex/plugin-meetings/src/media/index.ts index 15d2376106e..a18040cdff8 100644 --- a/packages/@webex/plugin-meetings/src/media/index.ts +++ b/packages/@webex/plugin-meetings/src/media/index.ts @@ -163,6 +163,19 @@ Media.createMediaConnection = ( // we might not have any TURN server if TURN discovery failed or wasn't done or // we might get an empty TURN url if we land on a video mesh node if (turnServerInfo?.url) { + if (!isBrowser('firefox')) { + let bareTurnServer = turnServerInfo.url; + bareTurnServer = bareTurnServer.replace('turns:', 'turn:'); + bareTurnServer = bareTurnServer.replace('443', '5004'); + + iceServers.push({ + urls: bareTurnServer, + username: turnServerInfo.username || '', + credential: turnServerInfo.password || '', + }); + } + + // TURN-TLS server iceServers.push({ urls: turnServerInfo.url, username: turnServerInfo.username || '', diff --git a/packages/@webex/plugin-meetings/test/unit/spec/media/index.ts b/packages/@webex/plugin-meetings/test/unit/spec/media/index.ts index 4c5e7acea08..8a28348a348 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/media/index.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/media/index.ts @@ -16,39 +16,39 @@ describe('createMediaConnection', () => { id: 'roap media connection', }; const fakeTrack = { - id: 'any fake track' - } + id: 'any fake track', + }; const fakeAudioStream = { outputStream: { getTracks: () => { return [fakeTrack]; - } - } + }, + }, }; const fakeVideoStream = { outputStream: { getTracks: () => { return [fakeTrack]; - } - } + }, + }, }; const fakeShareVideoStream = { outputStream: { getTracks: () => { return [fakeTrack]; - } - } + }, + }, }; const fakeShareAudioStream = { outputStream: { getTracks: () => { return [fakeTrack]; - } - } + }, + }, }; afterEach(() => { sinon.restore(); - clock.uninstall() + clock.uninstall(); }); it('creates a RoapMediaConnection when multistream is disabled', () => { @@ -80,7 +80,7 @@ describe('createMediaConnection', () => { enableRtx: ENABLE_RTX, enableExtmap: ENABLE_EXTMAP, turnServerInfo: { - url: 'turn server url', + url: 'turns:turn-server-url:443?transport=tcp', username: 'turn username', password: 'turn password', }, @@ -91,7 +91,12 @@ describe('createMediaConnection', () => { { iceServers: [ { - urls: 'turn server url', + urls: 'turn:turn-server-url:5004?transport=tcp', + username: 'turn username', + credential: 'turn password', + }, + { + urls: 'turns:turn-server-url:443?transport=tcp', username: 'turn username', credential: 'turn password', }, @@ -146,7 +151,7 @@ describe('createMediaConnection', () => { }, }, turnServerInfo: { - url: 'turn server url', + url: 'turns:turn-server-url:443?transport=tcp', username: 'turn username', password: 'turn password', }, @@ -158,7 +163,12 @@ describe('createMediaConnection', () => { { iceServers: [ { - urls: 'turn server url', + urls: 'turn:turn-server-url:5004?transport=tcp', + username: 'turn username', + credential: 'turn password', + }, + { + urls: 'turns:turn-server-url:443?transport=tcp', username: 'turn username', credential: 'turn password', }, @@ -171,7 +181,10 @@ describe('createMediaConnection', () => { [ {testCase: 'turnServerInfo is undefined', turnServerInfo: undefined}, - {testCase: 'turnServerInfo.url is empty string', turnServerInfo: {url: '', username: 'turn username', password: 'turn password'}}, + { + testCase: 'turnServerInfo.url is empty string', + turnServerInfo: {url: '', username: 'turn username', password: 'turn password'}, + }, ].forEach(({testCase, turnServerInfo}) => { it(`passes empty ICE servers array to MultistreamRoapMediaConnection if ${testCase} (multistream enabled)`, () => { const multistreamRoapMediaConnectionConstructorStub = sinon @@ -198,7 +211,7 @@ describe('createMediaConnection', () => { iceServers: [], }, 'meeting id' - ); + ); }); }); @@ -232,7 +245,10 @@ describe('createMediaConnection', () => { [ {testCase: 'turnServerInfo is undefined', turnServerInfo: undefined}, - {testCase: 'turnServerInfo.url is empty string', turnServerInfo: {url: '', username: 'turn username', password: 'turn password'}}, + { + testCase: 'turnServerInfo.url is empty string', + turnServerInfo: {url: '', username: 'turn username', password: 'turn password'}, + }, ].forEach(({testCase, turnServerInfo}) => { it(`passes empty ICE servers array to RoapMediaConnection if ${testCase} (multistream disabled)`, () => { const roapMediaConnectionConstructorStub = sinon diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js index 24e352c815f..ea5fca28e99 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js @@ -625,7 +625,9 @@ describe('plugin-meetings', () => { beforeEach(() => { meeting.join = sinon.stub().returns(Promise.resolve(fakeJoinResult)); - addMediaInternalStub = sinon.stub(meeting, 'addMediaInternal').returns(Promise.resolve(test4)); + addMediaInternalStub = sinon + .stub(meeting, 'addMediaInternal') + .returns(Promise.resolve(test4)); webex.meetings.reachability.getReachabilityResults.resolves(fakeReachabilityResults); @@ -656,12 +658,21 @@ describe('plugin-meetings', () => { meeting, fakeJoinResult ); - assert.calledOnceWithExactly(meeting.addMediaInternal, sinon.match.any, fakeTurnServerInfo, false, mediaOptions); + assert.calledOnceWithExactly( + meeting.addMediaInternal, + sinon.match.any, + fakeTurnServerInfo, + false, + mediaOptions + ); assert.deepEqual(result, {join: fakeJoinResult, media: test4}); // resets joinWithMediaRetryInfo - assert.deepEqual(meeting.joinWithMediaRetryInfo, {isRetry: false, prevJoinResponse: undefined}); + assert.deepEqual(meeting.joinWithMediaRetryInfo, { + isRetry: false, + prevJoinResponse: undefined, + }); }); it("should not call handleTurnDiscoveryHttpResponse if we don't send a TURN discovery request with join", async () => { @@ -681,7 +692,13 @@ describe('plugin-meetings', () => { assert.calledOnceWithExactly(generateTurnDiscoveryRequestMessageStub, meeting, true); assert.notCalled(handleTurnDiscoveryHttpResponseStub); assert.notCalled(abortTurnDiscoveryStub); - assert.calledOnceWithExactly(meeting.addMediaInternal, sinon.match.any, undefined, false, mediaOptions); + assert.calledOnceWithExactly( + meeting.addMediaInternal, + sinon.match.any, + undefined, + false, + mediaOptions + ); assert.deepEqual(result, {join: fakeJoinResult, media: test4}); assert.equal(meeting.turnServerUsed, false); @@ -711,7 +728,13 @@ describe('plugin-meetings', () => { fakeJoinResult ); assert.calledOnceWithExactly(abortTurnDiscoveryStub); - assert.calledOnceWithExactly(meeting.addMediaInternal, sinon.match.any, undefined, false, mediaOptions); + assert.calledOnceWithExactly( + meeting.addMediaInternal, + sinon.match.any, + undefined, + false, + mediaOptions + ); assert.deepEqual(result, {join: fakeJoinResult, media: test4}); }); @@ -758,12 +781,20 @@ describe('plugin-meetings', () => { ); // resets joinWithMediaRetryInfo - assert.deepEqual(meeting.joinWithMediaRetryInfo, {isRetry: false, prevJoinResponse: undefined}); + assert.deepEqual(meeting.joinWithMediaRetryInfo, { + isRetry: false, + prevJoinResponse: undefined, + }); }); it('should resolve if join() fails the first time but succeeds the second time', async () => { const error = new Error('fake'); - meeting.join = sinon.stub().onFirstCall().returns(Promise.reject(error)).onSecondCall().returns(Promise.resolve(fakeJoinResult)); + meeting.join = sinon + .stub() + .onFirstCall() + .returns(Promise.reject(error)) + .onSecondCall() + .returns(Promise.resolve(fakeJoinResult)); const leaveStub = sinon.stub(meeting, 'leave').resolves(); const result = await meeting.joinWithMedia({ @@ -795,7 +826,10 @@ describe('plugin-meetings', () => { assert.deepEqual(result, {join: fakeJoinResult, media: test4}); // resets joinWithMediaRetryInfo - assert.deepEqual(meeting.joinWithMediaRetryInfo, {isRetry: false, prevJoinResponse: undefined}); + assert.deepEqual(meeting.joinWithMediaRetryInfo, { + isRetry: false, + prevJoinResponse: undefined, + }); }); it('should fail if called with allowMediaInLobby:false', async () => { @@ -828,7 +862,6 @@ describe('plugin-meetings', () => { reason: 'joinWithMedia failure', }); - // Behavioral metric is sent on both calls of joinWithMedia assert.calledTwice(Metrics.sendBehavioralMetric); assert.calledWith( @@ -1068,12 +1101,15 @@ describe('plugin-meetings', () => { const addMediaError = new Error('fake addMedia error'); addMediaError.name = 'SdpOfferCreationError'; - meeting.addMediaInternal.rejects(addMediaError) + meeting.addMediaInternal.rejects(addMediaError); - await assert.isRejected(meeting.joinWithMedia({ - joinOptions, - mediaOptions, - }), addMediaError); + await assert.isRejected( + meeting.joinWithMedia({ + joinOptions, + mediaOptions, + }), + addMediaError + ); // check that only 1 attempt was done assert.calledOnce(meeting.join); @@ -1339,10 +1375,7 @@ describe('plugin-meetings', () => { it('should trigger meeting:caption-received event', () => { meeting.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]({}); - assert.calledWith( - meeting.trigger, - EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED - ); + assert.calledWith(meeting.trigger, EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED); }); it('should trigger meeting:receiveTranscription:started event', () => { @@ -1355,10 +1388,7 @@ describe('plugin-meetings', () => { it('should trigger meeting:caption-received event', () => { meeting.voiceaListenerCallbacks[VOICEAEVENTS.NEW_CAPTION]({}); - assert.calledWith( - meeting.trigger, - EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED - ); + assert.calledWith(meeting.trigger, EVENT_TRIGGERS.MEETING_CAPTION_RECEIVED); }); }); @@ -1513,11 +1543,7 @@ describe('plugin-meetings', () => { it('turns off llm online, emits transcription connected events', () => { meeting.handleLLMOnline(); - assert.calledOnceWithExactly( - webex.internal.llm.off, - 'online', - meeting.handleLLMOnline - ); + assert.calledOnceWithExactly(webex.internal.llm.off, 'online', meeting.handleLLMOnline); assert.calledWith( TriggerProxy.trigger, sinon.match.instanceOf(Meeting), @@ -1579,11 +1605,7 @@ describe('plugin-meetings', () => { assert.calledOnce(MeetingUtil.joinMeeting); assert.calledOnce(meeting.setLocus); assert.equal(result, joinMeetingResult); - assert.calledWith( - webex.internal.llm.on, - 'online', - meeting.handleLLMOnline - ); + assert.calledWith(webex.internal.llm.on, 'online', meeting.handleLLMOnline); }); [true, false].forEach((enableMultistream) => { @@ -1906,7 +1928,9 @@ describe('plugin-meetings', () => { }; meeting.mediaProperties.setMediaDirection = sinon.stub().returns(true); meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves(); - meeting.mediaProperties.getCurrentConnectionInfo = sinon.stub().resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1}); + meeting.mediaProperties.getCurrentConnectionInfo = sinon + .stub() + .resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1}); meeting.audio = muteStateStub; meeting.video = muteStateStub; sinon.stub(Media, 'createMediaConnection').returns(fakeMediaConnection); @@ -1920,7 +1944,12 @@ describe('plugin-meetings', () => { // normally the first Roap message we send is creating confluence, so mock LocusMediaRequest.isConfluenceCreated() // to return false the first time it's called and true the 2nd time, to simulate how it would happen for real meeting.locusMediaRequest = { - isConfluenceCreated: sinon.stub().onFirstCall().returns(false).onSecondCall().returns(true) + isConfluenceCreated: sinon + .stub() + .onFirstCall() + .returns(false) + .onSecondCall() + .returns(true), }; }); @@ -2485,7 +2514,6 @@ describe('plugin-meetings', () => { it('should reject if waitForMediaConnectionConnected() rejects after turn server retry', async () => { const FAKE_ERROR = {fatal: true}; const getErrorPayloadForClientErrorCodeStub = - (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode = sinon.stub().returns(FAKE_ERROR)); webex.meetings.reachability = { @@ -2690,9 +2718,16 @@ describe('plugin-meetings', () => { it('should resolve if waitForMediaConnectionConnected() rejects the first time but resolves the second time', async () => { const FAKE_ERROR = {fatal: true}; webex.meetings.reachability = { - isWebexMediaBackendUnreachable: sinon.stub().onCall(0).rejects().onCall(1).resolves(true).onCall(2).resolves(false), + isWebexMediaBackendUnreachable: sinon + .stub() + .onCall(0) + .rejects() + .onCall(1) + .resolves(true) + .onCall(2) + .resolves(false), getReachabilityMetrics: sinon.stub().resolves({}), - } + }; const getErrorPayloadForClientErrorCodeStub = (webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode = sinon.stub().returns(FAKE_ERROR)); @@ -3445,9 +3480,9 @@ describe('plugin-meetings', () => { .returns(clientErrorCode); meeting.meetingState = 'ACTIVE'; - meeting.mediaProperties.waitForMediaConnectionConnected.rejects( - {iceConnected: false} - ); + meeting.mediaProperties.waitForMediaConnectionConnected.rejects({ + iceConnected: false, + }); let errorThrown = false; @@ -3562,12 +3597,18 @@ describe('plugin-meetings', () => { meeting.meetingState = 'ACTIVE'; meeting.selfUrl = 'selfUrl'; meeting.mediaProperties.waitForMediaConnectionConnected = sinon.stub().resolves(); - meeting.mediaProperties.getCurrentConnectionInfo = sinon.stub().resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1}); + meeting.mediaProperties.getCurrentConnectionInfo = sinon + .stub() + .resolves({connectionType: 'udp', selectedCandidatePairChanges: 2, numTransports: 1}); meeting.setMercuryListener = sinon.stub(); meeting.locusInfo.onFullLocus = sinon.stub(); meeting.webex.meetings.geoHintInfo = {regionCode: 'EU', countryCode: 'UK'}; meeting.roap.doTurnDiscovery = sinon.stub().resolves({ - turnServerInfo: {url: 'turn-url', username: 'turn user', password: 'turn password'}, + turnServerInfo: { + url: 'turns:turn-server-url:443?transport=tcp', + username: 'turn user', + password: 'turn password', + }, turnDiscoverySkippedReason: 'reachability', }); meeting.deferSDPAnswer = new Defer(); @@ -3580,7 +3621,18 @@ describe('plugin-meetings', () => { // setup things that are expected to be the same across all the tests and are actually irrelevant for these tests expectedDebugId = `MC-${meeting.id.substring(0, 4)}`; expectedMediaConnectionConfig = { - iceServers: [{urls: 'turn-url', username: 'turn user', credential: 'turn password'}], + iceServers: [ + { + urls: 'turn:turn-server-url:5004?transport=tcp', + username: 'turn user', + credential: 'turn password', + }, + { + urls: 'turns:turn-server-url:443?transport=tcp', + username: 'turn user', + credential: 'turn password', + }, + ], skipInactiveTransceivers: false, requireH264: true, sdpMunging: { @@ -3665,13 +3717,11 @@ describe('plugin-meetings', () => { // that's being tested in these tests) meeting.webex.meetings.registered = true; meeting.webex.internal.device.config = {}; - sinon - .stub(MeetingUtil, 'joinMeeting') - .resolves({ - id: 'fake locus from mocked join request', - locusUrl: 'fake locus url', - mediaId: 'fake media id', - }); + sinon.stub(MeetingUtil, 'joinMeeting').resolves({ + id: 'fake locus from mocked join request', + locusUrl: 'fake locus url', + mediaId: 'fake media id', + }); await meeting.join({enableMultistream: isMultistream}); }); @@ -3701,7 +3751,8 @@ describe('plugin-meetings', () => { for (let idx = 0; idx < roapMediaConnectionToCheck.on.callCount; idx += 1) { if ( - roapMediaConnectionToCheck.on.getCall(idx).args[0] === MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND + roapMediaConnectionToCheck.on.getCall(idx).args[0] === + MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND ) { return roapMediaConnectionToCheck.on.getCall(idx).args[1]; } @@ -4148,13 +4199,13 @@ describe('plugin-meetings', () => { await meeting.addMedia({ localStreams: {microphone: fakeMicrophoneStream}, audioEnabled: false, - videoEnabled: false + videoEnabled: false, }); await simulateRoapOffer(); await simulateRoapOk(); assert.notCalled(handleDeviceLoggingSpy); - }) + }); it('addMedia() works correctly when media is disabled with no streams to publish', async () => { await meeting.addMedia({audioEnabled: false}); @@ -6589,14 +6640,14 @@ describe('plugin-meetings', () => { beforeEach(() => { sandbox = sinon.createSandbox(); meeting.statsAnalyzer = { - stopAnalyzer: sinon.stub().returns(Promise.resolve()) + stopAnalyzer: sinon.stub().returns(Promise.resolve()), }; meeting.reconnectionManager = { - cleanUp: sinon.stub() + cleanUp: sinon.stub(), }; - meeting.cleanupLocalStreams=sinon.stub(); + meeting.cleanupLocalStreams = sinon.stub(); meeting.closeRemoteStreams = sinon.stub().returns(Promise.resolve()); meeting.closePeerConnections = sinon.stub().returns(Promise.resolve()); meeting.unsetRemoteStreams = sinon.stub(); @@ -6678,7 +6729,6 @@ describe('plugin-meetings', () => { 'SELF_OBSERVING' ); - // Verify that the event handler behaves as expected expect(meeting.statsAnalyzer.stopAnalyzer.calledOnce).to.be.true; expect(meeting.closeRemoteStreams.calledOnce).to.be.true; @@ -6690,11 +6740,13 @@ describe('plugin-meetings', () => { expect(meeting.unsetPeerConnections.calledOnce).to.be.true; expect(meeting.reconnectionManager.cleanUp.calledOnce).to.be.true; expect(meeting.mediaProperties.setMediaDirection.calledOnce).to.be.true; - expect(meeting.addMedia.calledOnceWithExactly({ - audioEnabled: false, - videoEnabled: false, - shareVideoEnabled: true - })).to.be.true; + expect( + meeting.addMedia.calledOnceWithExactly({ + audioEnabled: false, + videoEnabled: false, + shareVideoEnabled: true, + }) + ).to.be.true; await testUtils.flushPromises(); assert.equal(meeting.isMoveToInProgress, false); }); @@ -7482,9 +7534,11 @@ describe('plugin-meetings', () => { getTracks: () => [{id: 'track', addEventListener: sinon.stub()}], }; const simulateConnectionStateChange = (newState) => { - meeting.mediaProperties.webrtcMediaConnection.getConnectionState = sinon.stub().returns(newState); + meeting.mediaProperties.webrtcMediaConnection.getConnectionState = sinon + .stub() + .returns(newState); eventListeners[MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED](); - } + }; beforeEach(() => { eventListeners = {}; @@ -7506,7 +7560,9 @@ describe('plugin-meetings', () => { assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_FAILURE]); assert.isFunction(eventListeners[MediaConnectionEventNames.ROAP_MESSAGE_TO_SEND]); assert.isFunction(eventListeners[MediaConnectionEventNames.REMOTE_TRACK_ADDED]); - assert.isFunction(eventListeners[MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED]); + assert.isFunction( + eventListeners[MediaConnectionEventNames.PEER_CONNECTION_STATE_CHANGED] + ); assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CONNECTION_STATE_CHANGED]); assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CANDIDATE]); assert.isFunction(eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]); @@ -7569,28 +7625,44 @@ describe('plugin-meetings', () => { }); it('should not collect skipped ice candidates error', () => { - eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({error: { errorCode: 600, errorText: 'Address not associated with the desired network interface.' }}); + eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({ + error: { + errorCode: 600, + errorText: 'Address not associated with the desired network interface.', + }, + }); assert.equal(meeting.iceCandidateErrors.size, 0); }); it('should collect valid ice candidates error', () => { - eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: '' }}); + eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({ + error: {errorCode: 701, errorText: ''}, + }); assert.equal(meeting.iceCandidateErrors.size, 1); assert.equal(meeting.iceCandidateErrors.has('701_'), true); }); it('should increment counter if same valid ice candidates error collected', () => { - eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: '' }}); + eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({ + error: {errorCode: 701, errorText: ''}, + }); - eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: 'STUN host lookup received error.' }}); - eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({error: { errorCode: 701, errorText: 'STUN host lookup received error.' }}); + eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({ + error: {errorCode: 701, errorText: 'STUN host lookup received error.'}, + }); + eventListeners[MediaConnectionEventNames.ICE_CANDIDATE_ERROR]({ + error: {errorCode: 701, errorText: 'STUN host lookup received error.'}, + }); assert.equal(meeting.iceCandidateErrors.size, 2); assert.equal(meeting.iceCandidateErrors.has('701_'), true); assert.equal(meeting.iceCandidateErrors.get('701_'), 1); - assert.equal(meeting.iceCandidateErrors.has('701_stun_host_lookup_received_error'), true); + assert.equal( + meeting.iceCandidateErrors.has('701_stun_host_lookup_received_error'), + true + ); assert.equal(meeting.iceCandidateErrors.get('701_stun_host_lookup_received_error'), 2); }); }); @@ -8263,8 +8335,12 @@ describe('plugin-meetings', () => { }); it('registers for audio and video source count changed', () => { - assert.isFunction(eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED]); - assert.isFunction(eventListeners[MediaConnectionEventNames.AUDIO_SOURCES_COUNT_CHANGED]); + assert.isFunction( + eventListeners[MediaConnectionEventNames.VIDEO_SOURCES_COUNT_CHANGED] + ); + assert.isFunction( + eventListeners[MediaConnectionEventNames.AUDIO_SOURCES_COUNT_CHANGED] + ); }); it('forwards the VIDEO_SOURCES_COUNT_CHANGED event as "media:remoteVideoSourceCountChanged"', () => { From d5da303ff866c0fa3769f092696277537e82ca8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Wa=C5=9Bniowski?= Date: Fri, 6 Sep 2024 16:52:07 +0200 Subject: [PATCH 05/48] fix: update internal media core (#3814) Co-authored-by: kwasniow --- packages/@webex/media-helpers/package.json | 2 +- packages/@webex/plugin-meetings/package.json | 2 +- packages/calling/package.json | 2 +- yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/@webex/media-helpers/package.json b/packages/@webex/media-helpers/package.json index 512e8e6fe42..f0970bfb4d9 100644 --- a/packages/@webex/media-helpers/package.json +++ b/packages/@webex/media-helpers/package.json @@ -22,7 +22,7 @@ "deploy:npm": "yarn npm publish" }, "dependencies": { - "@webex/internal-media-core": "2.9.2", + "@webex/internal-media-core": "2.9.3", "@webex/ts-events": "^1.1.0", "@webex/web-media-effects": "2.18.0" }, diff --git a/packages/@webex/plugin-meetings/package.json b/packages/@webex/plugin-meetings/package.json index 31a709e3185..cabb064b969 100644 --- a/packages/@webex/plugin-meetings/package.json +++ b/packages/@webex/plugin-meetings/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@webex/common": "workspace:*", - "@webex/internal-media-core": "2.9.2", + "@webex/internal-media-core": "2.9.3", "@webex/internal-plugin-conversation": "workspace:*", "@webex/internal-plugin-device": "workspace:*", "@webex/internal-plugin-llm": "workspace:*", diff --git a/packages/calling/package.json b/packages/calling/package.json index 9ab50e04dff..64df00bfc0b 100644 --- a/packages/calling/package.json +++ b/packages/calling/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "@types/platform": "1.3.4", - "@webex/internal-media-core": "2.9.2", + "@webex/internal-media-core": "2.9.3", "@webex/media-helpers": "workspace:*", "async-mutex": "0.4.0", "buffer": "6.0.3", diff --git a/yarn.lock b/yarn.lock index f789ccf87b5..1e007670e5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7419,7 +7419,7 @@ __metadata: "@typescript-eslint/eslint-plugin": 5.38.1 "@typescript-eslint/parser": 5.38.1 "@web/dev-server": 0.4.5 - "@webex/internal-media-core": 2.9.2 + "@webex/internal-media-core": 2.9.3 "@webex/media-helpers": "workspace:*" async-mutex: 0.4.0 buffer: 6.0.3 @@ -7710,9 +7710,9 @@ __metadata: languageName: unknown linkType: soft -"@webex/internal-media-core@npm:2.9.2": - version: 2.9.2 - resolution: "@webex/internal-media-core@npm:2.9.2" +"@webex/internal-media-core@npm:2.9.3": + version: 2.9.3 + resolution: "@webex/internal-media-core@npm:2.9.3" dependencies: "@babel/runtime": ^7.18.9 "@babel/runtime-corejs2": ^7.25.0 @@ -7724,7 +7724,7 @@ __metadata: uuid: ^8.3.2 webrtc-adapter: ^8.1.2 xstate: ^4.30.6 - checksum: 429ccc2a73ae226fb721f6fcd72f50653ded0fd54f8682542321d2b3dd7504a79d46c8746e2abf24d83c17401c0302c82bde4c73d232036afbbbcb544fd6639e + checksum: 568e54be4afa7f5a93a843f927c5af12828b781deebeb651f89e7f4639f149b7b1e1642a9f3c75d8d20a065838d170853f3ea90a0f57aed0601481d584e321b6 languageName: node linkType: hard @@ -8483,7 +8483,7 @@ __metadata: "@babel/preset-typescript": 7.22.11 "@webex/babel-config-legacy": "workspace:*" "@webex/eslint-config-legacy": "workspace:*" - "@webex/internal-media-core": 2.9.2 + "@webex/internal-media-core": 2.9.3 "@webex/jest-config-legacy": "workspace:*" "@webex/legacy-tools": "workspace:*" "@webex/test-helper-chai": "workspace:*" @@ -8719,7 +8719,7 @@ __metadata: "@webex/babel-config-legacy": "workspace:*" "@webex/common": "workspace:*" "@webex/eslint-config-legacy": "workspace:*" - "@webex/internal-media-core": 2.9.2 + "@webex/internal-media-core": 2.9.3 "@webex/internal-plugin-conversation": "workspace:*" "@webex/internal-plugin-device": "workspace:*" "@webex/internal-plugin-llm": "workspace:*" From 7e6755b407b5d08a41916f8f2f263ee6802ec6a4 Mon Sep 17 00:00:00 2001 From: Adhwaith Menon <111346225+adhmenon@users.noreply.github.com> Date: Mon, 9 Sep 2024 14:15:55 +0530 Subject: [PATCH 06/48] feat:(munge-sdp): updated-sdk-with-latest-media-core (#3815) --- packages/@webex/media-helpers/package.json | 2 +- packages/@webex/plugin-meetings/package.json | 2 +- packages/calling/package.json | 2 +- .../src/CallingClient/calling/call.test.ts | 27 +------------------ .../calling/src/CallingClient/calling/call.ts | 23 +--------------- yarn.lock | 14 +++++----- 6 files changed, 12 insertions(+), 58 deletions(-) diff --git a/packages/@webex/media-helpers/package.json b/packages/@webex/media-helpers/package.json index f0970bfb4d9..2c8eae9c839 100644 --- a/packages/@webex/media-helpers/package.json +++ b/packages/@webex/media-helpers/package.json @@ -22,7 +22,7 @@ "deploy:npm": "yarn npm publish" }, "dependencies": { - "@webex/internal-media-core": "2.9.3", + "@webex/internal-media-core": "2.10.0", "@webex/ts-events": "^1.1.0", "@webex/web-media-effects": "2.18.0" }, diff --git a/packages/@webex/plugin-meetings/package.json b/packages/@webex/plugin-meetings/package.json index cabb064b969..e8f6d152c15 100644 --- a/packages/@webex/plugin-meetings/package.json +++ b/packages/@webex/plugin-meetings/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@webex/common": "workspace:*", - "@webex/internal-media-core": "2.9.3", + "@webex/internal-media-core": "2.10.0", "@webex/internal-plugin-conversation": "workspace:*", "@webex/internal-plugin-device": "workspace:*", "@webex/internal-plugin-llm": "workspace:*", diff --git a/packages/calling/package.json b/packages/calling/package.json index 64df00bfc0b..3638a90ad9b 100644 --- a/packages/calling/package.json +++ b/packages/calling/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "@types/platform": "1.3.4", - "@webex/internal-media-core": "2.9.3", + "@webex/internal-media-core": "2.10.0", "@webex/media-helpers": "workspace:*", "async-mutex": "0.4.0", "buffer": "6.0.3", diff --git a/packages/calling/src/CallingClient/calling/call.test.ts b/packages/calling/src/CallingClient/calling/call.test.ts index ce43512f806..b15c416981f 100644 --- a/packages/calling/src/CallingClient/calling/call.test.ts +++ b/packages/calling/src/CallingClient/calling/call.test.ts @@ -109,6 +109,7 @@ describe('Call Tests', () => { sdpMunging: { convertPort9to0: true, addContentSlides: false, + copyClineToSessionLevel: true, }, }; @@ -717,32 +718,6 @@ describe('Call Tests', () => { {file: 'call', method: 'updateMedia'} ); }); - - describe('#addSessionConnection', () => { - let call; - - beforeEach(() => { - call = callManager.createCall(dest, CallDirection.INBOUND, deviceId, mockLineId); - }); - - it('should copy the c-line from media level to the session level', () => { - const sdp = `v=0\r\no=- 2890844526 2890842807 IN IP4 192.0.2.3\r\ns=-\r\nt=0 0\r\nm=audio 49170 RTP/AVP 0\r\nc=IN IP4 203.0.113.1\r\na=rtpmap:0 PCMU/8000`; - - const expectedSdp = `v=0\r\no=- 2890844526 2890842807 IN IP4 192.0.2.3\r\ns=-\r\nc=IN IP4 203.0.113.1\r\nt=0 0\r\nm=audio 49170 RTP/AVP 0\r\nc=IN IP4 203.0.113.1\r\na=rtpmap:0 PCMU/8000`; - - const result = call.addSessionConnection(sdp); - expect(result).toBe(expectedSdp); - }); - - it('should handle multiple media sections correctly', () => { - const sdp = `v=0\r\no=- 2890844526 2890842807 IN IP4 192.0.2.3\r\ns=-\r\nt=0 0\r\nm=audio 49170 RTP/AVP 0\r\nc=IN IP4 203.0.113.1\r\na=rtpmap:0 PCMU/8000\r\nm=video 51372 RTP/AVP 31\r\nc=IN IP4 203.0.113.2\r\na=rtpmap:31 H261/90000`; - - const expectedSdp = `v=0\r\no=- 2890844526 2890842807 IN IP4 192.0.2.3\r\ns=-\r\nc=IN IP4 203.0.113.1\r\nt=0 0\r\nm=audio 49170 RTP/AVP 0\r\nc=IN IP4 203.0.113.1\r\na=rtpmap:0 PCMU/8000\r\nm=video 51372 RTP/AVP 31\r\nc=IN IP4 203.0.113.2\r\na=rtpmap:31 H261/90000`; - - const result = call.addSessionConnection(sdp); - expect(result).toBe(expectedSdp); - }); - }); }); describe('State Machine handler tests', () => { diff --git a/packages/calling/src/CallingClient/calling/call.ts b/packages/calling/src/CallingClient/calling/call.ts index 9fc68949cba..fd7e64718e1 100644 --- a/packages/calling/src/CallingClient/calling/call.ts +++ b/packages/calling/src/CallingClient/calling/call.ts @@ -1904,6 +1904,7 @@ export class Call extends Eventing implements ICall { sdpMunging: { convertPort9to0: true, addContentSlides: false, + copyClineToSessionLevel: true, }, }, { @@ -2375,24 +2376,6 @@ export class Call extends Eventing implements ICall { }); } - /* istanbul ignore next */ - /** - * Copy SDP's c-line to session level from media level. - * SPARK-522437 - */ - private addSessionConnection(sdp: string): string { - const lines: string[] = sdp.split(/\r\n|\r|\n/); - const mIndex: number = lines.findIndex((line) => line.startsWith('m=')); - const tIndex: number = lines.findIndex((line) => line.startsWith('t=')); - - if (mIndex !== -1 && mIndex < lines.length - 1 && lines[mIndex + 1].startsWith('c=')) { - const cLine: string = lines[mIndex + 1]; - lines.splice(tIndex, 0, cLine); - } - - return lines.join('\r\n'); - } - /* istanbul ignore next */ /** * Setup a listener for roap events emitted by the media sdk. @@ -2413,10 +2396,6 @@ export class Call extends Eventing implements ICall { method: this.mediaRoapEventsListener.name, }); - if (event.roapMessage?.sdp) { - event.roapMessage.sdp = this.addSessionConnection(event.roapMessage.sdp); - } - switch (event.roapMessage.messageType) { case RoapScenario.OK: { const mediaOk = { diff --git a/yarn.lock b/yarn.lock index 1e007670e5f..01fd898c89c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7419,7 +7419,7 @@ __metadata: "@typescript-eslint/eslint-plugin": 5.38.1 "@typescript-eslint/parser": 5.38.1 "@web/dev-server": 0.4.5 - "@webex/internal-media-core": 2.9.3 + "@webex/internal-media-core": 2.10.0 "@webex/media-helpers": "workspace:*" async-mutex: 0.4.0 buffer: 6.0.3 @@ -7710,9 +7710,9 @@ __metadata: languageName: unknown linkType: soft -"@webex/internal-media-core@npm:2.9.3": - version: 2.9.3 - resolution: "@webex/internal-media-core@npm:2.9.3" +"@webex/internal-media-core@npm:2.10.0": + version: 2.10.0 + resolution: "@webex/internal-media-core@npm:2.10.0" dependencies: "@babel/runtime": ^7.18.9 "@babel/runtime-corejs2": ^7.25.0 @@ -7724,7 +7724,7 @@ __metadata: uuid: ^8.3.2 webrtc-adapter: ^8.1.2 xstate: ^4.30.6 - checksum: 568e54be4afa7f5a93a843f927c5af12828b781deebeb651f89e7f4639f149b7b1e1642a9f3c75d8d20a065838d170853f3ea90a0f57aed0601481d584e321b6 + checksum: 4191b75de30a22c131dba7cbc4922cf5da52ad799ab059391539355f50dcdf8aa02fb2fd738a5bbf4938a54bc869e00ba7f74fdd58e04f231825665fd7c05ce2 languageName: node linkType: hard @@ -8483,7 +8483,7 @@ __metadata: "@babel/preset-typescript": 7.22.11 "@webex/babel-config-legacy": "workspace:*" "@webex/eslint-config-legacy": "workspace:*" - "@webex/internal-media-core": 2.9.3 + "@webex/internal-media-core": 2.10.0 "@webex/jest-config-legacy": "workspace:*" "@webex/legacy-tools": "workspace:*" "@webex/test-helper-chai": "workspace:*" @@ -8719,7 +8719,7 @@ __metadata: "@webex/babel-config-legacy": "workspace:*" "@webex/common": "workspace:*" "@webex/eslint-config-legacy": "workspace:*" - "@webex/internal-media-core": 2.9.3 + "@webex/internal-media-core": 2.10.0 "@webex/internal-plugin-conversation": "workspace:*" "@webex/internal-plugin-device": "workspace:*" "@webex/internal-plugin-llm": "workspace:*" From 77ac358eb87bc52fe4a228e18650fb94cdf88e07 Mon Sep 17 00:00:00 2001 From: Coread Date: Mon, 9 Sep 2024 16:14:35 +0100 Subject: [PATCH 07/48] fix(plugin-authorization): use orgId if emailhash unavailable (#3788) --- .../src/authorization.js | 28 +++++++- .../test/unit/spec/authorization.js | 65 ++++++++++++++++++- 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/packages/@webex/plugin-authorization-browser-first-party/src/authorization.js b/packages/@webex/plugin-authorization-browser-first-party/src/authorization.js index 916287f34d8..477b64e751d 100644 --- a/packages/@webex/plugin-authorization-browser-first-party/src/authorization.js +++ b/packages/@webex/plugin-authorization-browser-first-party/src/authorization.js @@ -103,14 +103,24 @@ const Authorization = WebexPlugin.extend({ this._verifySecurityToken(location.query); this._cleanUrl(location); + let preauthCatalogParams; + + const orgId = this._extractOrgIdFromCode(code); + + if (emailhash) { + preauthCatalogParams = {emailhash}; + } else if (orgId) { + preauthCatalogParams = {orgId}; + } + // Wait until nextTick in case `credentials` hasn't initialized yet process.nextTick(() => { this.webex.internal.services - .collectPreauthCatalog(emailhash ? {emailhash}: undefined) + .collectPreauthCatalog(preauthCatalogParams) .catch(() => Promise.resolve()) .then(() => this.requestAuthorizationCodeGrant({code, codeVerifier})) .catch((error) => { - this.logger.warn('authorization: failed initial authorization code grant request', error) + this.logger.warn('authorization: failed initial authorization code grant request', error); }) .then(() => { this.ready = true; @@ -230,6 +240,20 @@ const Authorization = WebexPlugin.extend({ }); }, + /** + * Extracts the orgId from the returned code from idbroker + * Description of how to parse the code can be found here: + * https://wiki.cisco.com/display/IDENTITY/Federated+Token+Validation + * @instance + * @memberof AuthorizationBrowserFirstParty + * @param {String} code + * @private + * @returns {String} + */ + _extractOrgIdFromCode(code) { + return code?.split('_')[2] || undefined; + }, + /** * Checks if the result of the login redirect contains an error string * @instance diff --git a/packages/@webex/plugin-authorization-browser-first-party/test/unit/spec/authorization.js b/packages/@webex/plugin-authorization-browser-first-party/test/unit/spec/authorization.js index fbc07485412..6069b45ac15 100644 --- a/packages/@webex/plugin-authorization-browser-first-party/test/unit/spec/authorization.js +++ b/packages/@webex/plugin-authorization-browser-first-party/test/unit/spec/authorization.js @@ -183,7 +183,7 @@ describe('plugin-authorization-browser-first-party', () => { }); it('collects the preauth catalog when emailhash is present in the state', async () => { - const code = 'auth code'; + const code = 'authcode_clusterid_theOrgId'; const webex = makeWebex( `http://example.com/?code=${code}&state=${base64.encode( JSON.stringify({emailhash: 'someemailhash'}) @@ -205,7 +205,7 @@ describe('plugin-authorization-browser-first-party', () => { }); it('collects the preauth catalog no emailhash is present in the state', async () => { - const code = 'auth code'; + const code = 'authcode_clusterid_theOrgId'; const webex = makeWebex( `http://example.com/?code=${code}` ); @@ -220,6 +220,26 @@ describe('plugin-authorization-browser-first-party', () => { await webex.authorization.when('change:ready'); + assert.calledOnce(requestAuthorizationCodeGrantStub); + assert.calledWith(requestAuthorizationCodeGrantStub, {code, codeVerifier: undefined}); + assert.calledOnce(collectPreauthCatalogStub); + assert.calledWith(collectPreauthCatalogStub, {orgId: 'theOrgId'}); + }); + + it('collects the preauth catalog with no emailhash and no orgId', async () => { + const code = 'authcode_clusterid'; + const webex = makeWebex(`http://example.com/?code=${code}`); + + const requestAuthorizationCodeGrantStub = sinon.stub( + Authorization.prototype, + 'requestAuthorizationCodeGrant' + ); + const collectPreauthCatalogStub = sinon + .stub(Services.prototype, 'collectPreauthCatalog') + .resolves(); + + await webex.authorization.when('change:ready'); + assert.calledOnce(requestAuthorizationCodeGrantStub); assert.calledWith(requestAuthorizationCodeGrantStub, {code, codeVerifier: undefined}); assert.calledOnce(collectPreauthCatalogStub); @@ -503,5 +523,46 @@ describe('plugin-authorization-browser-first-party', () => { assert.notInclude(href, 'csrf_token'); }); }); + + describe('#_extractOrgIdFromCode', () => { + it('extracts the orgId from the code', () => { + const webex = makeWebex(undefined, undefined, { + credentials: { + clientType: 'confidential', + }, + }); + + const code = 'authcode_clusterid_theOrgId'; + const orgId = webex.authorization._extractOrgIdFromCode(code); + + assert.equal(orgId, 'theOrgId'); + }); + + it('handles an invalid code', () => { + const webex = makeWebex(undefined, undefined, { + credentials: { + clientType: 'confidential', + }, + }); + + const code = 'authcode_clusterid_'; + const orgId = webex.authorization._extractOrgIdFromCode(code); + + assert.isUndefined(orgId); + }); + + it('handles an completely invalid code', () => { + const webex = makeWebex(undefined, undefined, { + credentials: { + clientType: 'confidential', + }, + }); + + const code = 'authcode'; + const orgId = webex.authorization._extractOrgIdFromCode(code); + + assert.isUndefined(orgId); + }) + }); }); }); From 6a34e63a2111b75b152448ac789eba0cd1f4c486 Mon Sep 17 00:00:00 2001 From: Coread Date: Tue, 10 Sep 2024 13:36:08 +0100 Subject: [PATCH 08/48] feat(webex-core): hostmap interceptor (#3789) --- packages/@webex/webex-core/src/index.js | 1 + .../webex-core/src/lib/services/index.js | 1 + .../src/lib/services/interceptors/hostmap.js | 36 +++++++++ .../webex-core/src/lib/services/services.js | 27 +++++++ packages/@webex/webex-core/src/webex-core.js | 2 + .../spec/services/interceptors/hostmap.js | 79 +++++++++++++++++++ .../test/unit/spec/services/services.js | 55 +++++++++++++ 7 files changed, 201 insertions(+) create mode 100644 packages/@webex/webex-core/src/lib/services/interceptors/hostmap.js create mode 100644 packages/@webex/webex-core/test/unit/spec/services/interceptors/hostmap.js diff --git a/packages/@webex/webex-core/src/index.js b/packages/@webex/webex-core/src/index.js index a3da486372a..7d4c1b9f6bc 100644 --- a/packages/@webex/webex-core/src/index.js +++ b/packages/@webex/webex-core/src/index.js @@ -25,6 +25,7 @@ export { Services, ServiceHost, ServiceUrl, + HostMapInterceptor, } from './lib/services'; export { diff --git a/packages/@webex/webex-core/src/lib/services/index.js b/packages/@webex/webex-core/src/lib/services/index.js index d18760f11c8..8f73c306e7e 100644 --- a/packages/@webex/webex-core/src/lib/services/index.js +++ b/packages/@webex/webex-core/src/lib/services/index.js @@ -18,6 +18,7 @@ registerInternalPlugin('services', Services, { export {constants}; export {default as ServiceInterceptor} from './interceptors/service'; export {default as ServerErrorInterceptor} from './interceptors/server-error'; +export {default as HostMapInterceptor} from './interceptors/hostmap'; export {default as Services} from './services'; export {default as ServiceCatalog} from './service-catalog'; export {default as ServiceRegistry} from './service-registry'; diff --git a/packages/@webex/webex-core/src/lib/services/interceptors/hostmap.js b/packages/@webex/webex-core/src/lib/services/interceptors/hostmap.js new file mode 100644 index 00000000000..0cf4370b7cb --- /dev/null +++ b/packages/@webex/webex-core/src/lib/services/interceptors/hostmap.js @@ -0,0 +1,36 @@ +/*! + * Copyright (c) 2015-2024 Cisco Systems, Inc. See LICENSE file. + */ + +import {Interceptor} from '@webex/http-core'; + +/** + * This interceptor replaces the host in the request uri with the host from the hostmap + * It will attempt to do this for every request, but not all URIs will be in the hostmap + * URIs with hosts that are not in the hostmap will be left unchanged + */ +export default class HostMapInterceptor extends Interceptor { + /** + * @returns {HostMapInterceptor} + */ + static create() { + return new HostMapInterceptor({webex: this}); + } + + /** + * @see Interceptor#onRequest + * @param {Object} options + * @returns {Object} + */ + onRequest(options) { + if (options.uri) { + try { + options.uri = this.webex.internal.services.replaceHostFromHostmap(options.uri); + } catch (error) { + /* empty */ + } + } + + return options; + } +} diff --git a/packages/@webex/webex-core/src/lib/services/services.js b/packages/@webex/webex-core/src/lib/services/services.js index cbc2972bc6f..bc3dc67e4c6 100644 --- a/packages/@webex/webex-core/src/lib/services/services.js +++ b/packages/@webex/webex-core/src/lib/services/services.js @@ -675,6 +675,33 @@ const Services = WebexPlugin.extend({ }); }, + /** + * Looks up the hostname in the host catalog + * and replaces it with the first host if it finds it + * @param {string} uri + * @returns {string} uri with the host replaced + */ + replaceHostFromHostmap(uri) { + const url = new URL(uri); + const hostCatalog = this._hostCatalog; + + if (!hostCatalog) { + return uri; + } + + const host = hostCatalog[url.host]; + + if (host && host[0]) { + const newHost = host[0].host; + + url.host = newHost; + + return url.toString(); + } + + return uri; + }, + /** * @private * Organize a received hostmap from a service diff --git a/packages/@webex/webex-core/src/webex-core.js b/packages/@webex/webex-core/src/webex-core.js index c9f33cdeda6..a8c52cf56ca 100644 --- a/packages/@webex/webex-core/src/webex-core.js +++ b/packages/@webex/webex-core/src/webex-core.js @@ -31,6 +31,7 @@ import WebexUserAgentInterceptor from './interceptors/webex-user-agent'; import RateLimitInterceptor from './interceptors/rate-limit'; import EmbargoInterceptor from './interceptors/embargo'; import DefaultOptionsInterceptor from './interceptors/default-options'; +import HostMapInterceptor from './lib/services/interceptors/hostmap'; import config from './config'; import {makeWebexStore} from './lib/storage'; import mixinWebexCorePlugins from './lib/webex-core-plugin-mixin'; @@ -71,6 +72,7 @@ const interceptors = { NetworkTimingInterceptor: NetworkTimingInterceptor.create, EmbargoInterceptor: EmbargoInterceptor.create, DefaultOptionsInterceptor: DefaultOptionsInterceptor.create, + HostMapInterceptor: HostMapInterceptor.create, }; const preInterceptors = [ diff --git a/packages/@webex/webex-core/test/unit/spec/services/interceptors/hostmap.js b/packages/@webex/webex-core/test/unit/spec/services/interceptors/hostmap.js new file mode 100644 index 00000000000..46ff05fd178 --- /dev/null +++ b/packages/@webex/webex-core/test/unit/spec/services/interceptors/hostmap.js @@ -0,0 +1,79 @@ +/*! + * Copyright (c) 2015-2024 Cisco Systems, Inc. See LICENSE file. + */ + +/* eslint-disable camelcase */ + +import sinon from 'sinon'; +import {assert} from '@webex/test-helper-chai'; +import MockWebex from '@webex/test-helper-mock-webex'; +import {HostMapInterceptor, config, Credentials} from '@webex/webex-core'; +import {cloneDeep} from 'lodash'; + +describe('webex-core', () => { + describe('Interceptors', () => { + describe('HostMapInterceptor', () => { + let interceptor, webex; + + beforeEach(() => { + webex = new MockWebex({ + children: { + credentials: Credentials, + }, + config: cloneDeep(config), + request: sinon.spy(), + }); + + webex.internal.services = { + replaceHostFromHostmap: sinon.stub().returns('http://replaceduri.com'), + } + + interceptor = Reflect.apply(HostMapInterceptor.create, webex, []); + }); + + describe('#onRequest', () => { + it('calls replaceHostFromHostmap if options.uri is defined', () => { + const options = { + uri: 'http://example.com', + }; + + interceptor.onRequest(options); + + sinon.assert.calledWith( + webex.internal.services.replaceHostFromHostmap, + 'http://example.com' + ); + + assert.equal(options.uri, 'http://replaceduri.com'); + }); + + it('does not call replaceHostFromHostmap if options.uri is not defined', () => { + const options = {}; + + interceptor.onRequest(options); + + sinon.assert.notCalled(webex.internal.services.replaceHostFromHostmap); + + assert.isUndefined(options.uri); + }); + + it('does not modify options.uri if replaceHostFromHostmap throws an error', () => { + const options = { + uri: 'http://example.com', + }; + + webex.internal.services.replaceHostFromHostmap.throws(new Error('replaceHostFromHostmap error')); + + interceptor.onRequest(options); + + sinon.assert.calledWith( + webex.internal.services.replaceHostFromHostmap, + 'http://example.com' + ); + + assert.equal(options.uri, 'http://example.com'); + }); + }); + }); + }); +}); diff --git a/packages/@webex/webex-core/test/unit/spec/services/services.js b/packages/@webex/webex-core/test/unit/spec/services/services.js index 80f63b5c5ba..9e13ee09310 100644 --- a/packages/@webex/webex-core/test/unit/spec/services/services.js +++ b/packages/@webex/webex-core/test/unit/spec/services/services.js @@ -290,6 +290,61 @@ describe('webex-core', () => { }); }); + describe('replaceHostFromHostmap', () => { + it('returns the same uri if the hostmap is not set', () => { + services._hostCatalog = null; + + const uri = 'http://example.com'; + + assert.equal(services.replaceHostFromHostmap(uri), uri); + }); + + it('returns the same uri if the hostmap does not contain the host', () => { + services._hostCatalog = { + 'not-example.com': [ + { + host: 'example-1.com', + ttl: -1, + priority: 5, + id: '0:0:0:example', + }, + ], + }; + + const uri = 'http://example.com'; + + assert.equal(services.replaceHostFromHostmap(uri), uri); + }); + + it('returns the original uri if the hostmap has no hosts for the host', () => { + + services._hostCatalog = { + 'example.com': [], + }; + + const uri = 'http://example.com'; + + assert.equal(services.replaceHostFromHostmap(uri), uri); + }); + + it('returns the replaces the host in the uri with the host from the hostmap', () => { + services._hostCatalog = { + 'example.com': [ + { + host: 'example-1.com', + ttl: -1, + priority: 5, + id: '0:0:0:example', + }, + ], + }; + + const uri = 'http://example.com/somepath'; + + assert.equal(services.replaceHostFromHostmap(uri), 'http://example-1.com/somepath'); + }); + }); + describe('#_formatReceivedHostmap()', () => { let serviceHostmap; let formattedHM; From de1524898654ea4b4edceb8a7251cae1fa4ae49a Mon Sep 17 00:00:00 2001 From: Sreekanth Narayanan <131740035+sreenara@users.noreply.github.com> Date: Wed, 11 Sep 2024 14:25:23 +0530 Subject: [PATCH 09/48] feat(contacts): move contacts to scim from dss (#3819) --- docs/samples/calling/app.js | 4 + .../CallingClient/calling/CallerId/types.ts | 27 +-- .../calling/src/CallingClient/constants.ts | 4 - .../src/Contacts/ContactsClient.test.ts | 69 +++++- .../calling/src/Contacts/ContactsClient.ts | 154 ++++++------- packages/calling/src/Contacts/constants.ts | 2 + .../calling/src/Contacts/contactFixtures.ts | 204 +++++++++++++++--- packages/calling/src/Contacts/types.ts | 18 +- packages/calling/src/SDKConnector/types.ts | 14 +- packages/calling/src/common/Utils.test.ts | 17 +- packages/calling/src/common/Utils.ts | 14 +- packages/calling/src/common/constants.ts | 5 + packages/calling/src/common/testUtil.ts | 5 - packages/calling/src/common/types.ts | 90 +++++--- packages/webex/src/calling.js | 1 - 15 files changed, 405 insertions(+), 223 deletions(-) diff --git a/docs/samples/calling/app.js b/docs/samples/calling/app.js index 1702f10f894..2555f593779 100644 --- a/docs/samples/calling/app.js +++ b/docs/samples/calling/app.js @@ -1301,6 +1301,10 @@ async function createCustomContact() { type: 'work', value: formData.get('phone') }], + emails: [{ + type: 'work', + value: formData.get('email') + }], contactType: 'CUSTOM', }; const res = await contacts.createContact(contact); diff --git a/packages/calling/src/CallingClient/calling/CallerId/types.ts b/packages/calling/src/CallingClient/calling/CallerId/types.ts index aefdfd25298..7fcf796f514 100644 --- a/packages/calling/src/CallingClient/calling/CallerId/types.ts +++ b/packages/calling/src/CallingClient/calling/CallerId/types.ts @@ -1,5 +1,5 @@ import {CallerIdInfo} from '../../../Events/types'; -import {DisplayInformation, PhoneNumber} from '../../../common/types'; +import {DisplayInformation} from '../../../common/types'; export type EmailType = { primary: boolean; @@ -18,31 +18,6 @@ export type PhotoType = { value: string; }; -export type ResourceType = { - userName: string; - emails: Array; - name: { - givenName: string; - familyName: string; - }; - phoneNumbers: Array; - entitlements: Array; - id: string; - photos: Array; - displayName: string; - active: boolean; - sipAddresses: Array; -}; - -/* The scim response has many fields , dropping few of them as they are not to be consumed by us */ -export type scimResponseBody = { - totalResults: number; - itemsPerPage: number; - startIndex: number; - schemas: Array; - Resources: Array; -}; - /** * Represents the interface for fetching caller ID details. */ diff --git a/packages/calling/src/CallingClient/constants.ts b/packages/calling/src/CallingClient/constants.ts index b395a335a5d..3e325e2c454 100644 --- a/packages/calling/src/CallingClient/constants.ts +++ b/packages/calling/src/CallingClient/constants.ts @@ -43,9 +43,7 @@ export const DUMMY_METRICS = { }; export const DUMMY_MOBIUS_URL = 'https://mobius.aintgen-a-1.int.infra.webex.com/api/v1'; export const FETCH_NAME = /^[a-zA-Z ]+/; -export const IDENTITY_BROKER = 'https://identitybts.webex.com/'; export const IP_ENDPOINT = 'myip'; -export const IDENTITY_ENDPOINT_RESOURCE = 'identity'; export const INITIAL_SEQ_NUMBER = 1; export const MEDIA_ENDPOINT_RESOURCE = 'media'; export const NETWORK_FLAP_TIMEOUT = 2000; @@ -54,8 +52,6 @@ export const CALL_TRANSFER_SERVICE = 'calltransfer'; export const HOLD_ENDPOINT = 'hold'; export const TRANSFER_ENDPOINT = 'commit'; export const RESUME_ENDPOINT = 'resume'; -export const SCIM_ENDPOINT_RESOURCE = 'scim'; -export const SCIM_USER_FILTER = 'v1/Users?filter='; export const SPARK_USER_AGENT = 'spark-user-agent'; export const REGISTER_RETRY_TIMEOUT = 10000; export const SUPPLEMENTARY_SERVICES_TIMEOUT = 10000; diff --git a/packages/calling/src/Contacts/ContactsClient.test.ts b/packages/calling/src/Contacts/ContactsClient.test.ts index ee1068f02eb..304ab3c5bf4 100644 --- a/packages/calling/src/Contacts/ContactsClient.test.ts +++ b/packages/calling/src/Contacts/ContactsClient.test.ts @@ -1,9 +1,15 @@ -import {HTTP_METHODS, WebexRequestPayload} from '../common/types'; +import {HTTP_METHODS, SCIMListResponse, WebexRequestPayload} from '../common/types'; import {getTestUtilsWebex} from '../common/testUtil'; import {LOGGER} from '../Logger/types'; import {Contact, ContactResponse, IContacts} from './types'; import {createContactsClient} from './ContactsClient'; -import {FAILURE_MESSAGE, SUCCESS_MESSAGE} from '../common/constants'; +import { + FAILURE_MESSAGE, + IDENTITY_ENDPOINT_RESOURCE, + SCIM_ENDPOINT_RESOURCE, + SCIM_USER_FILTER, + SUCCESS_MESSAGE, +} from '../common/constants'; import log from '../Logger'; import { CONTACTS_FILE, @@ -24,12 +30,12 @@ import { mockContactResponseBodyOne, mockCountry, mockDisplayNameOne, - mockDSSResponse, mockEmail, mockFirstName, mockLastName, mockNumber1, mockNumber2, + mockSCIMListResponse, mockSipAddress, mockState, mockStreet, @@ -51,6 +57,7 @@ describe('ContactClient Tests', () => { // eslint-disable-next-line no-underscore-dangle const contactServiceUrl = `${webex.internal.services._serviceUrls.contactsService}/${ENCRYPT_FILTER}/${USERS}/${CONTACT_FILTER}`; + const scimUrl = `${webex.internal.services._serviceUrls.identity}/${IDENTITY_ENDPOINT_RESOURCE}/${SCIM_ENDPOINT_RESOURCE}/${webex.internal.device.orgId}/${SCIM_USER_FILTER}id%20eq%20%22801bb994-343b-4f6b-97ae-d13c91d4b877%22`; // eslint-disable-next-line no-underscore-dangle const contactServiceGroupUrl = `${webex.internal.services._serviceUrls.contactsService}/${ENCRYPT_FILTER}/${USERS}/${GROUP_FILTER}`; const serviceErrorCodeHandlerSpy = jest.spyOn(utils, 'serviceErrorCodeHandler'); @@ -90,6 +97,8 @@ describe('ContactClient Tests', () => { expectedMessage: string; expectedStatusCode: number; decryptTextList: Array; + cloudContactPresent?: boolean; + scimResponse?: SCIMListResponse; }[] = [ { name: 'Success case 1: fetch contacts using get contacts api, custom and cloud contact present', @@ -118,6 +127,8 @@ describe('ContactClient Tests', () => { mockSipAddress, mockGroupName, ], + cloudContactPresent: true, + scimResponse: mockSCIMListResponse, }, { name: 'Success case 2: fetch contacts using get contacts api, single custom contact with mandatory details present', @@ -202,7 +213,10 @@ describe('ContactClient Tests', () => { codeObj.decryptTextList.forEach((text) => { webex.internal.encryption.decryptText.mockResolvedValueOnce(text); }); - webex.internal.dss.lookup.mockResolvedValueOnce(mockDSSResponse); + + if (codeObj.scimResponse) { + webex.request.mockResolvedValueOnce(mockSCIMListResponse); + } } else { respPayload['message'] = FAILURE_MESSAGE; respPayload['data'] = codeObj.payloadData; @@ -211,10 +225,34 @@ describe('ContactClient Tests', () => { const contactsResponse = await contactClient.getContacts(); - expect(webex.request).toBeCalledOnceWith({ - uri: contactServiceUrl, - method: HTTP_METHODS.GET, - }); + if (codeObj.inputStatusCode === 200) { + if (codeObj.cloudContactPresent) { + expect(webex.request).toBeCalledTimes(2); + } else { + expect(webex.request).toBeCalledTimes(1); + } + expect(webex.request).toHaveBeenNthCalledWith(1, { + uri: contactServiceUrl, + method: HTTP_METHODS.GET, + }); + + if (codeObj.cloudContactPresent) { + expect(webex.request).toHaveBeenNthCalledWith(2, { + uri: scimUrl, + method: HTTP_METHODS.GET, + headers: { + 'cisco-device-url': + 'https://wdm-intb.ciscospark.com/wdm/api/v1/devices/c5ae3b86-1bb7-40f1-a6a9-c296ee7e61d5', + 'spark-user-agent': 'webex-calling/beta', + }, + }); + } + } else { + expect(webex.request).toBeCalledOnceWith({ + uri: contactServiceUrl, + method: HTTP_METHODS.GET, + }); + } expect(contactsResponse).toEqual({ data: expect.any(Object), @@ -563,7 +601,8 @@ describe('ContactClient Tests', () => { webex.request .mockResolvedValueOnce(successResponsePayloadGroup) - .mockResolvedValueOnce(successResponsePayload); + .mockResolvedValueOnce(successResponsePayload) + .mockResolvedValueOnce(mockSCIMListResponse); webex.internal.encryption.encryptText.mockResolvedValueOnce('Encrypted group name'); @@ -581,14 +620,13 @@ describe('ContactClient Tests', () => { expect(res.statusCode).toEqual(400); expect(res.data.error).toEqual('contactId is required for contactType:CLOUD.'); - webex.internal.dss.lookup.mockResolvedValueOnce(mockDSSResponse); contact.contactId = mockContactResponse.contactId; res = await contactClient.createContact(contact); expect(res.statusCode).toEqual(201); expect(res.data.contact?.contactId).toBe(mockContactResponse.contactId); - expect(webex.request).toBeCalledTimes(2); + expect(webex.request).toBeCalledTimes(3); expect(webex.request).toHaveBeenNthCalledWith(1, { method: HTTP_METHODS.POST, uri: contactServiceGroupUrl, @@ -610,6 +648,15 @@ describe('ContactClient Tests', () => { groups: ['1561977e-3443-4ccf-a591-69686275d7d2'], }, }); + expect(webex.request).toHaveBeenNthCalledWith(3, { + uri: scimUrl, + method: HTTP_METHODS.GET, + headers: { + 'cisco-device-url': + 'https://wdm-intb.ciscospark.com/wdm/api/v1/devices/c5ae3b86-1bb7-40f1-a6a9-c296ee7e61d5', + 'spark-user-agent': 'webex-calling/beta', + }, + }); }); it('create a contact - service unavailable', async () => { diff --git a/packages/calling/src/Contacts/ContactsClient.ts b/packages/calling/src/Contacts/ContactsClient.ts index d701f7d8f0e..28bf3c1a934 100644 --- a/packages/calling/src/Contacts/ContactsClient.ts +++ b/packages/calling/src/Contacts/ContactsClient.ts @@ -1,6 +1,12 @@ /* eslint-disable no-await-in-loop */ -import {FAILURE_MESSAGE, STATUS_CODE, SUCCESS_MESSAGE} from '../common/constants'; -import {HTTP_METHODS, WebexRequestPayload, ContactDetail} from '../common/types'; +import { + FAILURE_MESSAGE, + SCIM_ENTERPRISE_USER, + SCIM_WEBEXIDENTITY_USER, + STATUS_CODE, + SUCCESS_MESSAGE, +} from '../common/constants'; +import {HTTP_METHODS, WebexRequestPayload, ContactDetail, SCIMListResponse} from '../common/types'; import {LoggerInterface} from '../Voicemail/types'; import {ISDKConnector, WebexSDK} from '../SDKConnector/types'; import SDKConnector from '../SDKConnector'; @@ -13,6 +19,8 @@ import { DEFAULT_GROUP_NAME, ENCRYPT_FILTER, GROUP_FILTER, + OR, + SCIM_ID_FILTER, USERS, encryptedFields, } from './constants'; @@ -27,7 +35,7 @@ import { GroupType, } from './types'; -import {serviceErrorCodeHandler} from '../common/Utils'; +import {scimQuery, serviceErrorCodeHandler} from '../common/Utils'; import Logger from '../Logger'; import ExtendedError from '../Errors/catalog/ExtendedError'; import {ERROR_TYPE} from '../Errors/types'; @@ -66,7 +74,6 @@ export class ContactsClient implements IContacts { } this.webex = this.sdkConnector.getWebex(); - this.webex.internal.dss.register(); this.encryptionKeyUrl = ''; this.groups = undefined; @@ -253,75 +260,59 @@ export class ContactsClient implements IContacts { return decryptedContact; } - /** - * Fetches contacts from DSS. - */ - private async fetchContactFromDSS( + private resolveCloudContacts( contactsDataMap: ContactIdContactInfo, - id: string - ): Promise { - try { - const contact = await this.webex.internal.dss.lookup({id, shouldBatch: true}); - - const contactId = contact.identity; - const {displayName, emails, phoneNumbers, sipAddresses, photos} = contact; - const {department, firstName, identityManager, jobTitle, lastName} = contact.additionalInfo; - const manager = - identityManager && identityManager.displayName ? identityManager.displayName : undefined; - const {contactType, avatarUrlDomain, encryptionKeyUrl, ownerId, groups} = - contactsDataMap[contactId]; - let avatarURL = ''; - - if (photos.length) { - avatarURL = photos[0].value; - } - const addedPhoneNumbers = contactsDataMap[contactId].phoneNumbers; + inputList: SCIMListResponse + ): Contact[] | null { + const finalContactList: Contact[] = []; - if (addedPhoneNumbers) { - const decryptedPhoneNumbers = await this.decryptContactDetail( - encryptionKeyUrl, - addedPhoneNumbers - ); - - decryptedPhoneNumbers.forEach((number) => phoneNumbers.push(number)); - } - - const addedSipAddresses = contactsDataMap[contactId].sipAddresses; + try { + const contactList = Object.keys(contactsDataMap); + for (let n = 0; n < contactList.length; n += 1) { + const filteredContact = inputList.Resources.filter((item) => item.id === contactList[n])[0]; + + const {displayName, emails, phoneNumbers, photos} = filteredContact; + const {sipAddresses} = filteredContact[SCIM_WEBEXIDENTITY_USER]; + const firstName = filteredContact.name.givenName; + const lastName = filteredContact.name.familyName; + const manager = filteredContact[SCIM_ENTERPRISE_USER].manager.displayName; + const department = filteredContact[SCIM_ENTERPRISE_USER].department; + + let avatarURL = ''; + if (photos?.length) { + avatarURL = photos[0].value; + } - if (addedSipAddresses) { - const decryptedSipAddresses = await this.decryptContactDetail( + const {contactType, avatarUrlDomain, encryptionKeyUrl, ownerId, groups} = + contactsDataMap[contactList[n]]; + + const cloudContact = { + avatarUrlDomain, + avatarURL, + contactId: contactList[n], + contactType, + department, + displayName, + emails, encryptionKeyUrl, - addedSipAddresses - ); - - decryptedSipAddresses.forEach((address) => sipAddresses.push(address)); + firstName, + groups, + lastName, + manager, + ownerId, + phoneNumbers, + sipAddresses, + }; + + finalContactList.push(cloudContact); } - - const cloudContact = { - avatarUrlDomain, - avatarURL, - contactId, - contactType, - department, - displayName, - emails, - encryptionKeyUrl, - firstName, - groups, - lastName, - manager, - ownerId, - phoneNumbers, - sipAddresses, - title: jobTitle, - }; - - return cloudContact; } catch (error: any) { Logger.error(new ExtendedError(error.message, {}, ERROR_TYPE.DEFAULT), {}); return null; } + + return finalContactList; } /** @@ -334,7 +325,7 @@ export class ContactsClient implements IContacts { }; const contactList: Contact[] = []; - const contactsDataMap: ContactIdContactInfo = {}; + const cloudContactsMap: ContactIdContactInfo = {}; try { const response = await this.webex.request({ @@ -351,19 +342,27 @@ export class ContactsClient implements IContacts { const {contacts, groups} = responseBody; - for (let i = 0; i < contacts.length; i += 1) { - const contact = contacts[i]; - + contacts.map(async (contact) => { if (contact.contactType === ContactType.CUSTOM) { const decryptedContact = await this.decryptContact(contact); contactList.push(decryptedContact); } else if (contact.contactType === ContactType.CLOUD && contact.contactId) { - contactsDataMap[contact.contactId] = contact; - const contactDetails = await this.fetchContactFromDSS(contactsDataMap, contact.contactId); - if (contactDetails) { - contactList.push(contactDetails); - } + cloudContactsMap[contact.contactId] = contact; + } + }); + + // Resolve cloud contacts + if (Object.keys(cloudContactsMap).length) { + const contactIdList = Object.keys(cloudContactsMap); + const query = contactIdList.map((item) => `${SCIM_ID_FILTER} "${item}"`).join(OR); + const result = await scimQuery(query); + const resolvedContacts = this.resolveCloudContacts( + cloudContactsMap, + result.body as SCIMListResponse + ); + if (resolvedContacts) { + resolvedContacts.map((item) => contactList.push(item)); } } @@ -699,13 +698,14 @@ export class ContactsClient implements IContacts { }; if (contact.contactType === ContactType.CLOUD && newContact.contactId) { - const decryptedContact = await this.fetchContactFromDSS( + const query = `${SCIM_ID_FILTER} "${newContact.contactId}"`; + const res = await scimQuery(query); + const resolvedContact = this.resolveCloudContacts( Object.fromEntries([[newContact.contactId, newContact]]) as ContactIdContactInfo, - newContact.contactId + res.body as SCIMListResponse ); - - if (decryptedContact) { - this.contacts?.push(decryptedContact); + if (resolvedContact) { + this.contacts?.push(resolvedContact[0]); } } else { this.contacts?.push(contact); diff --git a/packages/calling/src/Contacts/constants.ts b/packages/calling/src/Contacts/constants.ts index 8b69f44686b..705ceda4381 100644 --- a/packages/calling/src/Contacts/constants.ts +++ b/packages/calling/src/Contacts/constants.ts @@ -5,6 +5,8 @@ export const ENCRYPT_FILTER = 'encrypt'; export const USERS = 'Users'; export const DEFAULT_GROUP_NAME = 'Other contacts'; export const CONTACTS_SCHEMA = 'urn:cisco:codev:identity:contact:core:1.0'; +export const SCIM_ID_FILTER = 'id eq'; +export const OR = ' or '; export enum encryptedFields { ADDRESS_INFO = 'addressInfo', diff --git a/packages/calling/src/Contacts/contactFixtures.ts b/packages/calling/src/Contacts/contactFixtures.ts index afe591a8f83..605925e7010 100644 --- a/packages/calling/src/Contacts/contactFixtures.ts +++ b/packages/calling/src/Contacts/contactFixtures.ts @@ -272,39 +272,187 @@ export const mockContactGroupListTwo = [ }, ]; -export const mockDSSResponse = { - additionalInfo: { - created: '2022-08-05T02:51:46.055Z', - department: '123029217', - extLinkedAccts: [{providerID: 'cisco.webex.com', accountGUID: '500802287', status: 'active'}], - firstName: 'Emily', - identityManager: { - displayName: 'Robert Langdon', - managerId: '9d0fce00-95b2-435f-99d1-b6b44759fbdc', +const scimUser1 = { + schemas: [ + 'urn:ietf:params:scim:schemas:core:2.0:User', + 'urn:scim:schemas:extension:cisco:webexidentity:2.0:User', + 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User', + ], + id: 'userId', + userName: 'johndoe@cisco.com', + active: true, + name: { + familyName: 'Doe', + givenName: 'John', + }, + displayName: 'John Doe', + emails: [ + { + value: 'johndoe@cisco.com', + type: 'work', + primary: true, }, - jobTitle: 'Software Engineer', - lastName: 'Nagakawa', - modified: '2023-03-03T13:37:03.196Z', - nickName: 'Emily', - userName: 'emikawa2@cisco.com', + ], + userType: 'user', + phoneNumbers: [ + { + value: '+91 22 1234 5678', + type: 'work', + }, + ], + photos: [ + { + value: 'photoUrl', + type: 'photo', + }, + { + value: 'thumbnailURL', + type: 'thumbnail', + }, + ], + addresses: [ + { + type: 'work', + streetAddress: 'Street', + locality: 'BANGALORE', + region: 'KARNATAKA', + postalCode: '560103', + country: 'IN', + }, + ], + 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User': { + department: '123029016', + manager: { + value: 'userId', + displayName: 'Jane Smith', + $ref: 'scimUrl', + }, + }, + 'urn:scim:schemas:extension:cisco:webexidentity:2.0:User': { + isTeamsOnJabberEnabled: false, + isUCCallOnJabberEnabled: false, + licenseID: ['license'], + userSettings: ['setting'], + userPreferences: ['preferences'], + sipAddresses: [ + { + value: 'johndoe@cisco.call.ciscospark.com', + type: 'cloud-calling', + }, + { + value: 'johndoe@cisco.calls.webex.com', + type: 'cloud-calling', + primary: true, + }, + { + value: 'johndoe@cisco.webex.com', + type: 'personal-room', + }, + ], + meta: { + organizationId: 'orgId', + }, + userNameType: 'email', + }, + meta: { + resourceType: 'User', + location: 'scimUrl', + version: 'W/"16629124099"', + created: '2019-12-24T02:01:42.803Z', + lastModified: '2024-08-21T14:22:55.987Z', }, - displayName: 'Emily Nagakawa', - emails: [{value: 'emikawa2@cisco.com'}], - entityProviderType: 'CI_USER', - identity: '801bb994-343b-4f6b-97ae-d13c91d4b877', - orgId: '1eb65fdf-9643-417f-9974-ad72cae0e10f', +}; + +const scimUser2NoPhoto = { + schemas: [ + 'urn:ietf:params:scim:schemas:core:2.0:User', + 'urn:scim:schemas:extension:cisco:webexidentity:2.0:User', + 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User', + ], + id: 'userId', + userName: 'janedoe@cisco.com', + active: true, + name: { + familyName: 'Doe', + givenName: 'Jane', + }, + displayName: 'Jane Doe', + emails: [ + { + value: 'janedoe@cisco.com', + type: 'work', + primary: true, + }, + ], + userType: 'user', phoneNumbers: [ - {type: 'mobile', value: '+1 835 648 8750'}, - {type: 'work', value: '+1 791 723 8825'}, + { + value: '+91 22 1234 5678', + type: 'work', + }, ], - photos: [{value: 'avatar-prod-us-east-2.webexcontent.com'}], - sipAddresses: [ - {type: 'cloud-calling', value: 'emikawa2@cisco.call.ciscospark.com', primary: true}, - {type: 'personal-room', value: 'emikawa2@cisco.webex.com', primary: false}, - {type: 'enterprise', value: 'emikawa2@cisco.com', primary: true}, - {type: 'personal-room', value: '25762555827@cisco.webex.com', primary: false}, + addresses: [ + { + type: 'work', + streetAddress: 'Street', + locality: 'BANGALORE', + region: 'KARNATAKA', + postalCode: '560103', + country: 'IN', + }, ], - type: 'PERSON', + 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User': { + department: '123029016', + manager: { + value: 'userId', + displayName: 'John Smith', + $ref: 'scimUrl', + }, + }, + 'urn:scim:schemas:extension:cisco:webexidentity:2.0:User': { + isTeamsOnJabberEnabled: false, + isUCCallOnJabberEnabled: false, + licenseID: ['license'], + userSettings: ['setting'], + userPreferences: ['preferences'], + sipAddresses: [ + { + value: 'janedoe@cisco.call.ciscospark.com', + type: 'cloud-calling', + }, + { + value: 'janedoe@cisco.calls.webex.com', + type: 'cloud-calling', + primary: true, + }, + { + value: 'janedoe@cisco.webex.com', + type: 'personal-room', + }, + ], + meta: { + organizationId: 'orgId', + }, + userNameType: 'email', + }, + meta: { + resourceType: 'User', + location: 'scimUrl', + version: 'W/"16629124099"', + created: '2019-12-24T02:01:42.803Z', + lastModified: '2024-08-21T14:22:55.987Z', + }, +}; + +export const mockSCIMListResponse = { + statusCode: 200, + body: { + schemas: ['urn:ietf:params:scim:api:messages:2.0:ListResponse'], + totalResults: 11, + itemsPerPage: 11, + startIndex: 1, + Resources: [scimUser1, scimUser2NoPhoto], + }, }; export const mockKmsKey = { diff --git a/packages/calling/src/Contacts/types.ts b/packages/calling/src/Contacts/types.ts index 2c9d2e16fbf..7c4484117a2 100644 --- a/packages/calling/src/Contacts/types.ts +++ b/packages/calling/src/Contacts/types.ts @@ -1,19 +1,11 @@ import {ISDKConnector} from '../SDKConnector/types'; -import {ContactDetail} from '../common/types'; +import {Address, PhoneNumber, URIAddress} from '../common/types'; export enum ContactType { CUSTOM = 'CUSTOM', CLOUD = 'CLOUD', } -export type AddressType = { - city?: string; - country?: string; - state?: string; - street?: string; - zipCode?: string; -}; - /** * `Contact` object is used to represent a contact. */ @@ -21,7 +13,7 @@ export type Contact = { /** * This represents the complete address of the contact. */ - addressInfo?: AddressType; + addressInfo?: Address; /** * This represents the URL of the avatar of the contact. */ @@ -53,7 +45,7 @@ export type Contact = { /** * This represents the array of different email addresses of the contact. */ - emails?: ContactDetail[]; + emails?: URIAddress[]; /** * This is encrypted key url of the contact used for encryption. */ @@ -85,7 +77,7 @@ export type Contact = { /** * This represents the array of different phone numbers of the contact. */ - phoneNumbers?: ContactDetail[]; + phoneNumbers?: PhoneNumber[]; /** * Primary contact method as set by the contact. */ @@ -97,7 +89,7 @@ export type Contact = { /** * This represents the array of different sip addresses of the contact. */ - sipAddresses?: ContactDetail[]; + sipAddresses?: URIAddress[]; /** * This represents the job title of the contact. */ diff --git a/packages/calling/src/SDKConnector/types.ts b/packages/calling/src/SDKConnector/types.ts index 3f5760d518b..68f92869c5c 100644 --- a/packages/calling/src/SDKConnector/types.ts +++ b/packages/calling/src/SDKConnector/types.ts @@ -1,11 +1,4 @@ -import { - DSSLookupResponse, - KmsKey, - KmsResourceObject, - LookupOptions, - PeopleListResponse, - WebexRequestPayload, -} from '../common/types'; +import {KmsKey, KmsResourceObject, PeopleListResponse, WebexRequestPayload} from '../common/types'; /* eslint-disable no-shadow */ type Listener = (e: string, data?: unknown) => void; @@ -82,10 +75,6 @@ export interface WebexSDK { }; }; }; - dss: { - lookup: (options: LookupOptions) => Promise; - register: () => Promise; - }; encryption: { decryptText: (encryptionKeyUrl: string, encryptedData?: string) => Promise; encryptText: (encryptionKeyUrl: string, text?: string) => Promise; @@ -109,7 +98,6 @@ export interface WebexSDK { mercuryApi: string; 'ucmgmt-gateway': string; contactsService: string; - directorySearch: string; }; fetchClientRegionInfo: () => Promise; }; diff --git a/packages/calling/src/common/Utils.test.ts b/packages/calling/src/common/Utils.test.ts index e480b3497b9..3014240caa8 100644 --- a/packages/calling/src/common/Utils.test.ts +++ b/packages/calling/src/common/Utils.test.ts @@ -20,15 +20,7 @@ import { RegistrationStatus, } from './types'; import log from '../Logger'; -import { - CALL_FILE, - DUMMY_METRICS, - UTILS_FILE, - IDENTITY_ENDPOINT_RESOURCE, - SCIM_ENDPOINT_RESOURCE, - SCIM_USER_FILTER, - REGISTER_UTIL, -} from '../CallingClient/constants'; +import {CALL_FILE, DUMMY_METRICS, UTILS_FILE, REGISTER_UTIL} from '../CallingClient/constants'; import { CALL_ERROR_CODE, ERROR_CODE, @@ -54,7 +46,12 @@ import { getAscVoicemailListJsonWXC, getDescVoicemailListJsonWXC, } from '../Voicemail/voicemailFixture'; -import {INFER_ID_CONSTANT} from './constants'; +import { + IDENTITY_ENDPOINT_RESOURCE, + INFER_ID_CONSTANT, + SCIM_ENDPOINT_RESOURCE, + SCIM_USER_FILTER, +} from './constants'; import {CALL_EVENT_KEYS} from '../Events/types'; const mockSubmitRegistrationMetric = jest.fn(); diff --git a/packages/calling/src/common/Utils.ts b/packages/calling/src/common/Utils.ts index d2a3d5b4763..68b7fb0211b 100644 --- a/packages/calling/src/common/Utils.ts +++ b/packages/calling/src/common/Utils.ts @@ -27,7 +27,6 @@ import { LineErrorObject, } from '../Errors/types'; import { - ALLOWED_SERVICES, CALLING_BACKEND, CorrelationId, DecodeType, @@ -36,6 +35,7 @@ import { IDeviceInfo, MobiusServers, RegistrationStatus, + SCIMListResponse, SORT, ServiceData, ServiceIndicator, @@ -52,7 +52,6 @@ import { CISCO_DEVICE_URL, CODEC_ID, DUMMY_METRICS, - IDENTITY_ENDPOINT_RESOURCE, INBOUND_CODEC_MATCH, INBOUND_RTP, JITTER_BUFFER_DELAY, @@ -75,8 +74,6 @@ import { RTC_ICE_CANDIDATE_PAIR, RTP_RX_STAT, RTP_TX_STAT, - SCIM_ENDPOINT_RESOURCE, - SCIM_USER_FILTER, SELECTED_CANDIDATE_PAIR_ID, SPARK_USER_AGENT, TARGET_BIT_RATE, @@ -113,9 +110,11 @@ import { NATIVE_WEBEX_TEAMS_CALLING, NATIVE_SIP_CALL_TO_UCM, BW_XSI_ENDPOINT_VERSION, + IDENTITY_ENDPOINT_RESOURCE, + SCIM_ENDPOINT_RESOURCE, + SCIM_USER_FILTER, } from './constants'; import {Model, WebexSDK} from '../SDKConnector/types'; -import {scimResponseBody} from '../CallingClient/calling/CallerId/types'; import SDKConnector from '../SDKConnector'; import {CallSettingResponse} from '../CallSettings/types'; import {ContactResponse} from '../Contacts/types'; @@ -1177,7 +1176,7 @@ export function getSortedVoicemailList( * @param filter - A filter for the query. * @returns - Promise. */ -async function scimQuery(filter: string) { +export async function scimQuery(filter: string) { log.info(`Starting resolution for filter:- ${filter}`, { file: UTILS_FILE, method: 'scimQuery', @@ -1195,7 +1194,6 @@ async function scimQuery(filter: string) { [CISCO_DEVICE_URL]: webex.internal.device.url, [SPARK_USER_AGENT]: CALLING_USER_AGENT, }, - service: ALLOWED_SERVICES.MOBIUS, })); } @@ -1211,7 +1209,7 @@ export async function resolveCallerIdDisplay(filter: string) { try { const response = await scimQuery(filter); - resolution = response.body as scimResponseBody; + resolution = response.body as SCIMListResponse; log.info(`Number of records found for this user :- ${resolution.totalResults}`, { file: UTILS_FILE, diff --git a/packages/calling/src/common/constants.ts b/packages/calling/src/common/constants.ts index 92d37654651..8166e6e25aa 100644 --- a/packages/calling/src/common/constants.ts +++ b/packages/calling/src/common/constants.ts @@ -4,6 +4,7 @@ export const BINARY = 'binary'; export const CONTENT = 'content'; export const DEVICES = 'devices'; export const FAILURE_MESSAGE = 'FAILURE'; +export const IDENTITY_ENDPOINT_RESOURCE = 'identity'; export const ITEMS = 'items'; export const KEY = 'key'; export const OBJECT = 'object'; @@ -12,6 +13,8 @@ export const RAW_REQUEST = 'rawRequest'; export const RESPONSE = 'response'; export const RESPONSE_DATA = 'responseData'; export const RESPONSE_MESSAGE = 'responseMessage'; +export const SCIM_ENDPOINT_RESOURCE = 'scim'; +export const SCIM_USER_FILTER = 'v2/Users?filter='; export const SETTINGS = 'settings'; export const STATUS_CODE = 'statusCode'; export const SUCCESS_MESSAGE = 'SUCCESS'; @@ -39,3 +42,5 @@ export const BW_XSI_URL = 'broadworksXsiActionsUrl'; export const WEBEX_CALLING_CONNECTOR_FILE = 'WxCallBackendConnector'; export const UCM_CONNECTOR_FILE = 'UcmBackendConnector'; export const VOICEMAIL = 'VOICEMAIL'; +export const SCIM_WEBEXIDENTITY_USER = 'urn:scim:schemas:extension:cisco:webexidentity:2.0:User'; +export const SCIM_ENTERPRISE_USER = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'; diff --git a/packages/calling/src/common/testUtil.ts b/packages/calling/src/common/testUtil.ts index 8dd11ab0909..0a8f2195f72 100644 --- a/packages/calling/src/common/testUtil.ts +++ b/packages/calling/src/common/testUtil.ts @@ -51,10 +51,6 @@ export function getTestUtilsWebex() { }, }, }, - dss: { - lookup: jest.fn(), - register: jest.fn(), - }, encryption: { decryptText: jest.fn(), encryptText: jest.fn(), @@ -78,7 +74,6 @@ export function getTestUtilsWebex() { mercuryApi: 'https://mercury-api-intb.ciscospark.com/v1', 'ucmgmt-gateway': 'https://gw.telemetry.int-ucmgmt.cisco.com', contactsService: 'https://contacts-service-a.wbx2.com/contact/api/v1', - directorySearch: 'https://directory-search-a.wbx2.com/direcory-search/api/v1/', }, fetchClientRegionInfo: jest.fn(), }, diff --git a/packages/calling/src/common/types.ts b/packages/calling/src/common/types.ts index 5a592b6b959..0e708ccd3e8 100644 --- a/packages/calling/src/common/types.ts +++ b/packages/calling/src/common/types.ts @@ -1,3 +1,5 @@ +import {SCIM_ENTERPRISE_USER, SCIM_WEBEXIDENTITY_USER} from './constants'; + export type MobiusDeviceId = string; export type MobiusDeviceUri = string; export type SettingEnabled = boolean; @@ -30,14 +32,13 @@ export enum CALLING_BACKEND { export type DeviceList = unknown; export type CallId = string; // guid; export type CorrelationId = string; -export type SipAddress = string; export enum CallType { URI = 'uri', TEL = 'tel', } export type CallDetails = { type: CallType; - address: SipAddress; // sip address + address: string; // sip address }; export type CallDestination = CallDetails; @@ -181,32 +182,11 @@ export type ContactDetail = { value: string; }; -export interface LookupOptions { - id: string; - shouldBatch: boolean; -} - -export type DSSLookupResponse = { - additionalInfo: { - department: string; - firstName: string; - identityManager: { - managerId: string; - displayName: string; - }; - jobTitle: string; - lastName: string; - }; - displayName: string; - emails: ContactDetail[]; - entityProviderType: string; - identity: string; - orgId: string; - phoneNumbers: ContactDetail[]; - photos: ContactDetail[]; - sipAddresses: ContactDetail[]; +export interface URIAddress { + value: string; type: string; -}; + primary?: boolean; +} export type KmsKey = { uri: string; @@ -222,3 +202,59 @@ export type KmsResourceObject = { keyUris: string[]; authorizationUris: string[]; }; + +export interface Name { + familyName: string; + givenName: string; +} + +export interface Address { + city?: string; + country?: string; + state?: string; + street?: string; + zipCode?: string; +} + +interface WebexIdentityMeta { + organizationId: string; +} +interface WebexIdentityUser { + sipAddresses: URIAddress[]; + meta: WebexIdentityMeta; +} + +interface Manager { + value: string; + displayName: string; + $ref: string; +} + +interface EnterpriseUser { + department: string; + manager: Manager; +} + +interface Resource { + schemas: string[]; + id: string; + userName: string; + active: boolean; + name: Name; + displayName: string; + emails: URIAddress[]; + userType: string; + phoneNumbers: PhoneNumber[]; + photos?: ContactDetail[]; + addresses: Address[]; + [SCIM_WEBEXIDENTITY_USER]: WebexIdentityUser; + [SCIM_ENTERPRISE_USER]: EnterpriseUser; +} + +export interface SCIMListResponse { + schemas: string[]; + totalResults: number; + itemsPerPage: number; + startIndex: number; + Resources: Resource[]; +} diff --git a/packages/webex/src/calling.js b/packages/webex/src/calling.js index c7af8455cfd..fb91d3b466d 100644 --- a/packages/webex/src/calling.js +++ b/packages/webex/src/calling.js @@ -5,7 +5,6 @@ import EventEmitter from 'events'; require('@webex/internal-plugin-device'); require('@webex/internal-plugin-mercury'); require('@webex/internal-plugin-encryption'); -require('@webex/internal-plugin-dss'); const merge = require('lodash/merge'); const WebexCore = require('@webex/webex-core').default; From eaa8f489a3cfd314fa35030d876f4e34c5661b2b Mon Sep 17 00:00:00 2001 From: Marcin Date: Thu, 12 Sep 2024 08:24:29 +0100 Subject: [PATCH 10/48] fix(meetings): sending ice candidate errors with add media success metric (#3823) --- packages/@webex/plugin-meetings/src/meeting/index.ts | 2 ++ .../@webex/plugin-meetings/test/unit/spec/meeting/index.js | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index e255e3f633b..35fd4e3a04f 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -7022,6 +7022,7 @@ export default class Meeting extends StatelessWebexPlugin { await this.mediaProperties.getCurrentConnectionInfo(); // @ts-ignore const reachabilityStats = await this.webex.meetings.reachability.getReachabilityMetrics(); + const iceCandidateErrors = Object.fromEntries(this.iceCandidateErrors); Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.ADD_MEDIA_SUCCESS, { correlation_id: this.correlationId, @@ -7033,6 +7034,7 @@ export default class Meeting extends StatelessWebexPlugin { retriedWithTurnServer: this.addMediaData.retriedWithTurnServer, isJoinWithMediaRetry: this.joinWithMediaRetryInfo.isRetry, ...reachabilityStats, + ...iceCandidateErrors, iceCandidatesCount: this.iceCandidatesCount, }); // @ts-ignore diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js index ea5fca28e99..2a7e9eff420 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js @@ -3023,6 +3023,8 @@ describe('plugin-meetings', () => { }), }; meeting.iceCandidatesCount = 3; + meeting.iceCandidateErrors.set('701_error', 3); + meeting.iceCandidateErrors.set('701_turn_host_lookup_received_error', 1); await meeting.addMedia({ mediaSettings: {}, @@ -3044,6 +3046,8 @@ describe('plugin-meetings', () => { someReachabilityMetric1: 'some value1', someReachabilityMetric2: 'some value2', iceCandidatesCount: 3, + '701_error': 3, + '701_turn_host_lookup_received_error': 1, } ); From 3f6f3752b38ecfe1c5d726f1742a43cc791809b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edmond=20Vuji=C4=87i?= <67634227+edvujic@users.noreply.github.com> Date: Thu, 12 Sep 2024 15:20:42 +0200 Subject: [PATCH 11/48] refactor: remove network quality monitor (#3817) Co-authored-by: evujici Co-authored-by: Anna Tsukanova --- packages/@webex/media-helpers/package.json | 2 +- packages/@webex/plugin-meetings/package.json | 2 +- .../plugin-meetings/src/meeting/index.ts | 7 +- .../src/networkQualityMonitor/index.ts | 211 ------------------ .../unit/spec/networkQualityMonitor/index.js | 99 -------- packages/calling/package.json | 2 +- yarn.lock | 96 ++++---- 7 files changed, 55 insertions(+), 364 deletions(-) delete mode 100644 packages/@webex/plugin-meetings/src/networkQualityMonitor/index.ts delete mode 100644 packages/@webex/plugin-meetings/test/unit/spec/networkQualityMonitor/index.js diff --git a/packages/@webex/media-helpers/package.json b/packages/@webex/media-helpers/package.json index 2c8eae9c839..b086980d52d 100644 --- a/packages/@webex/media-helpers/package.json +++ b/packages/@webex/media-helpers/package.json @@ -22,7 +22,7 @@ "deploy:npm": "yarn npm publish" }, "dependencies": { - "@webex/internal-media-core": "2.10.0", + "@webex/internal-media-core": "2.10.2", "@webex/ts-events": "^1.1.0", "@webex/web-media-effects": "2.18.0" }, diff --git a/packages/@webex/plugin-meetings/package.json b/packages/@webex/plugin-meetings/package.json index e8f6d152c15..e3f65d30687 100644 --- a/packages/@webex/plugin-meetings/package.json +++ b/packages/@webex/plugin-meetings/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@webex/common": "workspace:*", - "@webex/internal-media-core": "2.10.0", + "@webex/internal-media-core": "2.10.2", "@webex/internal-plugin-conversation": "workspace:*", "@webex/internal-plugin-device": "workspace:*", "@webex/internal-plugin-llm": "workspace:*", diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index 35fd4e3a04f..1b7b9099c4d 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -24,6 +24,8 @@ import { RoapMessage, StatsAnalyzer, StatsAnalyzerEventNames, + NetworkQualityEventNames, + NetworkQualityMonitor, } from '@webex/internal-media-core'; import { @@ -54,7 +56,6 @@ import { AddMediaFailed, } from '../common/errors/webex-errors'; -import NetworkQualityMonitor from '../networkQualityMonitor'; import LoggerProxy from '../common/logs/logger-proxy'; import EventsUtil from '../common/events/util'; import Trigger from '../common/events/trigger-proxy'; @@ -6500,7 +6501,7 @@ export default class Meeting extends StatelessWebexPlugin { }); this.setupStatsAnalyzerEventHandlers(); this.networkQualityMonitor.on( - EVENT_TRIGGERS.NETWORK_QUALITY, + NetworkQualityEventNames.NETWORK_QUALITY, this.sendNetworkQualityEvent.bind(this) ); } @@ -8193,7 +8194,7 @@ export default class Meeting extends StatelessWebexPlugin { * @private * @memberof Meeting */ - private sendNetworkQualityEvent(res: any) { + private sendNetworkQualityEvent(res: {networkQualityScore: number; mediaType: string}) { Trigger.trigger( this, { diff --git a/packages/@webex/plugin-meetings/src/networkQualityMonitor/index.ts b/packages/@webex/plugin-meetings/src/networkQualityMonitor/index.ts deleted file mode 100644 index ee77feeb153..00000000000 --- a/packages/@webex/plugin-meetings/src/networkQualityMonitor/index.ts +++ /dev/null @@ -1,211 +0,0 @@ -import EventsScope from '../common/events/events-scope'; -import {EVENT_TRIGGERS} from '../constants'; - -/** - * Meeting - network quality event - * Emitted on each interval of retrieving stats Analyzer data - * @event network:quality - * @type {Object} - * @property {string} mediaType {video|audio} - * @property {number} networkQualityScore - value determined in determineUplinkNetworkQuality - * @memberof NetworkQualityMonitor - */ -/** - * NetworkQualityMonitor class that will emit events based on detected quality - * - * @class NetworkQualityMonitor - * @extends {EventsScope} - */ -export default class NetworkQualityMonitor extends EventsScope { - config: any; - frequencyTypes: any; - indicatorTypes: any; - mediaType: any; - networkQualityScore: any; - networkQualityStatus: any; - - /** - * Creates a new instance of NetworkQualityMonitor - * @constructor - * @public - * @param {Object} config - * @property {Object} indicatorTypes - network properties used to evaluate network quality used as constants - * @property {Object} frequencyTypes - frequency properties used as constants {uplink|send} {downlink|receive} - * @property {number} networkQualityScore - 0|1 1 is acceptable 0 is bad/unknown - * @property {Object} networkQualityStatus - hash object based on indicatorTypes and frequencyTypes - * @property {string} mediaType - audio|video - */ - constructor(config: any) { - super(); - this.config = config; - this.indicatorTypes = Object.freeze({ - PACKETLOSS: 'packetLoss', - LATENCY: 'latency', - JITTER: 'jitter', - }); - this.frequencyTypes = Object.freeze({ - UPLINK: 'uplink', - DOWNLINK: 'downlink', - }); - this.networkQualityScore = 1; - this.networkQualityStatus = { - [this.frequencyTypes.UPLINK]: {}, - }; - this.mediaType = null; - } - - /** - * emits NETWORK_QUALITY event on meeting with payload of media type and uplinkNetworkQuality score - * - * @memberof NetworkQualityMonitor - * @returns {void} - */ - emitNetworkQuality() { - this.emit( - { - file: 'networkQualityMonitor', - function: 'emitNetworkQuality', - }, - EVENT_TRIGGERS.NETWORK_QUALITY, - { - mediaType: this.mediaType, - networkQualityScore: this.networkQualityScore, - } - ); - } - - /** - * invokes emitNetworkQuality method resets values back to default - * @returns {void} - * @memberof NetworkQualityMonitor - */ - updateNetworkQualityStatus() { - this.emitNetworkQuality(); - - // reset values - this.networkQualityScore = 1; - this.mediaType = null; - } - - /** - * filter data to determine uplink network quality, invoked on same interval as stats analyzer remote-inbout-rtp - * @param {Object} configObj - * @param {string} configObj.mediaType {audio|video} - * @param {RTCStats} configObj.remoteRtpResults RTC stats remote obj - * @param {Object} configObj.statsAnalyzerCurrentStats statsResults - * @returns {void} - * @public - * @memberof NetworkQualityMonitor - */ - public determineUplinkNetworkQuality({ - mediaType, - remoteRtpResults, - statsAnalyzerCurrentStats, - }: { - mediaType: string; - remoteRtpResults: any; - statsAnalyzerCurrentStats: object; - }) { - const roundTripTimeInMilliseconds = remoteRtpResults.roundTripTime * 1000; - const jitterInMilliseconds = remoteRtpResults.jitter * 1000; - const {currentPacketLossRatio} = statsAnalyzerCurrentStats[mediaType].send; - - this.mediaType = mediaType; - - const {JITTER, PACKETLOSS, LATENCY} = this.indicatorTypes; - const {UPLINK} = this.frequencyTypes; - - /** - * determines if packetLoss ratio is over threshold set in config - * sets networkQualityScore to 0 if over threshold - * @returns {boolean} - */ - const determinePacketLoss = () => { - if (currentPacketLossRatio > this.config.videoPacketLossRatioThreshold) { - this.networkQualityScore = 0; - - return false; - } - - return true; - }; - - /** - * determines if round trip time value is over threshold set in config - * sets networkQualityScore to 0 if over threshold - * @returns {boolean} - */ - const determineLatency = () => { - if (roundTripTimeInMilliseconds > this.config.rttThreshold) { - this.networkQualityScore = 0; - - return false; - } - - return true; - }; - - /** - * determines if jitter value is over threshold in config - * sets networkQualityScore to 0 if over threshold - * @returns {boolean} - */ - const deterMineJitter = () => { - if (jitterInMilliseconds > this.config.jitterThreshold) { - this.networkQualityScore = 0; - - return false; - } - - return true; - }; - - /** - * returns null if val is specifically undefined - * @param {(number|undefined)} value - * @returns {(number|null)} - */ - const determineIfUndefined = (value: number | undefined) => - typeof value === 'undefined' ? null : value; - - if (!this.networkQualityStatus[UPLINK][mediaType]) { - this.networkQualityStatus[UPLINK][mediaType] = {}; - } - - /** - * Values for some browsers specifically Safari will be undefined we explicitly set to null - * https://bugs.webkit.org/show_bug.cgi?id=206645 - * https://bugs.webkit.org/show_bug.cgi?id=212668 - */ - // PACKET LOSS - this.networkQualityStatus[UPLINK][mediaType][PACKETLOSS] = { - acceptable: determinePacketLoss(), - value: determineIfUndefined(currentPacketLossRatio), - }; - - // LATENCY measured in Round trip time - this.networkQualityStatus[UPLINK][mediaType][LATENCY] = { - acceptable: determineLatency(), - value: determineIfUndefined(remoteRtpResults.roundTripTime), - }; - - // JITTER - this.networkQualityStatus[UPLINK][mediaType][JITTER] = { - acceptable: deterMineJitter(), - value: determineIfUndefined(remoteRtpResults.jitter), - }; - - this.updateNetworkQualityStatus(); - } - - /** - * Get the current status of network quaility object - networkQualityStatus - * @returns {Object} - * @public - */ - get networkQualityStats() { - const {UPLINK} = this.frequencyTypes; - - return this.networkQualityStatus[UPLINK]; - } -} diff --git a/packages/@webex/plugin-meetings/test/unit/spec/networkQualityMonitor/index.js b/packages/@webex/plugin-meetings/test/unit/spec/networkQualityMonitor/index.js deleted file mode 100644 index 762effbb645..00000000000 --- a/packages/@webex/plugin-meetings/test/unit/spec/networkQualityMonitor/index.js +++ /dev/null @@ -1,99 +0,0 @@ -import 'jsdom-global/register'; -import chai from 'chai'; -import chaiAsPromised from 'chai-as-promised'; -import sinon from 'sinon'; - -import NetworkQualityMonitor from '../../../../src/networkQualityMonitor'; -import {EVENT_TRIGGERS} from '../../../../src/constants'; - -const {assert} = chai; - -chai.use(chaiAsPromised); -sinon.assert.expose(chai.assert, {prefix: ''}); - -// eslint-disable-next-line mocha/no-exclusive-tests -describe('plugin-meetings', () => { - describe('NetworkQualityMonitor', () => { - let networkQualityMonitor; - let sandBoxEmitSpy; - - const initialConfig = { - videoPacketLossRatioThreshold: 9, - rttThreshold: 500, - jitterThreshold: 500, - }; - - const configObject = { - mediaType: 'video-send', - remoteRtpResults: { - id: 'RTCRemoteInboundRtpVideoStream_2411086660', - timestamp: 1624472676193.79, - type: 'remote-inbound-rtp', - ssrc: 2411086660, - kind: 'video', - transportId: 'RTCTransport_1_1', - codecId: 'RTCCodec_1_Outbound_102', - jitter: 0.004, - packetsLost: 8, - localId: 'RTCOutboundRTPVideoStream_2411086660', - roundTripTime: 0.648, - fractionLost: 0, - totalRoundTripTime: 3.554, - roundTripTimeMeasurements: 14, - }, - statsAnalyzerCurrentStats: { - 'audio-send': { - send: { - currentPacketLossRatio: 8, - }, - }, - 'video-send': { - send: { - currentPacketLossRatio: 10, - }, - }, - }, - }; - - const sandbox = sinon.createSandbox(); - - beforeEach(() => { - networkQualityMonitor = new NetworkQualityMonitor(initialConfig); - sandbox.spy(networkQualityMonitor, 'updateNetworkQualityStatus'); - sandBoxEmitSpy = sandbox.spy(networkQualityMonitor, 'emit'); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it('should trigger updateNetworkQualityStatus when determineUplinkNetworkQuality has finished', async () => { - await networkQualityMonitor.determineUplinkNetworkQuality(configObject); - - assert.calledOnce(networkQualityMonitor.updateNetworkQualityStatus); - }); - - it('should emit a network quality judgement event with the proper payload', async () => { - await networkQualityMonitor.determineUplinkNetworkQuality(configObject); - assert( - sandBoxEmitSpy.calledWith( - sinon.match({ - file: 'networkQualityMonitor', - function: 'emitNetworkQuality', - }), - sinon.match(EVENT_TRIGGERS.NETWORK_QUALITY), - sinon.match({ - mediaType: 'video-send', - networkQualityScore: 0, - }) - ) - ); - }); - - it('should reset to default values after determineUplinkNetworkQuality call stack is complete', async () => { - await networkQualityMonitor.determineUplinkNetworkQuality(configObject); - assert.isNull(networkQualityMonitor.mediaType); - assert.deepEqual(networkQualityMonitor.networkQualityScore, 1); - }); - }); -}); diff --git a/packages/calling/package.json b/packages/calling/package.json index 3638a90ad9b..0d975f4a253 100644 --- a/packages/calling/package.json +++ b/packages/calling/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "@types/platform": "1.3.4", - "@webex/internal-media-core": "2.10.0", + "@webex/internal-media-core": "2.10.2", "@webex/media-helpers": "workspace:*", "async-mutex": "0.4.0", "buffer": "6.0.3", diff --git a/yarn.lock b/yarn.lock index 01fd898c89c..081ac9f3761 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2035,21 +2035,21 @@ __metadata: linkType: hard "@babel/runtime-corejs2@npm:^7.25.0": - version: 7.25.4 - resolution: "@babel/runtime-corejs2@npm:7.25.4" + version: 7.25.6 + resolution: "@babel/runtime-corejs2@npm:7.25.6" dependencies: core-js: ^2.6.12 regenerator-runtime: ^0.14.0 - checksum: 5c543575e3eef559a81657e696ee7c2b71da159905b59eaf1b9b9a2efbd8c5259edb4f79d5a86c97326fa47a7dc447f60133e00ca4977ac5244fefdf34fc5921 + checksum: afd5406391d7e41ac291ae40c549dc8da734ec8d160b560081b4153b4063409b7402e5c469bcebd0789aaeaca740d3fe088cbe45be3a4c7341e3a2b3f48fdd98 languageName: node linkType: hard -"@babel/runtime@npm:^7.18.9, @babel/runtime@npm:^7.25.0": - version: 7.25.4 - resolution: "@babel/runtime@npm:7.25.4" +"@babel/runtime@npm:^7.18.9, @babel/runtime@npm:^7.25.6": + version: 7.25.6 + resolution: "@babel/runtime@npm:7.25.6" dependencies: regenerator-runtime: ^0.14.0 - checksum: 5c2aab03788e77f1f959d7e6ce714c299adfc9b14fb6295c2a17eb7cad0dd9c2ebfb2d25265f507f68c43d5055c5cd6f71df02feb6502cea44b68432d78bcbbe + checksum: ee1a69d3ac7802803f5ee6a96e652b78b8addc28c6a38c725a4ad7d61a059d9e6cb9f6550ed2f63cce67a1bd82e0b1ef66a1079d895be6bfb536a5cfbd9ccc32 languageName: node linkType: hard @@ -5921,11 +5921,11 @@ __metadata: linkType: hard "@types/node@npm:^20.14.1": - version: 20.16.1 - resolution: "@types/node@npm:20.16.1" + version: 20.16.5 + resolution: "@types/node@npm:20.16.5" dependencies: undici-types: ~6.19.2 - checksum: 2b8f30f416f5c1851ffa8a13ef6c464a5e355edfd763713c22813a7839f6419a64e27925f9e89c972513d78432263179332f0bffb273d16498233bfdf495d096 + checksum: f38b7bd8c4993dcf38943afa2ffdd7dfd18fc94f8f3f28d0c1045a10d39871a6cc1b8f8d3bf0c7ed848457d0e1d283482f6ca125579c13fed1b7575d23e8e8f5 languageName: node linkType: hard @@ -7419,7 +7419,7 @@ __metadata: "@typescript-eslint/eslint-plugin": 5.38.1 "@typescript-eslint/parser": 5.38.1 "@web/dev-server": 0.4.5 - "@webex/internal-media-core": 2.10.0 + "@webex/internal-media-core": 2.10.2 "@webex/media-helpers": "workspace:*" async-mutex: 0.4.0 buffer: 6.0.3 @@ -7710,9 +7710,9 @@ __metadata: languageName: unknown linkType: soft -"@webex/internal-media-core@npm:2.10.0": - version: 2.10.0 - resolution: "@webex/internal-media-core@npm:2.10.0" +"@webex/internal-media-core@npm:2.10.2": + version: 2.10.2 + resolution: "@webex/internal-media-core@npm:2.10.2" dependencies: "@babel/runtime": ^7.18.9 "@babel/runtime-corejs2": ^7.25.0 @@ -7724,7 +7724,7 @@ __metadata: uuid: ^8.3.2 webrtc-adapter: ^8.1.2 xstate: ^4.30.6 - checksum: 4191b75de30a22c131dba7cbc4922cf5da52ad799ab059391539355f50dcdf8aa02fb2fd738a5bbf4938a54bc869e00ba7f74fdd58e04f231825665fd7c05ce2 + checksum: 76fe4feba27b6734fde24dd0b84f0a43d24b6faf9110ced0ab0c8122746ffa27b50cd5862dde1192750432f2daff618c6ca0cb4ca9496e1b9a2ba47788a58174 languageName: node linkType: hard @@ -8483,7 +8483,7 @@ __metadata: "@babel/preset-typescript": 7.22.11 "@webex/babel-config-legacy": "workspace:*" "@webex/eslint-config-legacy": "workspace:*" - "@webex/internal-media-core": 2.10.0 + "@webex/internal-media-core": 2.10.2 "@webex/jest-config-legacy": "workspace:*" "@webex/legacy-tools": "workspace:*" "@webex/test-helper-chai": "workspace:*" @@ -8719,7 +8719,7 @@ __metadata: "@webex/babel-config-legacy": "workspace:*" "@webex/common": "workspace:*" "@webex/eslint-config-legacy": "workspace:*" - "@webex/internal-media-core": 2.10.0 + "@webex/internal-media-core": 2.10.2 "@webex/internal-plugin-conversation": "workspace:*" "@webex/internal-plugin-device": "workspace:*" "@webex/internal-plugin-llm": "workspace:*" @@ -9012,12 +9012,12 @@ __metadata: linkType: soft "@webex/rtcstats@npm:^1.3.2": - version: 1.3.3 - resolution: "@webex/rtcstats@npm:1.3.3" + version: 1.4.0 + resolution: "@webex/rtcstats@npm:1.4.0" dependencies: "@types/node": ^20.14.1 uuid: ^8.3.2 - checksum: 7ac73b2f6bf8bf44bfaff7a5e904b175509632b80bf4cbdb09c5570c940d7e02445ba0c95f713067b03770eee49162d964b98a8d1e05f42e3f80b5bfd503352c + checksum: a83455d93f66b39e4c2f694d7665fca5d7da9eab33431d9c09262f438971085fecb664f7f8bb6775fec63b62af01612f3eb6b48125619e3a532a8b764019520b languageName: node linkType: hard @@ -16976,13 +16976,13 @@ __metadata: languageName: node linkType: hard -"fast-unique-numbers@npm:^9.0.8": - version: 9.0.8 - resolution: "fast-unique-numbers@npm:9.0.8" +"fast-unique-numbers@npm:^9.0.9": + version: 9.0.9 + resolution: "fast-unique-numbers@npm:9.0.9" dependencies: - "@babel/runtime": ^7.25.0 - tslib: ^2.6.3 - checksum: 27840ed4ada274f6391cc81a977ab068ef0931a835711415794010ff8775f1ba2ffc4ae890b00b28fdee2e54a929efec6cf9e154ae958f55d16d55cc4023f6cc + "@babel/runtime": ^7.25.6 + tslib: ^2.7.0 + checksum: 58481531260a91d57859a631378609368a5c3828a36da2e833f1b82f205eec7fe698df76151af84a4bbb1bf911ed9659b79e646d418462d7981e0e24e48f6394 languageName: node linkType: hard @@ -31144,7 +31144,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.6.3": +"tslib@npm:^2.7.0": version: 2.7.0 resolution: "tslib@npm:2.7.0" checksum: 1606d5c89f88d466889def78653f3aab0f88692e80bb2066d090ca6112ae250ec1cfa9dbfaab0d17b60da15a4186e8ec4d893801c67896b277c17374e36e1d28 @@ -33307,37 +33307,37 @@ __metadata: languageName: node linkType: hard -"worker-timers-broker@npm:^7.1.1": - version: 7.1.1 - resolution: "worker-timers-broker@npm:7.1.1" +"worker-timers-broker@npm:^7.1.2": + version: 7.1.2 + resolution: "worker-timers-broker@npm:7.1.2" dependencies: - "@babel/runtime": ^7.25.0 - fast-unique-numbers: ^9.0.8 - tslib: ^2.6.3 - worker-timers-worker: ^8.0.3 - checksum: baa4a7bf49efb8ade9be7cb7865ac27e8030b278d7698ce70a09972ab92a5a7a3e6aaf5dbb3ebecc2d9f94562f10643dada43159382b40a2b9d257f82f473071 + "@babel/runtime": ^7.25.6 + fast-unique-numbers: ^9.0.9 + tslib: ^2.7.0 + worker-timers-worker: ^8.0.4 + checksum: ec2deb097662ef2331cdf4681023fe970504cd30b58cbf13ceab0741d05fd21a49e518c73f03b20a01e2684d9b8d6d59787aed82d05e615be99c8dc0229623c4 languageName: node linkType: hard -"worker-timers-worker@npm:^8.0.3": - version: 8.0.3 - resolution: "worker-timers-worker@npm:8.0.3" +"worker-timers-worker@npm:^8.0.4": + version: 8.0.4 + resolution: "worker-timers-worker@npm:8.0.4" dependencies: - "@babel/runtime": ^7.25.0 - tslib: ^2.6.3 - checksum: af06dc1df2eb5b45ed5d0b5d56de621411f9354633eb9d4368effa00c856301d02c83c632c77645f2753167f0168136d17c2b8cbf986b041ba5b08cf8c10ad83 + "@babel/runtime": ^7.25.6 + tslib: ^2.7.0 + checksum: fd59d4c947895efd036e46cd8d4c288b228256f7bac24ff5b83c682ef44e53584ce8bc4f0525eee469be00dbf1f89e9266682d0297df7478704996087a7553b2 languageName: node linkType: hard "worker-timers@npm:^8.0.2": - version: 8.0.4 - resolution: "worker-timers@npm:8.0.4" + version: 8.0.5 + resolution: "worker-timers@npm:8.0.5" dependencies: - "@babel/runtime": ^7.25.0 - tslib: ^2.6.3 - worker-timers-broker: ^7.1.1 - worker-timers-worker: ^8.0.3 - checksum: 51bcc64f01ea143ac644b88d10b211afd0dd05991ae2a747066f25456f9e84eaa48e530c01d71998a1bc2a00303d8ea40ea97f070bea5e7ec84890140f7f1cb6 + "@babel/runtime": ^7.25.6 + tslib: ^2.7.0 + worker-timers-broker: ^7.1.2 + worker-timers-worker: ^8.0.4 + checksum: e8c00e33e5af252a37472c0fc1bc3c3b26cdd174e179ab1b301364b880db7e560f6f3eb82a4438970fe113c1e9a4855569df0ef61edc6d5de613a51789587ef4 languageName: node linkType: hard From 8573ece085707036204d4ae11f46f5d76c9b4a5d Mon Sep 17 00:00:00 2001 From: Sreekanth Narayanan <131740035+sreenara@users.noreply.github.com> Date: Fri, 13 Sep 2024 13:51:37 +0530 Subject: [PATCH 12/48] fix(contacts): handle optional scim fields (#3827) --- .../src/Contacts/ContactsClient.test.ts | 38 +++++++++++++++++++ .../calling/src/Contacts/ContactsClient.ts | 26 ++++++------- .../calling/src/Contacts/contactFixtures.ts | 30 ++++++++++++++- packages/calling/src/Contacts/types.ts | 4 +- packages/calling/src/common/Utils.test.ts | 24 ++++++++++++ packages/calling/src/common/Utils.ts | 6 +-- packages/calling/src/common/testUtil.ts | 26 +++++++++++++ packages/calling/src/common/types.ts | 24 ++++++------ 8 files changed, 146 insertions(+), 32 deletions(-) diff --git a/packages/calling/src/Contacts/ContactsClient.test.ts b/packages/calling/src/Contacts/ContactsClient.test.ts index 304ab3c5bf4..01b6fc6b4a7 100644 --- a/packages/calling/src/Contacts/ContactsClient.test.ts +++ b/packages/calling/src/Contacts/ContactsClient.test.ts @@ -48,6 +48,8 @@ import { mockContactGroupListOne, mockContactGroupListTwo, mockAvatarURL, + mockSCIMMinListResponse, + mockContactMinimum, } from './contactFixtures'; describe('ContactClient Tests', () => { @@ -724,4 +726,40 @@ describe('ContactClient Tests', () => { expect(contactClient['contacts']).toEqual(mockContactListOne); }); + + it('test resolveContacts function for a minimal contact with few details', () => { + const contact = contactClient['resolveCloudContacts']( + {userId: mockContactMinimum}, + mockSCIMMinListResponse.body + ); + + expect(contact).toEqual([ + { + avatarURL: '', + avatarUrlDomain: undefined, + contactId: 'userId', + contactType: 'CLOUD', + department: undefined, + displayName: undefined, + emails: undefined, + encryptionKeyUrl: 'kms://cisco.com/keys/dcf18f9d-155e-44ff-ad61-c8a69b7103ab', + firstName: undefined, + groups: ['1561977e-3443-4ccf-a591-69686275d7d2'], + lastName: undefined, + manager: undefined, + ownerId: 'ownerId', + phoneNumbers: undefined, + sipAddresses: undefined, + }, + ]); + }); + + it('test resolveContacts function encountering an error', () => { + const contact = contactClient['resolveCloudContacts']( + {userId: mockContactMinimum}, + mockSCIMMinListResponse + ); + + expect(contact).toEqual(null); + }); }); diff --git a/packages/calling/src/Contacts/ContactsClient.ts b/packages/calling/src/Contacts/ContactsClient.ts index 28bf3c1a934..5bb05d67899 100644 --- a/packages/calling/src/Contacts/ContactsClient.ts +++ b/packages/calling/src/Contacts/ContactsClient.ts @@ -36,9 +36,6 @@ import { } from './types'; import {scimQuery, serviceErrorCodeHandler} from '../common/Utils'; -import Logger from '../Logger'; -import ExtendedError from '../Errors/catalog/ExtendedError'; -import {ERROR_TYPE} from '../Errors/types'; /** * `ContactsClient` module is designed to offer a set of APIs for retrieving and updating contacts and groups from the contacts-service. @@ -264,6 +261,10 @@ export class ContactsClient implements IContacts { contactsDataMap: ContactIdContactInfo, inputList: SCIMListResponse ): Contact[] | null { + const loggerContext = { + file: CONTACTS_FILE, + method: 'resolveCloudContacts', + }; const finalContactList: Contact[] = []; try { @@ -272,16 +273,15 @@ export class ContactsClient implements IContacts { const filteredContact = inputList.Resources.filter((item) => item.id === contactList[n])[0]; const {displayName, emails, phoneNumbers, photos} = filteredContact; - const {sipAddresses} = filteredContact[SCIM_WEBEXIDENTITY_USER]; - const firstName = filteredContact.name.givenName; - const lastName = filteredContact.name.familyName; - const manager = filteredContact[SCIM_ENTERPRISE_USER].manager.displayName; - const department = filteredContact[SCIM_ENTERPRISE_USER].department; - - let avatarURL = ''; - if (photos?.length) { - avatarURL = photos[0].value; + let sipAddresses; + if (filteredContact[SCIM_WEBEXIDENTITY_USER]) { + sipAddresses = filteredContact[SCIM_WEBEXIDENTITY_USER].sipAddresses; } + const firstName = filteredContact.name?.givenName; + const lastName = filteredContact.name?.familyName; + const manager = filteredContact[SCIM_ENTERPRISE_USER]?.manager?.displayName; + const department = filteredContact[SCIM_ENTERPRISE_USER]?.department; + const avatarURL = photos?.length ? photos[0].value : ''; const {contactType, avatarUrlDomain, encryptionKeyUrl, ownerId, groups} = contactsDataMap[contactList[n]]; @@ -307,7 +307,7 @@ export class ContactsClient implements IContacts { finalContactList.push(cloudContact); } } catch (error: any) { - Logger.error(new ExtendedError(error.message, {}, ERROR_TYPE.DEFAULT), {}); + log.warn('Error occurred while parsing resolved contacts', loggerContext); return null; } diff --git a/packages/calling/src/Contacts/contactFixtures.ts b/packages/calling/src/Contacts/contactFixtures.ts index 605925e7010..4295a607411 100644 --- a/packages/calling/src/Contacts/contactFixtures.ts +++ b/packages/calling/src/Contacts/contactFixtures.ts @@ -272,6 +272,21 @@ export const mockContactGroupListTwo = [ }, ]; +export const mockContactMinimum = { + contactId: 'userId', + contactType: 'CLOUD', + encryptionKeyUrl: 'kms://cisco.com/keys/dcf18f9d-155e-44ff-ad61-c8a69b7103ab', + groups: ['1561977e-3443-4ccf-a591-69686275d7d2'], + ownerId: 'ownerId', +}; + +export const scimUserMinimum = { + schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'], + id: 'userId', + userName: 'userName', + userType: 'user', +}; + const scimUser1 = { schemas: [ 'urn:ietf:params:scim:schemas:core:2.0:User', @@ -448,13 +463,24 @@ export const mockSCIMListResponse = { statusCode: 200, body: { schemas: ['urn:ietf:params:scim:api:messages:2.0:ListResponse'], - totalResults: 11, - itemsPerPage: 11, + totalResults: 2, + itemsPerPage: 2, startIndex: 1, Resources: [scimUser1, scimUser2NoPhoto], }, }; +export const mockSCIMMinListResponse = { + statusCode: 200, + body: { + schemas: ['urn:ietf:params:scim:api:messages:2.0:ListResponse'], + totalResults: 1, + itemsPerPage: 1, + startIndex: 1, + Resources: [scimUserMinimum], + }, +}; + export const mockKmsKey = { uri: 'kms://kms-cisco.wbx2.com/keys/16095024-612d-4424-ba51-57cad2402e14', }; diff --git a/packages/calling/src/Contacts/types.ts b/packages/calling/src/Contacts/types.ts index 7c4484117a2..443f0149034 100644 --- a/packages/calling/src/Contacts/types.ts +++ b/packages/calling/src/Contacts/types.ts @@ -29,7 +29,7 @@ export type Contact = { /** * Unique identifier of the contact. */ - contactId?: string; + contactId: string; /** * Indicates the type of the contact, can be `CLOUD` or `CUSTOM`. */ @@ -41,7 +41,7 @@ export type Contact = { /** * This represents the display name of the contact. */ - displayName: string; + displayName?: string; /** * This represents the array of different email addresses of the contact. */ diff --git a/packages/calling/src/common/Utils.test.ts b/packages/calling/src/common/Utils.test.ts index 3014240caa8..f35d45ef89b 100644 --- a/packages/calling/src/common/Utils.test.ts +++ b/packages/calling/src/common/Utils.test.ts @@ -8,6 +8,7 @@ import { getSamplePeopleListResponse, getSampleRawAndParsedMediaStats, getMobiusDiscoveryResponse, + getSampleMinimumScimResponse, } from './testUtil'; import { CallDirection, @@ -1048,6 +1049,29 @@ describe('resolveContact tests', () => { }); }); + it('Resolve with minimal response from SCIM', () => { + const callingPartyInfo = {} as CallingPartyInfo; + const scimResponse = getSampleMinimumScimResponse(); + + // scimResponse.Resources[0].photos = []; + const webexSpy = jest.spyOn(webex, 'request').mockResolvedValue({ + statusCode: 200, + body: scimResponse, + }); + + callingPartyInfo.userExternalId = {$: 'userExternalId'}; + resolveContact(callingPartyInfo).then((displayInfo) => { + expect(displayInfo?.name).toBeUndefined(); + expect(displayInfo?.num).toBeUndefined(); + expect(displayInfo?.avatarSrc).toStrictEqual('unknown'); + expect(displayInfo?.id).toStrictEqual(getSampleMinimumScimResponse().Resources[0].id); + + const query = scimUrl + encodeURIComponent(`id eq "${callingPartyInfo.userExternalId?.$}"`); + + expect(webexSpy).toBeCalledOnceWith(expect.objectContaining({uri: query})); + }); + }); + it('Resolve by name', () => { const callingPartyInfo = {} as CallingPartyInfo; const webexSpy = jest diff --git a/packages/calling/src/common/Utils.ts b/packages/calling/src/common/Utils.ts index 68b7fb0211b..44e4577d902 100644 --- a/packages/calling/src/common/Utils.ts +++ b/packages/calling/src/common/Utils.ts @@ -1234,12 +1234,12 @@ export async function resolveCallerIdDisplay(filter: string) { /* Pick only the primary number OR 2nd preference Work */ const numberObj = - scimResource.phoneNumbers.find((num) => num.primary) || - scimResource.phoneNumbers.find((num) => num.type.toLowerCase() === 'work'); + scimResource.phoneNumbers?.find((num) => num.primary) || + scimResource.phoneNumbers?.find((num) => num.type.toLowerCase() === 'work'); if (numberObj) { displayResult.num = numberObj.value; - } else if (scimResource.phoneNumbers.length > 0) { + } else if (scimResource.phoneNumbers && scimResource.phoneNumbers.length > 0) { /* When no primary number exists OR PA-ID/From failed to populate, we take the first number */ log.info('Failure to resolve caller information. Setting number as caller ID', { file: UTILS_FILE, diff --git a/packages/calling/src/common/testUtil.ts b/packages/calling/src/common/testUtil.ts index 0a8f2195f72..dff60f0e25f 100644 --- a/packages/calling/src/common/testUtil.ts +++ b/packages/calling/src/common/testUtil.ts @@ -260,6 +260,32 @@ export const getSampleScimResponse = () => { }; }; +export const getSampleMinimumScimResponse = () => { + return { + totalResults: '1', + itemsPerPage: '1', + startIndex: '1', + schemas: ['urn:scim:schemas:core:1.0'], + Resources: [ + { + userName: 'atlas.test.wxcwebrtc+user8@gmail.com', + id: 'userExternalId', + meta: { + created: '2022-03-16T16:13:53.847Z', + lastModified: '2022-05-31T14:39:12.782Z', + lastLoginTime: '2022-05-31T14:39:12.780Z', + version: 'W/"66025591113"', + location: + 'https://identitybts.webex.com/identity/scim/1704d30d-a131-4bc7-9449-948487643793/v1/Users/652fe0c7-05ce-4acd-8bda-9a080830187f', + organizationID: '1704d30d-a131-4bc7-9449-948487643793', + creator: '97fe25e3-d3e8-400e-856b-5b0cd5b0c790', + modifier: '8c7abf2f-0c8e-49cf-b8e4-693d4ec7daee', + }, + }, + ], + }; +}; + /** * Returns a sample people list response object. */ diff --git a/packages/calling/src/common/types.ts b/packages/calling/src/common/types.ts index 0e708ccd3e8..02605e6a1f0 100644 --- a/packages/calling/src/common/types.ts +++ b/packages/calling/src/common/types.ts @@ -220,8 +220,8 @@ interface WebexIdentityMeta { organizationId: string; } interface WebexIdentityUser { - sipAddresses: URIAddress[]; - meta: WebexIdentityMeta; + sipAddresses?: URIAddress[]; + meta?: WebexIdentityMeta; } interface Manager { @@ -231,24 +231,24 @@ interface Manager { } interface EnterpriseUser { - department: string; - manager: Manager; + department?: string; + manager?: Manager; } interface Resource { schemas: string[]; id: string; userName: string; - active: boolean; - name: Name; - displayName: string; - emails: URIAddress[]; + active?: boolean; + name?: Name; + displayName?: string; + emails?: URIAddress[]; userType: string; - phoneNumbers: PhoneNumber[]; + phoneNumbers?: PhoneNumber[]; photos?: ContactDetail[]; - addresses: Address[]; - [SCIM_WEBEXIDENTITY_USER]: WebexIdentityUser; - [SCIM_ENTERPRISE_USER]: EnterpriseUser; + addresses?: Address[]; + [SCIM_WEBEXIDENTITY_USER]?: WebexIdentityUser; + [SCIM_ENTERPRISE_USER]?: EnterpriseUser; } export interface SCIMListResponse { From 523ae6d0cea6f208794e87bc6622e728a7bf73a1 Mon Sep 17 00:00:00 2001 From: Sreekanth Narayanan <131740035+sreenara@users.noreply.github.com> Date: Fri, 13 Sep 2024 17:09:33 +0530 Subject: [PATCH 13/48] fix(contacts): fix loop and use only resolved contacts from scim (#3829) --- packages/calling/src/Contacts/ContactsClient.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/calling/src/Contacts/ContactsClient.ts b/packages/calling/src/Contacts/ContactsClient.ts index 5bb05d67899..efb2add327e 100644 --- a/packages/calling/src/Contacts/ContactsClient.ts +++ b/packages/calling/src/Contacts/ContactsClient.ts @@ -268,10 +268,8 @@ export class ContactsClient implements IContacts { const finalContactList: Contact[] = []; try { - const contactList = Object.keys(contactsDataMap); - for (let n = 0; n < contactList.length; n += 1) { - const filteredContact = inputList.Resources.filter((item) => item.id === contactList[n])[0]; - + for (let n = 0; n < inputList.Resources.length; n += 1) { + const filteredContact = inputList.Resources[n]; const {displayName, emails, phoneNumbers, photos} = filteredContact; let sipAddresses; if (filteredContact[SCIM_WEBEXIDENTITY_USER]) { @@ -284,12 +282,12 @@ export class ContactsClient implements IContacts { const avatarURL = photos?.length ? photos[0].value : ''; const {contactType, avatarUrlDomain, encryptionKeyUrl, ownerId, groups} = - contactsDataMap[contactList[n]]; + contactsDataMap[inputList.Resources[n].id]; const cloudContact = { avatarUrlDomain, avatarURL, - contactId: contactList[n], + contactId: inputList.Resources[n].id, contactType, department, displayName, From 1de73b3c9083efefdd823361db5648e030e9bb10 Mon Sep 17 00:00:00 2001 From: Sreekanth Narayanan <131740035+sreenara@users.noreply.github.com> Date: Sat, 14 Sep 2024 18:33:51 +0530 Subject: [PATCH 14/48] fix(contacts): use webexapis.com url to avoid cors (#3834) --- .../src/CallSettings/UcmBackendConnector.test.ts | 11 ++++++++--- .../src/CallSettings/UcmBackendConnector.ts | 14 ++++++-------- packages/calling/src/CallSettings/constants.ts | 2 -- .../calling/src/Contacts/ContactsClient.test.ts | 3 ++- packages/calling/src/common/Utils.test.ts | 3 ++- packages/calling/src/common/Utils.ts | 7 ++++++- packages/calling/src/common/constants.ts | 4 ++++ 7 files changed, 28 insertions(+), 16 deletions(-) diff --git a/packages/calling/src/CallSettings/UcmBackendConnector.test.ts b/packages/calling/src/CallSettings/UcmBackendConnector.test.ts index 45df2f651c0..7b3409e57b1 100644 --- a/packages/calling/src/CallSettings/UcmBackendConnector.test.ts +++ b/packages/calling/src/CallSettings/UcmBackendConnector.test.ts @@ -1,10 +1,15 @@ import {LOGGER} from '../Logger/types'; import * as utils from '../common/Utils'; -import {FAILURE_MESSAGE, SUCCESS_MESSAGE, UCM_CONNECTOR_FILE} from '../common/constants'; +import { + FAILURE_MESSAGE, + SUCCESS_MESSAGE, + UCM_CONNECTOR_FILE, + WEBEX_API_CONFIG_INT_URL, +} from '../common/constants'; import {getTestUtilsWebex} from '../common/testUtil'; import {HTTP_METHODS, WebexRequestPayload} from '../common/types'; import {UcmBackendConnector} from './UcmBackendConnector'; -import {CF_ENDPOINT, ORG_ENDPOINT, PEOPLE_ENDPOINT, WEBEX_APIS_INT_URL} from './constants'; +import {CF_ENDPOINT, ORG_ENDPOINT, PEOPLE_ENDPOINT} from './constants'; import {CallForwardAlwaysSetting, CallForwardingSettingsUCM, IUcmBackendConnector} from './types'; describe('Call Settings Client Tests for UcmBackendConnector', () => { @@ -44,7 +49,7 @@ describe('Call Settings Client Tests for UcmBackendConnector', () => { }, }; - const callForwardingUri = `${WEBEX_APIS_INT_URL}/${PEOPLE_ENDPOINT}/${userId}/${CF_ENDPOINT.toLowerCase()}?${ORG_ENDPOINT}=${orgId}`; + const callForwardingUri = `${WEBEX_API_CONFIG_INT_URL}/${PEOPLE_ENDPOINT}/${userId}/${CF_ENDPOINT.toLowerCase()}?${ORG_ENDPOINT}=${orgId}`; beforeAll(() => { callSettingsClient = new UcmBackendConnector(webex, {level: LOGGER.INFO}, false); diff --git a/packages/calling/src/CallSettings/UcmBackendConnector.ts b/packages/calling/src/CallSettings/UcmBackendConnector.ts index cbf6fd7e8f2..3aab3e44ee6 100644 --- a/packages/calling/src/CallSettings/UcmBackendConnector.ts +++ b/packages/calling/src/CallSettings/UcmBackendConnector.ts @@ -8,15 +8,11 @@ import { SUCCESS_MESSAGE, UCM_CONNECTOR_FILE, VOICEMAIL, + WEBEX_API_CONFIG_INT_URL, + WEBEX_API_CONFIG_PROD_URL, } from '../common/constants'; import {HTTP_METHODS, WebexRequestPayload} from '../common/types'; -import { - CF_ENDPOINT, - ORG_ENDPOINT, - PEOPLE_ENDPOINT, - WEBEX_APIS_INT_URL, - WEBEX_APIS_PROD_URL, -} from './constants'; +import {CF_ENDPOINT, ORG_ENDPOINT, PEOPLE_ENDPOINT} from './constants'; import { CallForwardAlwaysSetting, CallForwardingSettingsUCM, @@ -132,7 +128,9 @@ export class UcmBackendConnector implements IUcmBackendConnector { method: this.getCallForwardAlwaysSetting.name, }; - const webexApisUrl = this.useProdWebexApis ? WEBEX_APIS_PROD_URL : WEBEX_APIS_INT_URL; + const webexApisUrl = this.useProdWebexApis + ? WEBEX_API_CONFIG_PROD_URL + : WEBEX_API_CONFIG_INT_URL; try { if (directoryNumber) { diff --git a/packages/calling/src/CallSettings/constants.ts b/packages/calling/src/CallSettings/constants.ts index a869a5703ce..95912f9fac4 100644 --- a/packages/calling/src/CallSettings/constants.ts +++ b/packages/calling/src/CallSettings/constants.ts @@ -7,5 +7,3 @@ export const CF_ENDPOINT = 'features/callForwarding'; export const VM_ENDPOINT = 'features/voicemail'; export const CALL_WAITING_ENDPOINT = 'CallWaiting'; export const XSI_VERSION = 'v2.0'; -export const WEBEX_APIS_INT_URL = 'https://integration.webexapis.com/v1/uc/config'; -export const WEBEX_APIS_PROD_URL = 'https://webexapis.com/v1/uc/config'; diff --git a/packages/calling/src/Contacts/ContactsClient.test.ts b/packages/calling/src/Contacts/ContactsClient.test.ts index 01b6fc6b4a7..404023aaba4 100644 --- a/packages/calling/src/Contacts/ContactsClient.test.ts +++ b/packages/calling/src/Contacts/ContactsClient.test.ts @@ -9,6 +9,7 @@ import { SCIM_ENDPOINT_RESOURCE, SCIM_USER_FILTER, SUCCESS_MESSAGE, + WEBEX_API_BTS, } from '../common/constants'; import log from '../Logger'; import { @@ -59,7 +60,7 @@ describe('ContactClient Tests', () => { // eslint-disable-next-line no-underscore-dangle const contactServiceUrl = `${webex.internal.services._serviceUrls.contactsService}/${ENCRYPT_FILTER}/${USERS}/${CONTACT_FILTER}`; - const scimUrl = `${webex.internal.services._serviceUrls.identity}/${IDENTITY_ENDPOINT_RESOURCE}/${SCIM_ENDPOINT_RESOURCE}/${webex.internal.device.orgId}/${SCIM_USER_FILTER}id%20eq%20%22801bb994-343b-4f6b-97ae-d13c91d4b877%22`; + const scimUrl = `${WEBEX_API_BTS}/${IDENTITY_ENDPOINT_RESOURCE}/${SCIM_ENDPOINT_RESOURCE}/${webex.internal.device.orgId}/${SCIM_USER_FILTER}id%20eq%20%22801bb994-343b-4f6b-97ae-d13c91d4b877%22`; // eslint-disable-next-line no-underscore-dangle const contactServiceGroupUrl = `${webex.internal.services._serviceUrls.contactsService}/${ENCRYPT_FILTER}/${USERS}/${GROUP_FILTER}`; const serviceErrorCodeHandlerSpy = jest.spyOn(utils, 'serviceErrorCodeHandler'); diff --git a/packages/calling/src/common/Utils.test.ts b/packages/calling/src/common/Utils.test.ts index f35d45ef89b..06b70c2c098 100644 --- a/packages/calling/src/common/Utils.test.ts +++ b/packages/calling/src/common/Utils.test.ts @@ -52,6 +52,7 @@ import { INFER_ID_CONSTANT, SCIM_ENDPOINT_RESOURCE, SCIM_USER_FILTER, + WEBEX_API_BTS, } from './constants'; import {CALL_EVENT_KEYS} from '../Events/types'; @@ -913,7 +914,7 @@ describe('Voicemail Sorting Tests', () => { }); describe('resolveContact tests', () => { - const scimUrl = `${webex.internal.services._serviceUrls.identity}/${IDENTITY_ENDPOINT_RESOURCE}/${SCIM_ENDPOINT_RESOURCE}/${webex.internal.device.orgId}/${SCIM_USER_FILTER}`; + const scimUrl = `${WEBEX_API_BTS}/${IDENTITY_ENDPOINT_RESOURCE}/${SCIM_ENDPOINT_RESOURCE}/${webex.internal.device.orgId}/${SCIM_USER_FILTER}`; it('Invalid CallingPartyInfo', () => { const callingPartyInfo = {} as CallingPartyInfo; diff --git a/packages/calling/src/common/Utils.ts b/packages/calling/src/common/Utils.ts index 44e4577d902..5c5ee2434ff 100644 --- a/packages/calling/src/common/Utils.ts +++ b/packages/calling/src/common/Utils.ts @@ -113,6 +113,8 @@ import { IDENTITY_ENDPOINT_RESOURCE, SCIM_ENDPOINT_RESOURCE, SCIM_USER_FILTER, + WEBEX_API_PROD, + WEBEX_API_BTS, } from './constants'; import {Model, WebexSDK} from '../SDKConnector/types'; import SDKConnector from '../SDKConnector'; @@ -1184,7 +1186,10 @@ export async function scimQuery(filter: string) { const sdkConnector = SDKConnector; const webex = sdkConnector.getWebex(); - const scimUrl = `${webex.internal.services._serviceUrls.identity}/${IDENTITY_ENDPOINT_RESOURCE}/${SCIM_ENDPOINT_RESOURCE}/${webex.internal.device.orgId}/${SCIM_USER_FILTER}`; + const isProd = !webex.internal.device.url.includes('-int'); + const webexHost = isProd ? WEBEX_API_PROD : WEBEX_API_BTS; + + const scimUrl = `${webexHost}/${IDENTITY_ENDPOINT_RESOURCE}/${SCIM_ENDPOINT_RESOURCE}/${webex.internal.device.orgId}/${SCIM_USER_FILTER}`; const query = scimUrl + encodeURIComponent(filter); return (webex.request({ diff --git a/packages/calling/src/common/constants.ts b/packages/calling/src/common/constants.ts index 8166e6e25aa..a71f3de888a 100644 --- a/packages/calling/src/common/constants.ts +++ b/packages/calling/src/common/constants.ts @@ -44,3 +44,7 @@ export const UCM_CONNECTOR_FILE = 'UcmBackendConnector'; export const VOICEMAIL = 'VOICEMAIL'; export const SCIM_WEBEXIDENTITY_USER = 'urn:scim:schemas:extension:cisco:webexidentity:2.0:User'; export const SCIM_ENTERPRISE_USER = 'urn:ietf:params:scim:schemas:extension:enterprise:2.0:User'; +export const WEBEX_API_PROD = 'https://webexapis.com'; +export const WEBEX_API_BTS = 'https://integration.webexapis.com'; +export const WEBEX_API_CONFIG_INT_URL = `https://${WEBEX_API_BTS}/v1/uc/config`; +export const WEBEX_API_CONFIG_PROD_URL = `https://${WEBEX_API_PROD}/v1/uc/config`; From 8fa3283f9fabaf84624794a8fe4f9838f77a1edf Mon Sep 17 00:00:00 2001 From: Marcin Date: Mon, 16 Sep 2024 10:40:29 +0100 Subject: [PATCH 15/48] fix(meetings): empty webrtc dumps when user closes the browser early (#3830) --- .../plugin-meetings/src/rtcMetrics/index.ts | 18 ++++++++--- .../test/unit/spec/rtcMetrics/index.ts | 31 +++++++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/rtcMetrics/index.ts b/packages/@webex/plugin-meetings/src/rtcMetrics/index.ts index a86c3eb9f33..5206306842d 100644 --- a/packages/@webex/plugin-meetings/src/rtcMetrics/index.ts +++ b/packages/@webex/plugin-meetings/src/rtcMetrics/index.ts @@ -34,6 +34,8 @@ export default class RtcMetrics { connectionId: string; + initialMetricsSent: boolean; + /** * Initialize the interval. * @@ -47,9 +49,7 @@ export default class RtcMetrics { this.meetingId = meetingId; this.webex = webex; this.correlationId = correlationId; - this.setNewConnectionId(); - // Send the first set of metrics at 5 seconds in the case of a user leaving the call shortly after joining. - setTimeout(this.sendMetricsInQueue.bind(this), 5 * 1000); + this.resetConnection(); } /** @@ -79,6 +79,13 @@ export default class RtcMetrics { this.metricsQueue.push(data); + if (!this.initialMetricsSent && data.name === 'stats-report') { + // this is the first useful set of data (WCME gives it to us after 5s), send it out immediately + // in case the user is unhappy and closes the browser early + this.sendMetricsInQueue(); + this.initialMetricsSent = true; + } + try { // If a connection fails, send the rest of the metrics in queue and get a new connection id. const parsedPayload = parseJsonPayload(data.payload); @@ -88,7 +95,7 @@ export default class RtcMetrics { parsedPayload.value === 'failed' ) { this.sendMetricsInQueue(); - this.setNewConnectionId(); + this.resetConnection(); } } catch (e) { console.error(e); @@ -130,8 +137,9 @@ export default class RtcMetrics { * * @returns {void} */ - private setNewConnectionId() { + private resetConnection() { this.connectionId = uuid.v4(); + this.initialMetricsSent = false; } /** diff --git a/packages/@webex/plugin-meetings/test/unit/spec/rtcMetrics/index.ts b/packages/@webex/plugin-meetings/test/unit/spec/rtcMetrics/index.ts index 7a0ff4eb3c4..cdcc25f3430 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/rtcMetrics/index.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/rtcMetrics/index.ts @@ -120,4 +120,35 @@ describe('RtcMetrics', () => { metrics.addMetrics({ name: 'stats-report', payload: [STATS_WITH_IP] }); assert.calledOnce(anonymizeIpSpy); }) + + it('should send metrics on first stats-report', () => { + assert.callCount(webex.request, 0); + + metrics.addMetrics(FAKE_METRICS_ITEM); + assert.callCount(webex.request, 0); + + // first stats-report should trigger a call to webex.request + metrics.addMetrics({ name: 'stats-report', payload: [STATS_WITH_IP] }); + assert.callCount(webex.request, 1); + }); + + it('should send metrics on first stats-report after a new connection', () => { + assert.callCount(webex.request, 0); + + // first stats-report should trigger a call to webex.request + metrics.addMetrics({ name: 'stats-report', payload: [STATS_WITH_IP] }); + assert.callCount(webex.request, 1); + + // subsequent stats-report doesn't trigger it + metrics.addMetrics({ name: 'stats-report', payload: [STATS_WITH_IP] }); + assert.callCount(webex.request, 1); + + // now, simulate a failure - that triggers a new connection and upload of the metrics + metrics.addMetrics(FAILURE_METRICS_ITEM); + assert.callCount(webex.request, 2); + + // and another stats-report should trigger another upload of the metrics + metrics.addMetrics({ name: 'stats-report', payload: [STATS_WITH_IP] }); + assert.callCount(webex.request, 3); + }); }); From cd652adc63e66562f4dfd5747508bc9dcaa3d3ce Mon Sep 17 00:00:00 2001 From: RAM1232 <46831391+RAM1232@users.noreply.github.com> Date: Mon, 16 Sep 2024 16:38:44 +0530 Subject: [PATCH 16/48] docs(samples): add support for locus calls and resolve join button issue (#3804) --- docs/samples/browser-plugin-meetings/app.js | 21 ++++++++++++++++++- .../browser-plugin-meetings/index.html | 4 ++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/docs/samples/browser-plugin-meetings/app.js b/docs/samples/browser-plugin-meetings/app.js index 2e4db88b0eb..c3a0671785a 100644 --- a/docs/samples/browser-plugin-meetings/app.js +++ b/docs/samples/browser-plugin-meetings/app.js @@ -299,6 +299,7 @@ const passwordCaptchaStatusElm = document.querySelector('#password-captcha-statu const refreshCaptchaElm = document.querySelector('#meetings-join-captcha-refresh'); const verifyPasswordElm = document.querySelector('#btn-verify-password'); const displayMeetingStatusElm = document.querySelector('#display-meeting-status'); +const notes=document.querySelector('#notes'); const spaceIDError = `Using the space ID as a destination is no longer supported. Please refer to the migration guide to migrate to use the meeting ID or SIP address.`; const BNR = 'BNR'; const VBG = 'VBG'; @@ -381,6 +382,17 @@ createMeetingSelectElm.addEventListener('change', (event) => { } }); +createMeetingSelectElm.addEventListener('change', (event) => { + if (event.target.value === 'Others') { + notes.classList.remove('hidden'); + } + else { + notes.classList.add('hidden'); + + } +}); + + function createMeeting(e) { e.preventDefault(); @@ -427,6 +439,7 @@ function refreshCaptcha() { meetingsListElm.onclick = (e) => { selectedMeetingId = e.target.value; const meeting = webex.meetings.getAllMeetings()[selectedMeetingId]; + const selectedMeetingType = createMeetingSelectElm.options[createMeetingSelectElm.selectedIndex].innerText; if (meeting && meeting.passwordStatus === 'REQUIRED') { meetingsJoinPinElm.disabled = false; @@ -434,12 +447,18 @@ meetingsListElm.onclick = (e) => { document.getElementById('btn-join').disabled = true; document.getElementById('btn-join-media').disabled = true; } - else if (meeting && meeting.passwordStatus === 'UNKNOWN') { + else if (meeting && (meeting.passwordStatus === 'UNKNOWN' && selectedMeetingType === 'SIP URI')) { meetingsJoinPinElm.disabled = true; verifyPasswordElm.disabled = true; document.getElementById('btn-join').disabled = true; document.getElementById('btn-join-media').disabled = true; } + else if(meeting && (meeting.passwordStatus === 'UNKNOWN' && selectedMeetingType != 'SIP URI')) { + meetingsJoinPinElm.disabled = true; + verifyPasswordElm.disabled = true; + document.getElementById('btn-join').disabled = false; + document.getElementById('btn-join-media').disabled = false; + } else { meetingsJoinPinElm.disabled = true; verifyPasswordElm.disabled = true; diff --git a/docs/samples/browser-plugin-meetings/index.html b/docs/samples/browser-plugin-meetings/index.html index 1df78750757..a994bf11ed5 100644 --- a/docs/samples/browser-plugin-meetings/index.html +++ b/docs/samples/browser-plugin-meetings/index.html @@ -338,14 +338,14 @@

+ From 9bb81a2629d35642e4f177676263b1f5ba3beb20 Mon Sep 17 00:00:00 2001 From: Marcin Date: Mon, 16 Sep 2024 16:04:42 +0100 Subject: [PATCH 17/48] fix(meetings): webrtc dumps are sometimes empty when users join a meeting via lobby (#3836) --- .../@webex/plugin-meetings/src/media/index.ts | 14 ++--- .../plugin-meetings/src/meeting/index.ts | 12 +++-- .../plugin-meetings/src/rtcMetrics/index.ts | 20 ++++++-- .../test/unit/spec/media/index.ts | 40 ++++++++++++--- .../test/unit/spec/meeting/index.js | 51 ++++++++++++++++--- 5 files changed, 108 insertions(+), 29 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/media/index.ts b/packages/@webex/plugin-meetings/src/media/index.ts index a18040cdff8..eac32f8e533 100644 --- a/packages/@webex/plugin-meetings/src/media/index.ts +++ b/packages/@webex/plugin-meetings/src/media/index.ts @@ -104,9 +104,7 @@ Media.getDirection = (forceSendRecv: boolean, receive: boolean, send: boolean) = * * @param {boolean} isMultistream * @param {string} debugId string useful for debugging (will appear in media connection logs) - * @param {object} webex main `webex` object. * @param {string} meetingId id for the meeting using this connection - * @param {string} correlationId id used in requests to correlate to this session * @param {Object} options * @param {Object} [options.mediaProperties] contains mediaDirection and local tracks: * audioTrack, videoTrack, shareVideoTrack, and shareAudioTrack @@ -120,10 +118,9 @@ Media.getDirection = (forceSendRecv: boolean, receive: boolean, send: boolean) = Media.createMediaConnection = ( isMultistream: boolean, debugId: string, - webex: object, meetingId: string, - correlationId: string, options: { + rtcMetrics?: RtcMetrics; mediaProperties: { mediaDirection?: { receiveAudio: boolean; @@ -150,6 +147,7 @@ Media.createMediaConnection = ( } ) => { const { + rtcMetrics, mediaProperties, remoteQualityLevel, enableRtx, @@ -192,15 +190,13 @@ Media.createMediaConnection = ( config.bundlePolicy = bundlePolicy; } - const rtcMetrics = new RtcMetrics(webex, meetingId, correlationId); - return new MultistreamRoapMediaConnection( config, meetingId, /* the rtc metrics objects callbacks */ - (data) => rtcMetrics.addMetrics(data), - () => rtcMetrics.closeMetrics(), - () => rtcMetrics.sendMetricsInQueue() + (data) => rtcMetrics?.addMetrics(data), + () => rtcMetrics?.closeMetrics(), + () => rtcMetrics?.sendMetricsInQueue() ); } diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index 1b7b9099c4d..8a5d0ed05f2 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -155,6 +155,7 @@ import ControlsOptionsManager from '../controls-options-manager'; import PermissionError from '../common/errors/permission'; import {LocusMediaRequest} from './locusMediaRequest'; import {ConnectionStateHandler, ConnectionStateEvent} from './connectionStateHandler'; +import RtcMetrics from '../rtcMetrics'; // default callback so we don't call an undefined function, but in practice it should never be used const DEFAULT_ICE_PHASE_CALLBACK = () => 'JOIN_MEETING_FINAL'; @@ -696,6 +697,7 @@ export default class Meeting extends StatelessWebexPlugin { private connectionStateHandler?: ConnectionStateHandler; private iceCandidateErrors: Map; private iceCandidatesCount: number; + private rtcMetrics?: RtcMetrics; /** * @param {Object} attrs @@ -3156,6 +3158,7 @@ export default class Meeting extends StatelessWebexPlugin { options: {meetingId: this.id}, }); } + this.rtcMetrics?.sendNextMetrics(); this.updateLLMConnection(); }); @@ -6307,14 +6310,17 @@ export default class Meeting extends StatelessWebexPlugin { * @returns {RoapMediaConnection | MultistreamRoapMediaConnection} */ private async createMediaConnection(turnServerInfo, bundlePolicy?: BundlePolicy) { + this.rtcMetrics = this.isMultistream + ? // @ts-ignore + new RtcMetrics(this.webex, this.id, this.correlationId) + : undefined; + const mc = Media.createMediaConnection( this.isMultistream, this.getMediaConnectionDebugId(), - // @ts-ignore - this.webex, this.id, - this.correlationId, { + rtcMetrics: this.rtcMetrics, mediaProperties: this.mediaProperties, remoteQualityLevel: this.mediaProperties.remoteQualityLevel, // @ts-ignore - config coming from registerPlugin diff --git a/packages/@webex/plugin-meetings/src/rtcMetrics/index.ts b/packages/@webex/plugin-meetings/src/rtcMetrics/index.ts index 5206306842d..6e9f6790011 100644 --- a/packages/@webex/plugin-meetings/src/rtcMetrics/index.ts +++ b/packages/@webex/plugin-meetings/src/rtcMetrics/index.ts @@ -34,7 +34,7 @@ export default class RtcMetrics { connectionId: string; - initialMetricsSent: boolean; + shouldSendMetricsOnNextStatsReport: boolean; /** * Initialize the interval. @@ -64,6 +64,18 @@ export default class RtcMetrics { } } + /** + * Forces sending metrics when we get the next stats-report + * + * This is useful for cases when something important happens that affects the media connection, + * for example when we move from lobby into the meeting. + * + * @returns {void} + */ + public sendNextMetrics() { + this.shouldSendMetricsOnNextStatsReport = true; + } + /** * Add metrics items to the metrics queue. * @@ -79,11 +91,11 @@ export default class RtcMetrics { this.metricsQueue.push(data); - if (!this.initialMetricsSent && data.name === 'stats-report') { + if (this.shouldSendMetricsOnNextStatsReport && data.name === 'stats-report') { // this is the first useful set of data (WCME gives it to us after 5s), send it out immediately // in case the user is unhappy and closes the browser early this.sendMetricsInQueue(); - this.initialMetricsSent = true; + this.shouldSendMetricsOnNextStatsReport = false; } try { @@ -139,7 +151,7 @@ export default class RtcMetrics { */ private resetConnection() { this.connectionId = uuid.v4(); - this.initialMetricsSent = false; + this.shouldSendMetricsOnNextStatsReport = true; } /** diff --git a/packages/@webex/plugin-meetings/test/unit/spec/media/index.ts b/packages/@webex/plugin-meetings/test/unit/spec/media/index.ts index 8a28348a348..096577011de 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/media/index.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/media/index.ts @@ -3,14 +3,12 @@ import Media from '@webex/plugin-meetings/src/media/index'; import {assert} from '@webex/test-helper-chai'; import sinon from 'sinon'; import StaticConfig from '@webex/plugin-meetings/src/common/config'; -import MockWebex from '@webex/test-helper-mock-webex'; describe('createMediaConnection', () => { let clock; beforeEach(() => { clock = sinon.useFakeTimers(); }); - const webex = MockWebex(); const fakeRoapMediaConnection = { id: 'roap media connection', @@ -61,7 +59,7 @@ describe('createMediaConnection', () => { const ENABLE_EXTMAP = false; const ENABLE_RTX = true; - Media.createMediaConnection(false, 'some debug id', webex, 'meetingId', 'correlationId', { + Media.createMediaConnection(false, 'some debug id', 'meetingId', { mediaProperties: { mediaDirection: { sendAudio: false, @@ -139,7 +137,13 @@ describe('createMediaConnection', () => { .stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection') .returns(fakeRoapMediaConnection); - Media.createMediaConnection(true, 'some debug id', webex, 'meeting id', 'correlationId', { + const rtcMetrics = { + addMetrics: sinon.stub(), + closeMetrics: sinon.stub(), + sendMetricsInQueue: sinon.stub(), + }; + + Media.createMediaConnection(true, 'some debug id', 'meeting id', { mediaProperties: { mediaDirection: { sendAudio: true, @@ -150,6 +154,7 @@ describe('createMediaConnection', () => { receiveShare: true, }, }, + rtcMetrics, turnServerInfo: { url: 'turns:turn-server-url:443?transport=tcp', username: 'turn username', @@ -177,6 +182,27 @@ describe('createMediaConnection', () => { }, 'meeting id' ); + + // check if rtcMetrics callbacks are configured correctly + const addMetricsCallback = multistreamRoapMediaConnectionConstructorStub.getCalls()[0].args[2]; + const closeMetricsCallback = multistreamRoapMediaConnectionConstructorStub.getCalls()[0].args[3]; + const sendMetricsInQueueCallback = multistreamRoapMediaConnectionConstructorStub.getCalls()[0].args[4]; + + assert.isFunction(addMetricsCallback); + assert.isFunction(closeMetricsCallback); + assert.isFunction(sendMetricsInQueueCallback); + + const fakeMetricsData = {id: 'metrics data'}; + + addMetricsCallback(fakeMetricsData); + assert.calledOnceWithExactly(rtcMetrics.addMetrics, fakeMetricsData); + + closeMetricsCallback(); + assert.calledOnce(rtcMetrics.closeMetrics); + + sendMetricsInQueueCallback(); + assert.calledOnce(rtcMetrics.sendMetricsInQueue); + }); [ @@ -191,7 +217,7 @@ describe('createMediaConnection', () => { .stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection') .returns(fakeRoapMediaConnection); - Media.createMediaConnection(true, 'debug string', webex, 'meeting id', 'correlationId', { + Media.createMediaConnection(true, 'debug string', 'meeting id', { mediaProperties: { mediaDirection: { sendAudio: true, @@ -220,7 +246,7 @@ describe('createMediaConnection', () => { .stub(InternalMediaCoreModule, 'MultistreamRoapMediaConnection') .returns(fakeRoapMediaConnection); - Media.createMediaConnection(true, 'debug string', webex, 'meeting id', 'correlationId', { + Media.createMediaConnection(true, 'debug string', 'meeting id', { mediaProperties: { mediaDirection: { sendAudio: true, @@ -260,7 +286,7 @@ describe('createMediaConnection', () => { const ENABLE_EXTMAP = false; const ENABLE_RTX = true; - Media.createMediaConnection(false, 'some debug id', webex, 'meeting id', 'correlationId', { + Media.createMediaConnection(false, 'some debug id', 'meeting id', { mediaProperties: { mediaDirection: { sendAudio: true, diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js index 2a7e9eff420..85b5bd30df5 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js @@ -5,6 +5,8 @@ import 'jsdom-global/register'; import {cloneDeep, forEach, isEqual, isUndefined} from 'lodash'; import sinon from 'sinon'; import * as InternalMediaCoreModule from '@webex/internal-media-core'; +import * as RtcMetricsModule from '@webex/plugin-meetings/src/rtcMetrics'; +import * as RemoteMediaManagerModule from '@webex/plugin-meetings/src/multistream/remoteMediaManager'; import StateMachine from 'javascript-state-machine'; import uuid from 'uuid'; import {assert, expect} from '@webex/test-helper-chai'; @@ -2405,9 +2407,7 @@ describe('plugin-meetings', () => { Media.createMediaConnection, false, meeting.getMediaConnectionDebugId(), - webex, meeting.id, - meeting.correlationId, sinon.match({turnServerInfo: undefined}) ); assert.calledOnce(meeting.setMercuryListener); @@ -2449,6 +2449,44 @@ describe('plugin-meetings', () => { checkWorking({allowMediaInLobby: true}); }); + it('should create rtcMetrics and pass them to Media.createMediaConnection()', async () => { + const fakeRtcMetrics = {id: 'fake rtc metrics object'}; + const rtcMetricsCtor = sinon.stub(RtcMetricsModule, 'default').returns(fakeRtcMetrics); + + // setup the minimum mocks required for multistream connection + fakeMediaConnection.createSendSlot = sinon.stub().returns({ + publishStream: sinon.stub(), + unpublishStream: sinon.stub(), + setNamedMediaGroups: sinon.stub(), + }); + sinon.stub(RemoteMediaManagerModule, 'RemoteMediaManager').returns({ + start: sinon.stub().resolves(), + on: sinon.stub(), + logAllReceiveSlots: sinon.stub(), + }); + + meeting.meetingState = 'ACTIVE'; + meeting.isMultistream = true; + + await meeting.addMedia({ + mediaSettings: {}, + }); + + assert.calledOnceWithExactly(rtcMetricsCtor, webex, meeting.id, meeting.correlationId); + + // check that rtcMetrics was passed to Media.createMediaConnection + assert.calledOnce(Media.createMediaConnection); + assert.calledWith( + Media.createMediaConnection, + true, + meeting.getMediaConnectionDebugId(), + meeting.id, + sinon.match({ + rtcMetrics: fakeRtcMetrics, + }) + ); + }); + it('should pass the turn server info to the peer connection', async () => { const FAKE_TURN_URL = 'turns:webex.com:3478'; const FAKE_TURN_USER = 'some-turn-username'; @@ -2478,9 +2516,7 @@ describe('plugin-meetings', () => { Media.createMediaConnection, false, meeting.getMediaConnectionDebugId(), - webex, meeting.id, - meeting.correlationId, sinon.match({ turnServerInfo: { url: FAKE_TURN_URL, @@ -3401,9 +3437,7 @@ describe('plugin-meetings', () => { Media.createMediaConnection, false, meeting.getMediaConnectionDebugId(), - webex, meeting.id, - meeting.correlationId, sinon.match({ turnServerInfo: { url: FAKE_TURN_URL, @@ -8454,6 +8488,9 @@ describe('plugin-meetings', () => { it('listens to the self admitted guest event', (done) => { meeting.stopKeepAlive = sinon.stub(); meeting.updateLLMConnection = sinon.stub(); + meeting.rtcMetrics = { + sendNextMetrics: sinon.stub(), + }; meeting.locusInfo.emit({function: 'test', file: 'test'}, 'SELF_ADMITTED_GUEST', test1); assert.calledOnceWithExactly(meeting.stopKeepAlive); assert.calledThrice(TriggerProxy.trigger); @@ -8465,6 +8502,8 @@ describe('plugin-meetings', () => { {payload: test1} ); assert.calledOnce(meeting.updateLLMConnection); + assert.calledOnceWithExactly(meeting.rtcMetrics.sendNextMetrics); + done(); }); From 4c321d2e115c722d18713946b7565161f6aed3d4 Mon Sep 17 00:00:00 2001 From: chrisadubois Date: Mon, 16 Sep 2024 09:33:21 -0700 Subject: [PATCH 18/48] Upstreamsvcs (#3832) --- .../internal-plugin-device/src/config.js | 6 ++ .../internal-plugin-device/src/device.js | 27 ++++++++ .../test/unit/spec/device.js | 68 ++++++++++++++++++- 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/packages/@webex/internal-plugin-device/src/config.js b/packages/@webex/internal-plugin-device/src/config.js index 8f37c6dc6f8..598c604231e 100644 --- a/packages/@webex/internal-plugin-device/src/config.js +++ b/packages/@webex/internal-plugin-device/src/config.js @@ -56,6 +56,12 @@ export default { * @type {boolean} */ ephemeralDeviceTTL: 30 * 60, + + /** + * energyForcast + * @type {boolean} + */ + energyForecast: false, }, /** diff --git a/packages/@webex/internal-plugin-device/src/device.js b/packages/@webex/internal-plugin-device/src/device.js index 65a28673b7c..da8ba42d7ea 100644 --- a/packages/@webex/internal-plugin-device/src/device.js +++ b/packages/@webex/internal-plugin-device/src/device.js @@ -306,6 +306,14 @@ const Device = WebexPlugin.extend({ */ isReachabilityChecked: ['boolean', false, false], + /** + * This property stores whether or not the next refresh or register request should request energy forecast data + * in order to prevent over fetching energy forecasts + * + * @type {boolean} + */ + energyForecastConfig: 'boolean', + /** * This property stores whether or not the current device is in a meeting * to prevent an unneeded timeout of a meeting due to inactivity. @@ -344,6 +352,15 @@ const Device = WebexPlugin.extend({ this.webex.trigger('meeting ended'); }, + /** + * Set the value of energy forecast config for the current registered device. + * @param {boolean} [energyForecastConfig=false] - fetch an energy forecast on the next refresh/register + * @returns {void} + */ + setEnergyForecastConfig(energyForecastConfig = false) { + this.energyForecastConfig = energyForecastConfig; + }, + // Registration method members /* eslint-disable require-jsdoc */ @@ -395,6 +412,11 @@ const Device = WebexPlugin.extend({ uri: this.url, body, headers, + qs: { + includeUpstreamServices: `all${ + this.config.energyForecast && this.energyForecastConfig ? ',energyforecast' : '' + }`, + }, }) .then((response) => this.processRegistrationSuccess(response)) .catch((reason) => { @@ -464,6 +486,11 @@ const Device = WebexPlugin.extend({ resource: 'devices', body, headers, + qs: { + includeUpstreamServices: `all${ + this.config.energyForecast && this.energyForecastConfig ? ',energyforecast' : '' + }`, + }, }) .catch((error) => { this.webex.internal.newMetrics.submitInternalEvent({ diff --git a/packages/@webex/internal-plugin-device/test/unit/spec/device.js b/packages/@webex/internal-plugin-device/test/unit/spec/device.js index 9c00a8b18c1..53d781c268c 100644 --- a/packages/@webex/internal-plugin-device/test/unit/spec/device.js +++ b/packages/@webex/internal-plugin-device/test/unit/spec/device.js @@ -176,11 +176,14 @@ describe('plugin-device', () => { describe('#refresh()', () => { let requestSpy; - const setup = () => { + const setup = (config = {}) => { sinon.stub(device, 'canRegister').callsFake(() => Promise.resolve()); sinon.stub(device, 'processRegistrationSuccess').callsFake(() => {}); requestSpy = sinon.spy(device, 'request'); device.config.defaults = {}; + Object.keys((config)).forEach((key) => { + device.config[key] = config[key]; + }); device.set('registered', true); }; @@ -207,15 +210,40 @@ describe('plugin-device', () => { assert.deepEqual(requestSpy.args[0][0].headers, {}); }); + + it('uses the energy forecast config to append upstream services to the outgoing call', async () => { + setup({energyForecast: true}); + device.setEnergyForecastConfig(true); + + await device.register(); + + assert.calledWith(requestSpy, sinon.match({ + qs: { includeUpstreamServices: 'all,energyforecast' } + })) + }); + + it('uses the energy forecast config to not append upstream services to the outgoing call', async () => { + setup({energyForecast: true}); + device.setEnergyForecastConfig(false); + + await device.register(); + + assert.calledWith(requestSpy, sinon.match({ + qs: { includeUpstreamServices: 'all' } + })) + }); }); describe('#register()', () => { - const setup = () => { + const setup = (config = {}) => { webex.internal.metrics.submitClientMetrics = sinon.stub(); sinon.stub(device, 'processRegistrationSuccess').callsFake(() => {}); device.config.defaults = {}; + Object.keys(config).forEach((key) => { + device.config[key] = config[key]; + }); device.set('registered', false); }; @@ -284,6 +312,42 @@ describe('plugin-device', () => { }); + it('uses the energy forecast config to append upstream services to the outgoing call', async () => { + setup({energyForecast: true}); + sinon.stub(device, 'canRegister').callsFake(() => Promise.resolve()); + const spy = sinon.spy(device, 'request'); + device.setEnergyForecastConfig(true); + + await device.register(); + + assert.calledWith(spy, { + method: 'POST', + service: 'wdm', + resource: 'devices', + body: {}, + headers: {}, + qs: { includeUpstreamServices: 'all,energyforecast' } + } ) + }); + + it('uses the energy forecast config to not append upstream services to the outgoing call', async () => { + setup({energyForecast: true}); + sinon.stub(device, 'canRegister').callsFake(() => Promise.resolve()); + const spy = sinon.spy(device, 'request'); + device.setEnergyForecastConfig(false); + + await device.register(); + + assert.calledWith(spy, { + method: 'POST', + service: 'wdm', + resource: 'devices', + body: {}, + headers: {}, + qs: { includeUpstreamServices: 'all' } + } ) + }); + }); describe('#processRegistrationSuccess()', () => { From d927e6f6a0c46a8d2ece9100f5a5f57366d1125b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Wa=C5=9Bniowski?= Date: Tue, 17 Sep 2024 10:07:26 +0200 Subject: [PATCH 19/48] fix(plugin-meetings): add retry mechanism to fetching clusters (#3813) --- .../plugin-meetings/src/reachability/index.ts | 30 ++++++++- .../test/unit/spec/reachability/index.ts | 67 ++++++++++++++++++- 2 files changed, 92 insertions(+), 5 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/reachability/index.ts b/packages/@webex/plugin-meetings/src/reachability/index.ts index 7f67e44ff74..c51243e0024 100644 --- a/packages/@webex/plugin-meetings/src/reachability/index.ts +++ b/packages/@webex/plugin-meetings/src/reachability/index.ts @@ -114,6 +114,32 @@ export default class Reachability extends EventsScope { this.clusterReachability = {}; } + /** + * Fetches the list of media clusters from the backend + * @param {boolean} isRetry + * @private + * @returns {Promise<{clusters: ClusterList, joinCookie: any}>} + */ + async getClusters(isRetry = false): Promise<{clusters: ClusterList; joinCookie: any}> { + try { + const {clusters, joinCookie} = await this.reachabilityRequest.getClusters( + MeetingUtil.getIpVersion(this.webex) + ); + + return {clusters, joinCookie}; + } catch (error) { + if (isRetry) { + throw error; + } + + LoggerProxy.logger.error( + `Reachability:index#getClusters --> Failed with error: ${error}, retrying...` + ); + + return this.getClusters(true); + } + } + /** * Gets a list of media clusters from the backend and performs reachability checks on all the clusters * @returns {Promise} reachability results @@ -123,9 +149,7 @@ export default class Reachability extends EventsScope { public async gatherReachability(): Promise { // Fetch clusters and measure latency try { - const {clusters, joinCookie} = await this.reachabilityRequest.getClusters( - MeetingUtil.getIpVersion(this.webex) - ); + const {clusters, joinCookie} = await this.getClusters(); // @ts-ignore await this.webex.boundedStorage.put( diff --git a/packages/@webex/plugin-meetings/test/unit/spec/reachability/index.ts b/packages/@webex/plugin-meetings/test/unit/spec/reachability/index.ts index 8be4ce92bb2..e243f4c5199 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/reachability/index.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/reachability/index.ts @@ -7,7 +7,7 @@ import Reachability, { ReachabilityResults, ReachabilityResultsForBackend, } from '@webex/plugin-meetings/src/reachability/'; -import { ClusterNode } from '../../../../src/reachability/request'; +import {ClusterNode} from '../../../../src/reachability/request'; import MeetingUtil from '@webex/plugin-meetings/src/meeting/util'; import * as ClusterReachabilityModule from '@webex/plugin-meetings/src/reachability/clusterReachability'; import Metrics from '@webex/plugin-meetings/src/metrics'; @@ -145,7 +145,6 @@ describe('isAnyPublicClusterReachable', () => { }); }); - describe('isWebexMediaBackendUnreachable', () => { let webex; @@ -1466,6 +1465,70 @@ describe('gatherReachability', () => { xtls: [], // empty list because TLS is disabled in config }); }); + + it('retry of getClusters is succesfull', async () => { + webex.config.meetings.experimental = { + enableTcpReachability: true, + enableTlsReachability: false, + }; + + const getClustersResult = { + clusters: { + 'cluster name': { + udp: ['testUDP1', 'testUDP2'], + tcp: ['testTCP1', 'testTCP2'], + xtls: ['testXTLS1', 'testXTLS2'], + isVideoMesh: false, + }, + }, + joinCookie: {id: 'id'}, + }; + + const reachability = new Reachability(webex); + + let getClustersCallCount = 0; + + reachability.reachabilityRequest.getClusters = sinon.stub().callsFake(() => { + getClustersCallCount++; + + if (getClustersCallCount == 1) { + throw new Error('fake error'); + } + + return getClustersResult; + }); + + const promise = reachability.gatherReachability(); + + await simulateTimeout(); + await promise; + + assert.equal(getClustersCallCount, 2); + + assert.calledOnce(clusterReachabilityCtorStub); + }); + + it('two failed calls to getClusters', async () => { + const reachability = new Reachability(webex); + + let getClustersCallCount = 0; + + reachability.reachabilityRequest.getClusters = sinon.stub().callsFake(() => { + getClustersCallCount++; + + throw new Error('fake error'); + }); + + const promise = reachability.gatherReachability(); + + await simulateTimeout(); + + await promise; + + assert.equal(getClustersCallCount, 2); + + assert.neverCalledWith(clusterReachabilityCtorStub); + }); }); describe('getReachabilityResults', () => { From 11f97f5b80888b8c6eb6aea3f72b33c14d620bc2 Mon Sep 17 00:00:00 2001 From: Marcin Date: Tue, 17 Sep 2024 16:24:40 +0100 Subject: [PATCH 20/48] feat(meetings): do ip version detection in parallel with reachability (#3838) --- .../plugin-meetings/src/reachability/index.ts | 15 +++ .../test/unit/spec/reachability/index.ts | 99 ++++++++++++++++++- 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/packages/@webex/plugin-meetings/src/reachability/index.ts b/packages/@webex/plugin-meetings/src/reachability/index.ts index c51243e0024..17b60313ab0 100644 --- a/packages/@webex/plugin-meetings/src/reachability/index.ts +++ b/packages/@webex/plugin-meetings/src/reachability/index.ts @@ -149,6 +149,11 @@ export default class Reachability extends EventsScope { public async gatherReachability(): Promise { // Fetch clusters and measure latency try { + // kick off ip version detection. For now we don't await it, as we're doing it + // to gather the timings and send them with our reachability metrics + // @ts-ignore + this.webex.internal.device.ipNetworkDetector.detect(); + const {clusters, joinCookie} = await this.getClusters(); // @ts-ignore @@ -537,6 +542,16 @@ export default class Reachability extends EventsScope { tcp: this.getStatistics(results, 'tcp', false), xtls: this.getStatistics(results, 'xtls', false), }, + ipver: { + // @ts-ignore + firstIpV4: this.webex.internal.device.ipNetworkDetector.firstIpV4, + // @ts-ignore + firstIpV6: this.webex.internal.device.ipNetworkDetector.firstIpV6, + // @ts-ignore + firstMdns: this.webex.internal.device.ipNetworkDetector.firstMdns, + // @ts-ignore + totalTime: this.webex.internal.device.ipNetworkDetector.totalTime, + }, }; Metrics.sendBehavioralMetric( BEHAVIORAL_METRICS.REACHABILITY_COMPLETED, diff --git a/packages/@webex/plugin-meetings/test/unit/spec/reachability/index.ts b/packages/@webex/plugin-meetings/test/unit/spec/reachability/index.ts index e243f4c5199..2e400817b5e 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/reachability/index.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/reachability/index.ts @@ -485,6 +485,16 @@ describe('gatherReachability', () => { JSON.stringify({old: 'joinCookie'}) ); + webex.internal.device.ipNetworkDetector = { + supportsIpV4: false, + supportsIpV6: false, + firstIpV4: -1, + firstIpV6: -1, + firstMdns: -1, + totalTime: -1, + detect: sinon.stub().resolves(), + }; + clock = sinon.useFakeTimers(); mockClusterReachabilityInstances = {}; @@ -1034,6 +1044,15 @@ describe('gatherReachability', () => { enableTlsReachability: true, }; + // the metrics related to ipver are not tested in these tests and are all the same, so setting them up here + const expectedMetricsFull = { + ...expectedMetrics, + ipver_firstIpV4: -1, + ipver_firstIpV6: -1, + ipver_firstMdns: -1, + ipver_totalTime: -1, + }; + const receivedEvents = { done: 0, firstResultAvailable: { @@ -1118,11 +1137,89 @@ describe('gatherReachability', () => { assert.calledWith( Metrics.sendBehavioralMetric, 'js_sdk_reachability_completed', - expectedMetrics + expectedMetricsFull ); }) ); + it(`starts ip network version detection and includes the results in the metrics`, async () => { + webex.config.meetings.experimental = { + enableTcpReachability: true, + enableTlsReachability: true, + }; + webex.internal.device.ipNetworkDetector = { + supportsIpV4: true, + supportsIpV6: true, + firstIpV4: 10, + firstIpV6: 20, + firstMdns: 30, + totalTime: 40, + detect: sinon.stub().resolves(), + }; + + const receivedEvents = { + done: 0, + }; + + const reachability = new Reachability(webex); + + reachability.on('reachability:done', () => { + receivedEvents.done += 1; + }); + + // simulate having just 1 cluster, we don't need more for this test + reachability.reachabilityRequest.getClusters = sinon.stub().returns({ + clusters: { + publicCluster: { + udp: ['udp-url'], + tcp: [], + xtls: [], + isVideoMesh: false, + }, + }, + joinCookie: {id: 'id'}, + }); + + const resultPromise = reachability.gatherReachability(); + + await testUtils.flushPromises(); + + // trigger mock result events from ClusterReachability instance + mockClusterReachabilityInstances['publicCluster'].emitFakeResult('udp', { + result: 'reachable', + clientMediaIPs: ['1.2.3.4'], + latencyInMilliseconds: 100, + }); + + await resultPromise; + + // check events emitted by Reachability class + assert.equal(receivedEvents['done'], 1); + + // and that ip network detection was started + assert.calledOnceWithExactly(webex.internal.device.ipNetworkDetector.detect); + + // finally, check the metrics - they should contain values from ipNetworkDetector + assert.calledWith(Metrics.sendBehavioralMetric, 'js_sdk_reachability_completed', { + vmn_udp_min: -1, + vmn_udp_max: -1, + vmn_udp_average: -1, + public_udp_min: 100, + public_udp_max: 100, + public_udp_average: 100, + public_tcp_min: -1, + public_tcp_max: -1, + public_tcp_average: -1, + public_xtls_min: -1, + public_xtls_max: -1, + public_xtls_average: -1, + ipver_firstIpV4: webex.internal.device.ipNetworkDetector.firstIpV4, + ipver_firstIpV6: webex.internal.device.ipNetworkDetector.firstIpV6, + ipver_firstMdns: webex.internal.device.ipNetworkDetector.firstMdns, + ipver_totalTime: webex.internal.device.ipNetworkDetector.totalTime, + }); + }); + it('keeps updating reachability results after the 3s public cloud timeout expires', async () => { webex.config.meetings.experimental = { enableTcpReachability: true, From c04a29f0ef5fe4f2980a443391fae59244692f55 Mon Sep 17 00:00:00 2001 From: Rajesh Kumar <131742425+rarajes2@users.noreply.github.com> Date: Wed, 18 Sep 2024 10:01:47 +0530 Subject: [PATCH 21/48] feat(calling): update failover logic for cc (#3805) --- .../calling/src/CallingClient/constants.ts | 1 + .../registration/register.test.ts | 107 +++++++++++++++++- .../CallingClient/registration/register.ts | 19 +++- 3 files changed, 118 insertions(+), 9 deletions(-) diff --git a/packages/calling/src/CallingClient/constants.ts b/packages/calling/src/CallingClient/constants.ts index 3e325e2c454..c3b8cc87fc4 100644 --- a/packages/calling/src/CallingClient/constants.ts +++ b/packages/calling/src/CallingClient/constants.ts @@ -107,6 +107,7 @@ export const SEC_TO_MSEC_MFACTOR = 1000; export const MINUTES_TO_SEC_MFACTOR = 60; export const REG_RANDOM_T_FACTOR_UPPER_LIMIT = 10000; export const REG_TRY_BACKUP_TIMER_VAL_IN_SEC = 1200; +export const REG_TRY_BACKUP_TIMER_VAL_FOR_CC_IN_SEC = 114; export const REG_FAILBACK_429_MAX_RETRIES = 5; export const REGISTER_UTIL = 'registerDevice'; export const GET_MOBIUS_SERVERS_UTIL = 'getMobiusServers'; diff --git a/packages/calling/src/CallingClient/registration/register.test.ts b/packages/calling/src/CallingClient/registration/register.test.ts index 0f0da477562..8283b073b86 100644 --- a/packages/calling/src/CallingClient/registration/register.test.ts +++ b/packages/calling/src/CallingClient/registration/register.test.ts @@ -22,6 +22,7 @@ import { KEEPALIVE_UTIL, MINUTES_TO_SEC_MFACTOR, REGISTRATION_FILE, + REG_TRY_BACKUP_TIMER_VAL_FOR_CC_IN_SEC, REG_TRY_BACKUP_TIMER_VAL_IN_SEC, SEC_TO_MSEC_MFACTOR, } from '../constants'; @@ -64,6 +65,17 @@ describe('Registration Tests', () => { }, }; + const ccMockResponse = { + ...mockResponse, + body: { + ...mockResponse.body, + serviceData: { + domain: '', + indicator: 'contactcenter', + }, + }, + }; + const failurePayload = ({ statusCode: 500, body: mockPostResponse, @@ -85,15 +97,19 @@ describe('Registration Tests', () => { let restoreSpy; let postRegistrationSpy; - beforeEach(() => { + const setupRegistration = (mockServiceData) => { const mutex = new Mutex(); - reg = createRegistration(webex, MockServiceData, mutex, lineEmitter, LOGGER.INFO); + reg = createRegistration(webex, mockServiceData, mutex, lineEmitter, LOGGER.INFO); reg.setMobiusServers(mobiusUris.primary, mobiusUris.backup); jest.clearAllMocks(); restartSpy = jest.spyOn(reg, 'restartRegistration'); failbackRetry429Spy = jest.spyOn(reg, FAILBACK_429_RETRY_UTIL); restoreSpy = jest.spyOn(reg, 'restorePreviousRegistration'); postRegistrationSpy = jest.spyOn(reg, 'postRegistration'); + }; + + beforeEach(() => { + setupRegistration(MockServiceData); }); afterEach(() => { @@ -218,6 +234,36 @@ describe('Registration Tests', () => { expect(reg.getActiveMobiusUrl()).toEqual(mobiusUris.backup[0]); }); + it('cc: verify unreachable primary with reachable backup server', async () => { + setupRegistration({...MockServiceData, indicator: ServiceIndicator.CONTACT_CENTER}); + + jest.useFakeTimers(); + webex.request + .mockRejectedValueOnce(failurePayload) + .mockRejectedValueOnce(failurePayload) + .mockResolvedValueOnce(successPayload); + + expect(reg.getStatus()).toEqual(RegistrationStatus.IDLE); + await reg.triggerRegistration(); + jest.advanceTimersByTime(REG_TRY_BACKUP_TIMER_VAL_FOR_CC_IN_SEC * SEC_TO_MSEC_MFACTOR); + await flushPromises(); + + expect(webex.request).toBeCalledTimes(3); + expect(webex.request).toBeCalledWith({ + ...ccMockResponse, + method: 'POST', + uri: `${mobiusUris.primary[0]}device`, + }); + expect(webex.request).toBeCalledWith({ + ...ccMockResponse, + method: 'POST', + uri: `${mobiusUris.backup[0]}device`, + }); + expect(reg.getStatus()).toEqual(RegistrationStatus.ACTIVE); + /* Active Url must match with the backup url as per the test */ + expect(reg.getActiveMobiusUrl()).toEqual(mobiusUris.backup[0]); + }); + it('verify unreachable primary and backup servers', async () => { jest.useFakeTimers(); // try the primary twice and register successfully with backup servers @@ -444,15 +490,14 @@ describe('Registration Tests', () => { file: REGISTRATION_FILE, method: 'startKeepaliveTimer', }; - const mockKeepAliveBody = {device: mockPostResponse.device}; - beforeEach(async () => { + const beforeEachSetupForKeepalive = async () => { postRegistrationSpy.mockResolvedValueOnce(successPayload); jest.useFakeTimers(); await reg.triggerRegistration(); expect(reg.getStatus()).toBe(RegistrationStatus.ACTIVE); - }); + }; afterEach(() => { jest.clearAllTimers(); @@ -471,6 +516,7 @@ describe('Registration Tests', () => { }); it('verify successful keep-alive cases', async () => { + await beforeEachSetupForKeepalive(); const keepAlivePayload = ({ statusCode: 200, body: mockKeepAliveBody, @@ -487,6 +533,7 @@ describe('Registration Tests', () => { }); it('verify failure keep-alive cases: Retry Success', async () => { + await beforeEachSetupForKeepalive(); const failurePayload = ({ statusCode: 503, body: mockKeepAliveBody, @@ -517,6 +564,7 @@ describe('Registration Tests', () => { }); it('verify failure keep-alive cases: Restore failure', async () => { + await beforeEachSetupForKeepalive(); const restoreSpy = jest.spyOn(reg, 'restorePreviousRegistration'); const restartRegSpy = jest.spyOn(reg, 'restartRegistration'); const reconnectSpy = jest.spyOn(reg, 'reconnectOnFailure'); @@ -565,6 +613,7 @@ describe('Registration Tests', () => { }); it('verify failure keep-alive cases: Restore Success', async () => { + await beforeEachSetupForKeepalive(); const restoreSpy = jest.spyOn(reg, 'restorePreviousRegistration'); const restartRegSpy = jest.spyOn(reg, 'restartRegistration'); const reconnectSpy = jest.spyOn(reg, 'reconnectOnFailure'); @@ -616,6 +665,7 @@ describe('Registration Tests', () => { }); it('verify failure followed by recovery of keepalive', async () => { + await beforeEachSetupForKeepalive(); const failurePayload = ({ statusCode: 503, body: mockKeepAliveBody, @@ -647,7 +697,53 @@ describe('Registration Tests', () => { expect(reg.keepaliveTimer).toBe(timer); }); + it('cc: verify failover to backup server after 4 keep alive failure with primary server', async () => { + // Register with contact center service + setupRegistration({...MockServiceData, indicator: ServiceIndicator.CONTACT_CENTER}); + await beforeEachSetupForKeepalive(); + + const failurePayload = ({ + statusCode: 503, + body: mockKeepAliveBody, + }); + const successPayload = ({ + statusCode: 200, + body: mockKeepAliveBody, + }); + + const clearIntervalSpy = jest.spyOn(global, 'clearInterval'); + + jest + .spyOn(reg, 'postKeepAlive') + .mockRejectedValueOnce(failurePayload) + .mockRejectedValueOnce(failurePayload) + .mockRejectedValueOnce(failurePayload) + .mockRejectedValueOnce(failurePayload) + .mockResolvedValue(successPayload); + + expect(reg.getStatus()).toBe(RegistrationStatus.ACTIVE); + + const timer = reg.keepaliveTimer; + + jest.advanceTimersByTime(5 * mockPostResponse.keepaliveInterval * SEC_TO_MSEC_MFACTOR); + await flushPromises(); + + expect(clearIntervalSpy).toBeCalledOnceWith(timer); + expect(reg.getStatus()).toBe(RegistrationStatus.INACTIVE); + expect(reg.keepaliveTimer).not.toBe(timer); + + webex.request.mockRejectedValueOnce(failurePayload).mockResolvedValue(successPayload); + + jest.advanceTimersByTime(REG_TRY_BACKUP_TIMER_VAL_FOR_CC_IN_SEC * SEC_TO_MSEC_MFACTOR); + await flushPromises(); + + /* Active Url must match with the backup url as per the test */ + expect(reg.getActiveMobiusUrl()).toEqual(mobiusUris.backup[0]); + expect(reg.getStatus()).toBe(RegistrationStatus.ACTIVE); + }); + it('verify final error for keep-alive', async () => { + await beforeEachSetupForKeepalive(); const restoreSpy = jest.spyOn(reg, 'restorePreviousRegistration'); const restartRegSpy = jest.spyOn(reg, 'restartRegistration'); const reconnectSpy = jest.spyOn(reg, 'reconnectOnFailure'); @@ -686,6 +782,7 @@ describe('Registration Tests', () => { }); it('verify failure keep-alive case with active call present: Restore Success after call ends', async () => { + await beforeEachSetupForKeepalive(); const restoreSpy = jest.spyOn(reg, 'restorePreviousRegistration'); const restartRegSpy = jest.spyOn(reg, 'restartRegistration'); const reconnectSpy = jest.spyOn(reg, 'reconnectOnFailure'); diff --git a/packages/calling/src/CallingClient/registration/register.ts b/packages/calling/src/CallingClient/registration/register.ts index 862f827300e..297c8fef62e 100644 --- a/packages/calling/src/CallingClient/registration/register.ts +++ b/packages/calling/src/CallingClient/registration/register.ts @@ -17,6 +17,7 @@ import { IDeviceInfo, RegistrationStatus, ServiceData, + ServiceIndicator, WebexRequestPayload, } from '../../common/types'; import {ISDKConnector, WebexSDK} from '../../SDKConnector/types'; @@ -39,6 +40,7 @@ import { DEFAULT_REHOMING_INTERVAL_MIN, DEFAULT_REHOMING_INTERVAL_MAX, DEFAULT_KEEPALIVE_INTERVAL, + REG_TRY_BACKUP_TIMER_VAL_FOR_CC_IN_SEC, } from '../constants'; import {LINE_EVENTS, LineEmitterCallback} from '../line/types'; import {LineError} from '../../Errors/catalog/LineError'; @@ -73,6 +75,7 @@ export class Registration implements IRegistration { private backupMobiusUris: string[]; private registerRetry = false; private reconnectPending = false; + private isCCFlow = false; /** */ @@ -85,6 +88,8 @@ export class Registration implements IRegistration { ) { this.sdkConnector = SDKConnector; this.serviceData = serviceData; + this.isCCFlow = serviceData.indicator === ServiceIndicator.CONTACT_CENTER; + if (!this.sdkConnector.getWebex()) { SDKConnector.setWebex(webex); } @@ -257,8 +262,12 @@ export class Registration implements IRegistration { let interval = this.getRegRetryInterval(attempt); - if (timeElapsed + interval > REG_TRY_BACKUP_TIMER_VAL_IN_SEC) { - const excessVal = timeElapsed + interval - REG_TRY_BACKUP_TIMER_VAL_IN_SEC; + const TIMER_THRESHOLD = this.isCCFlow + ? REG_TRY_BACKUP_TIMER_VAL_FOR_CC_IN_SEC + : REG_TRY_BACKUP_TIMER_VAL_IN_SEC; + + if (timeElapsed + interval > TIMER_THRESHOLD) { + const excessVal = timeElapsed + interval - TIMER_THRESHOLD; interval -= excessVal; } @@ -681,13 +690,15 @@ export class Registration implements IRegistration { private startKeepaliveTimer(url: string, interval: number) { let keepAliveRetryCount = 0; this.clearKeepaliveTimer(); + const RETRY_COUNT_THRESHOLD = this.isCCFlow ? 4 : 5; + this.keepaliveTimer = setInterval(async () => { const logContext = { file: REGISTRATION_FILE, method: this.startKeepaliveTimer.name, }; await this.mutex.runExclusive(async () => { - if (this.isDeviceRegistered() && keepAliveRetryCount < 5) { + if (this.isDeviceRegistered() && keepAliveRetryCount < RETRY_COUNT_THRESHOLD) { try { const res = await this.postKeepAlive(url); log.info(`Sent Keepalive, status: ${res.statusCode}`, logContext); @@ -720,7 +731,7 @@ export class Registration implements IRegistration { {method: this.startKeepaliveTimer.name, file: REGISTRATION_FILE} ); - if (abort || keepAliveRetryCount >= 5) { + if (abort || keepAliveRetryCount >= RETRY_COUNT_THRESHOLD) { this.setStatus(RegistrationStatus.INACTIVE); this.clearKeepaliveTimer(); this.clearFailbackTimer(); From e42b39053f93f453f497aac0c6b1bdf16fc0e672 Mon Sep 17 00:00:00 2001 From: akulakum <74420487+akulakum@users.noreply.github.com> Date: Wed, 18 Sep 2024 12:13:24 +0530 Subject: [PATCH 22/48] feat(callhistory): add-support-for-multiline-in-callhistory (#3816) --- .../src/CallHistory/CallHistory.test.ts | 115 ++++++++- .../calling/src/CallHistory/CallHistory.ts | 95 ++++++- .../src/CallHistory/callHistoryFixtures.ts | 236 +++++++++++++++++- packages/calling/src/CallHistory/constants.ts | 8 +- packages/calling/src/CallHistory/types.ts | 16 +- packages/calling/src/Events/types.ts | 30 ++- packages/calling/src/common/Utils.ts | 7 +- 7 files changed, 497 insertions(+), 10 deletions(-) diff --git a/packages/calling/src/CallHistory/CallHistory.test.ts b/packages/calling/src/CallHistory/CallHistory.test.ts index 5efa59d50b8..dadfbf0541a 100644 --- a/packages/calling/src/CallHistory/CallHistory.test.ts +++ b/packages/calling/src/CallHistory/CallHistory.test.ts @@ -3,7 +3,7 @@ /* eslint-disable @typescript-eslint/no-shadow */ import {LOGGER} from '../Logger/types'; import {getTestUtilsWebex} from '../common/testUtil'; -import {HTTP_METHODS, SORT, SORT_BY, WebexRequestPayload} from '../common/types'; +import {CALLING_BACKEND, HTTP_METHODS, SORT, SORT_BY, WebexRequestPayload} from '../common/types'; import {CallHistory, createCallHistoryClient} from './CallHistory'; import {ICallHistory} from './types'; import { @@ -16,6 +16,10 @@ import { janusSetReadStateUrl, ERROR_DETAILS_401, ERROR_DETAILS_400, + MOCK_LINES_API_CALL_RESPONSE, + MOCK_LINES_API_CALL_RESPONSE_WITH_NO_LINEDATA, + MOCK_CALL_HISTORY_WITH_UCM_LINE_NUMBER, + MOCK_CALL_HISTORY_WITHOUT_UCM_LINE_NUMBER, } from './callHistoryFixtures'; import { COMMON_EVENT_KEYS, @@ -247,4 +251,113 @@ describe('Call history tests', () => { ); }); }); + + describe('fetchUCMLinesData test', () => { + it('verify successful UCM lines API case', async () => { + const ucmLinesAPIPayload = (MOCK_LINES_API_CALL_RESPONSE); + + webex.request.mockResolvedValue(ucmLinesAPIPayload); + const response = await callHistory['fetchUCMLinesData'](); + + expect(response.statusCode).toBe(200); + expect(response.message).toBe('SUCCESS'); + }); + + it('verify bad request failed UCM lines API case', async () => { + const failurePayload = { + statusCode: 400, + }; + const ucmLinesAPIPayload = (failurePayload); + + webex.request.mockRejectedValue(ucmLinesAPIPayload); + const response = await callHistory['fetchUCMLinesData'](); + + expect(response).toStrictEqual(ERROR_DETAILS_400); + expect(response.data.error).toEqual(ERROR_DETAILS_400.data.error); + expect(response.statusCode).toBe(400); + expect(response.message).toBe('FAILURE'); + expect(serviceErrorCodeHandlerSpy).toHaveBeenCalledWith( + {statusCode: 400}, + {file: 'CallHistory', method: 'fetchLinesData'} + ); + }); + + it('should call fetchUCMLinesData when calling backend is UCM and userSessions contain valid cucmDN', async () => { + jest.spyOn(utils, 'getCallingBackEnd').mockReturnValue(CALLING_BACKEND.UCM); + // Since fetchUCMLinesData is a private method, TypeScript restricts direct access to it. + // To bypass this restriction, we are using 'as any' to access and invoke the method for testing purposes. + const fetchUCMLinesDataSpy = jest + .spyOn(callHistory as any, 'fetchUCMLinesData') + .mockResolvedValue(MOCK_LINES_API_CALL_RESPONSE); + + const mockCallHistoryPayload = ( + (MOCK_CALL_HISTORY_WITH_UCM_LINE_NUMBER) + ); + webex.request.mockResolvedValue(mockCallHistoryPayload); + + const response = await callHistory.getCallHistoryData(7, 10, SORT.DEFAULT, SORT_BY.DEFAULT); + + expect(fetchUCMLinesDataSpy).toHaveBeenCalledTimes(1); + + expect(response.statusCode).toBe(200); + expect( + response.data.userSessions && response.data.userSessions[0].self.ucmLineNumber + ).toEqual(1); + }); + + it('should fetchUCMLinesData but not assign ucmLineNumber when UCM backend has no line data', async () => { + jest.spyOn(utils, 'getCallingBackEnd').mockReturnValue(CALLING_BACKEND.UCM); + + // Since fetchUCMLinesData is a private method, TypeScript restricts direct access to it. + // To bypass this restriction, we are using 'as any' to access and invoke the method for testing purposes. + const fetchUCMLinesDataSpy = jest + .spyOn(callHistory as any, 'fetchUCMLinesData') + .mockResolvedValue(MOCK_LINES_API_CALL_RESPONSE_WITH_NO_LINEDATA); + + const mockCallHistoryPayload = ( + (MOCK_CALL_HISTORY_WITHOUT_UCM_LINE_NUMBER) + ); + webex.request.mockResolvedValue(mockCallHistoryPayload); + + const response = await callHistory.getCallHistoryData(7, 10, SORT.DEFAULT, SORT_BY.DEFAULT); + + expect(fetchUCMLinesDataSpy).toHaveBeenCalledTimes(1); + + expect(response.statusCode).toBe(200); + expect(response.data.userSessions && response.data.userSessions[0].self.cucmDN).toBeDefined(); + expect( + response.data.userSessions && response.data.userSessions[0].self.ucmLineNumber + ).toEqual(undefined); + }); + + it('should not call fetchUCMLinesData when calling backend is UCM but no valid cucmDN is present', async () => { + jest.spyOn(utils, 'getCallingBackEnd').mockReturnValue(CALLING_BACKEND.UCM); + // Since fetchUCMLinesData is a private method, TypeScript restricts direct access to it. + // To bypass this restriction, we are using 'as any' to access and invoke the method for testing purposes. + const fetchUCMLinesDataSpy = jest + .spyOn(callHistory as any, 'fetchUCMLinesData') + .mockResolvedValue({}); + + const callHistoryPayload = (mockCallHistoryBody); + webex.request.mockResolvedValue(callHistoryPayload); + + await callHistory.getCallHistoryData(7, 10, SORT.DEFAULT, SORT_BY.DEFAULT); + + expect(fetchUCMLinesDataSpy).not.toHaveBeenCalled(); + }); + + it('should not call fetchUCMLinesData when calling backend is not UCM', async () => { + jest.spyOn(utils, 'getCallingBackEnd').mockReturnValue(CALLING_BACKEND.WXC); + // Since fetchUCMLinesData is a private method, TypeScript restricts direct access to it. + // To bypass this restriction, we are using 'as any' to access and invoke the method for testing purposes. + const fetchUCMLinesDataSpy = jest + .spyOn(callHistory as any, 'fetchUCMLinesData') + .mockResolvedValue({}); + + const callHistoryPayload = (mockCallHistoryBody); + webex.request.mockResolvedValue(callHistoryPayload); + await callHistory.getCallHistoryData(7, 10, SORT.DEFAULT, SORT_BY.DEFAULT); + expect(fetchUCMLinesDataSpy).not.toHaveBeenCalled(); // Check that fetchUCMLinesData was not called + }); + }); }); diff --git a/packages/calling/src/CallHistory/CallHistory.ts b/packages/calling/src/CallHistory/CallHistory.ts index 4afab6d6722..dc9f6fa551b 100644 --- a/packages/calling/src/CallHistory/CallHistory.ts +++ b/packages/calling/src/CallHistory/CallHistory.ts @@ -2,15 +2,23 @@ /* eslint-disable no-underscore-dangle */ import SDKConnector from '../SDKConnector'; import {ISDKConnector, WebexSDK} from '../SDKConnector/types'; -import {ALLOWED_SERVICES, HTTP_METHODS, WebexRequestPayload, SORT, SORT_BY} from '../common/types'; +import { + ALLOWED_SERVICES, + HTTP_METHODS, + WebexRequestPayload, + SORT, + SORT_BY, + CALLING_BACKEND, +} from '../common/types'; import { ICallHistory, JanusResponseEvent, LoggerInterface, UpdateMissedCallsResponse, + UCMLinesResponse, } from './types'; import log from '../Logger'; -import {serviceErrorCodeHandler} from '../common/Utils'; +import {serviceErrorCodeHandler, getVgActionEndpoint, getCallingBackEnd} from '../common/Utils'; import { APPLICATION_JSON, CALL_HISTORY_FILE, @@ -21,6 +29,12 @@ import { NUMBER_OF_DAYS, UPDATE_MISSED_CALLS_ENDPOINT, SET_READ_STATE_SUCCESS_MESSAGE, + VERSION_1, + UNIFIED_COMMUNICATIONS, + CONFIG, + PEOPLE, + LINES, + ORG_ID, } from './constants'; import {STATUS_CODE, SUCCESS_MESSAGE, USER_SESSIONS} from '../common/constants'; import { @@ -32,6 +46,7 @@ import { EndTimeSessionId, CallSessionViewedEvent, SanitizedEndTimeAndSessionId, + UCMLinesApiResponse, } from '../Events/types'; import {Eventing} from '../Events/impl'; /** @@ -128,6 +143,43 @@ export class CallHistory extends Eventing implements ICal ); } } + // Check the calling backend + const callingBackend = getCallingBackEnd(this.webex); + if (callingBackend === CALLING_BACKEND.UCM) { + // Check if userSessions exist and the length is greater than 0 + if (this.userSessions[USER_SESSIONS] && this.userSessions[USER_SESSIONS].length > 0) { + // Check if cucmDN exists and is valid in any of the userSessions + const hasCucmDN = this.userSessions[USER_SESSIONS].some( + (session: UserSession) => session.self.cucmDN && session.self.cucmDN.length > 0 + ); + // If any user session has cucmDN, proceed to fetch line data + if (hasCucmDN) { + // Fetch the Lines data + const ucmLinesResponse = await this.fetchUCMLinesData(); + + // Check if the Lines API response was successful + if (ucmLinesResponse.statusCode === 200 && ucmLinesResponse.data.lines?.devices) { + const ucmLinesData = ucmLinesResponse.data.lines.devices; + + // Iterate over user sessions and match with Lines data + this.userSessions[USER_SESSIONS].forEach((session: UserSession) => { + const cucmDN = session.self.cucmDN; + + if (cucmDN) { + ucmLinesData.forEach((device) => { + device.lines.forEach((line) => { + if (line.dnorpattern === cucmDN) { + session.self.ucmLineNumber = line.index; // Assign the ucmLineNumber + } + }); + }); + } + }); + } + } + } + } + const responseDetails = { statusCode: this.userSessions[STATUS_CODE], data: { @@ -202,6 +254,45 @@ export class CallHistory extends Eventing implements ICal } } + /** + * Function to display the UCM Lines API response. + * @returns {Promise} Resolves to an object of type {@link UCMLinesResponse}.Response details with success or error status. + */ + private async fetchUCMLinesData(): Promise { + const loggerContext = { + file: CALL_HISTORY_FILE, + method: 'fetchLinesData', + }; + const vgEndpoint = getVgActionEndpoint(this.webex, CALLING_BACKEND.UCM); + const userId = this.webex.internal.device.userId; + const orgId = this.webex.internal.device.orgId; + const linesURIForUCM = `${vgEndpoint}/${VERSION_1}/${UNIFIED_COMMUNICATIONS}/${CONFIG}/${PEOPLE}/${userId}/${LINES}?${ORG_ID}=${orgId}`; + + try { + const response = await this.webex.request({ + uri: `${linesURIForUCM}`, + method: HTTP_METHODS.GET, + }); + + const ucmLineDetails: UCMLinesResponse = { + statusCode: Number(response.statusCode), + data: { + lines: response.body as UCMLinesApiResponse, + }, + message: SUCCESS_MESSAGE, + }; + + log.info(`Line details fetched successfully`, loggerContext); + + return ucmLineDetails; + } catch (err: unknown) { + const errorInfo = err as WebexRequestPayload; + const errorStatus = serviceErrorCodeHandler(errorInfo, loggerContext); + + return errorStatus; + } + } + handleSessionEvents = async (event?: CallSessionEvent) => { if (event && event.data.userSessions.userSessions) { this.emit(COMMON_EVENT_KEYS.CALL_HISTORY_USER_SESSION_INFO, event as CallSessionEvent); diff --git a/packages/calling/src/CallHistory/callHistoryFixtures.ts b/packages/calling/src/CallHistory/callHistoryFixtures.ts index ef30a1e8219..b7901a6dcc6 100644 --- a/packages/calling/src/CallHistory/callHistoryFixtures.ts +++ b/packages/calling/src/CallHistory/callHistoryFixtures.ts @@ -5,7 +5,7 @@ import { SessionType, CallSessionViewedEvent, } from '../Events/types'; -import {UpdateMissedCallsResponse} from './types'; +import {UCMLinesResponse, UpdateMissedCallsResponse} from './types'; export const sortedCallHistory = { body: { @@ -287,6 +287,200 @@ export const mockCallHistoryBody = { }, }; +/** + * MOCK_CALL_HISTORY_WITH_UCM_LINE_NUMBER simulates a call history response where the session contains + * both cucmDN and ucmLineNumber data. This implies that the cucmDN was successfully matched with the UCM lines data. + */ +export const MOCK_CALL_HISTORY_WITH_UCM_LINE_NUMBER = { + body: { + statusCode: 200, + userSessions: [ + { + id: '123456', + durationSecs: 438, + self: { + id: 'fd2e1234', + name: 'Mark', + cucmDN: '1001', + ucmLineNumber: 1, + incomingCallProtocols: [], + callbackInfo: { + callbackAddress: 'test@cisco.com', + callbackType: 'EMAIL', + }, + lookUpInfo: { + lookupLink: 'https://conv-a.wbx2.com/conversation/api/v1/conversations/98765', + type: 'CONVERSATION', + }, + }, + url: 'https://janus-a.wbx2.com/janus/api/v1/history/userSessions/654321', + sessionId: '123456', + sessionType: 'SPARK', + startTime: '2022-08-22T10:45:21.565Z', + endTime: '2022-08-22T10:53:01.624Z', + direction: 'OUTGOING', + disposition: 'INITIATED', + other: { + id: '100001', + name: 'test', + isPrivate: false, + callbackAddress: '89998888', + }, + durationSeconds: 438, + joinedDurationSeconds: 457, + participantCount: 2, + links: { + locusUrl: 'https://locus-a.wbx2.com/locus/api/v1/loci/786765', + conversationUrl: 'https://conv-a.wbx2.com/conversation/api/v1/conversations/55443322', + callbackAddress: '01010101', + }, + isDeleted: false, + isPMR: false, + correlationIds: ['008899'], + }, + { + id: '20191817', + durationSecs: 438, + self: { + id: '12131415', + name: 'Mark', + cucmDN: '1002', + ucmLineNumber: 2, + incomingCallProtocols: [], + callbackInfo: { + callbackAddress: 'test@cisco.com', + callbackType: 'EMAIL', + }, + lookUpInfo: { + lookupLink: 'https://conv-a.wbx2.com/conversation/api/v1/conversations/21314151', + type: 'CONVERSATION', + }, + }, + url: 'https://janus-a.wbx2.com/janus/api/v1/history/userSessions/100101102', + sessionId: '20191817', + sessionType: 'SPARK', + startTime: '2022-08-30T10:45:21.565Z', + endTime: '2022-08-30T10:53:01.624Z', + direction: 'OUTGOING', + disposition: 'INITIATED', + other: { + id: '301302303', + name: 'test', + isPrivate: false, + callbackAddress: '401402403', + }, + durationSeconds: 438, + joinedDurationSeconds: 457, + participantCount: 2, + links: { + locusUrl: 'https://locus-a.wbx2.com/locus/api/v1/loci/501502503', + conversationUrl: 'https://conv-a.wbx2.com/conversation/api/v1/conversations/601602603', + callbackAddress: '801802803', + }, + isDeleted: false, + isPMR: false, + correlationIds: ['901902903'], + }, + ], + }, +}; + +/** + * MOCK_CALL_HISTORY_WITHOUT_UCM_LINE_NUMBER simulates a call history response where the session contains + * cucmDN, but no ucmLineNumber is present. This implies that the cucmDN was not matched with any UCM lines data. + */ +export const MOCK_CALL_HISTORY_WITHOUT_UCM_LINE_NUMBER = { + body: { + statusCode: 200, + userSessions: [ + { + id: '123456', + durationSecs: 438, + self: { + id: 'fd2e1234', + name: 'Mark', + cucmDN: '1001', + incomingCallProtocols: [], + callbackInfo: { + callbackAddress: 'test@cisco.com', + callbackType: 'EMAIL', + }, + lookUpInfo: { + lookupLink: 'https://conv-a.wbx2.com/conversation/api/v1/conversations/98765', + type: 'CONVERSATION', + }, + }, + url: 'https://janus-a.wbx2.com/janus/api/v1/history/userSessions/654321', + sessionId: '123456', + sessionType: 'SPARK', + startTime: '2022-08-22T10:45:21.565Z', + endTime: '2022-08-22T10:53:01.624Z', + direction: 'OUTGOING', + disposition: 'INITIATED', + other: { + id: '100001', + name: 'test', + isPrivate: false, + callbackAddress: '89998888', + }, + durationSeconds: 438, + joinedDurationSeconds: 457, + participantCount: 2, + links: { + locusUrl: 'https://locus-a.wbx2.com/locus/api/v1/loci/786765', + conversationUrl: 'https://conv-a.wbx2.com/conversation/api/v1/conversations/55443322', + callbackAddress: '01010101', + }, + isDeleted: false, + isPMR: false, + correlationIds: ['008899'], + }, + { + id: '20191817', + durationSecs: 438, + self: { + id: '12131415', + name: 'Mark', + cucmDN: '1002', + incomingCallProtocols: [], + callbackInfo: { + callbackAddress: 'test@cisco.com', + callbackType: 'EMAIL', + }, + lookUpInfo: { + lookupLink: 'https://conv-a.wbx2.com/conversation/api/v1/conversations/21314151', + type: 'CONVERSATION', + }, + }, + url: 'https://janus-a.wbx2.com/janus/api/v1/history/userSessions/100101102', + sessionId: '20191817', + sessionType: 'SPARK', + startTime: '2022-08-30T10:45:21.565Z', + endTime: '2022-08-30T10:53:01.624Z', + direction: 'OUTGOING', + disposition: 'INITIATED', + other: { + id: '301302303', + name: 'test', + isPrivate: false, + callbackAddress: '401402403', + }, + durationSeconds: 438, + joinedDurationSeconds: 457, + participantCount: 2, + links: { + locusUrl: 'https://locus-a.wbx2.com/locus/api/v1/loci/501502503', + conversationUrl: 'https://conv-a.wbx2.com/conversation/api/v1/conversations/601602603', + callbackAddress: '801802803', + }, + isDeleted: false, + isPMR: false, + correlationIds: ['901902903'], + }, + ], + }, +}; + const WEBEX_CALL_SESSION = { id: 'd74d19cc-6aa7-f341-6012-aec433cc6f8d', durationSecs: 438, @@ -439,3 +633,43 @@ export const ERROR_DETAILS_400 = { }, message: 'FAILURE', }; + +/* + * MOCK_LINES_API_CALL_RESPONSE simulates a successful response from the UCM lines API. + */ +export const MOCK_LINES_API_CALL_RESPONSE: UCMLinesResponse = { + statusCode: 200, + data: { + lines: { + devices: [ + { + name: 'CSFheliosucm01', + model: 503, + lines: [ + { + dnorpattern: '+14928000001', + index: 1, + label: '', + }, + { + dnorpattern: '+14928000003', + index: 2, + label: '', + }, + ], + }, + ], + }, + }, + message: 'SUCCESS', +}; + +/** + * MOCK_LINES_API_CALL_RESPONSE_WITH_NO_LINEDATA simulates a successful UCM lines API response + * where no line data is present. The `lines` field is empty, indicating no devices or lines available. + */ +export const MOCK_LINES_API_CALL_RESPONSE_WITH_NO_LINEDATA: UCMLinesResponse = { + statusCode: 200, + data: {}, + message: 'SUCCESS', +}; diff --git a/packages/calling/src/CallHistory/constants.ts b/packages/calling/src/CallHistory/constants.ts index ac222e6edc3..f22c1d04e86 100644 --- a/packages/calling/src/CallHistory/constants.ts +++ b/packages/calling/src/CallHistory/constants.ts @@ -1,13 +1,19 @@ export const APPLICATION_JSON = 'application/json'; export const CALL_HISTORY_FILE = 'CallHistory'; export const CONTENT_TYPE = 'Content-Type'; +export const CONFIG = 'config'; export const FROM_DATE = '?from'; export const HISTORY = 'history'; export const LIMIT = 50; +export const LINES = 'lines'; export const NUMBER_OF_DAYS = 10; +export const ORG_ID = 'orgId'; +export const PEOPLE = 'people'; export const RESPONSE_MESSAGE = 'responseMessage'; -export const UPDATE_MISSED_CALLS_ENDPOINT = 'setReadState'; export const SET_READ_STATE_SUCCESS_MESSAGE = 'Missed calls are read by the user.'; export const SUCCESS_MESSAGE = 'SUCCESS'; export const STATUS_CODE = 'statusCode'; export const USER_SESSIONS = 'userSessions'; +export const UPDATE_MISSED_CALLS_ENDPOINT = 'setReadState'; +export const UNIFIED_COMMUNICATIONS = 'uc'; +export const VERSION_1 = 'v1'; diff --git a/packages/calling/src/CallHistory/types.ts b/packages/calling/src/CallHistory/types.ts index ca0ca3f333e..60be740f665 100644 --- a/packages/calling/src/CallHistory/types.ts +++ b/packages/calling/src/CallHistory/types.ts @@ -1,5 +1,10 @@ import {Eventing} from '../Events/impl'; -import {CallHistoryEventTypes, EndTimeSessionId, UserSession} from '../Events/types'; +import { + CallHistoryEventTypes, + EndTimeSessionId, + UserSession, + UCMLinesApiResponse, +} from '../Events/types'; import {LOGGER} from '../Logger/types'; import {SORT, SORT_BY} from '../common/types'; @@ -25,6 +30,15 @@ export type UpdateMissedCallsResponse = { message: string | null; }; +export type UCMLinesResponse = { + statusCode: number; + data: { + lines?: UCMLinesApiResponse; + error?: string; + }; + message: string | null; +}; + /** * Interface for CallHistory Client. * This encompasses a set of APIs designed to facilitate the retrieval of recent Call History Record. diff --git a/packages/calling/src/Events/types.ts b/packages/calling/src/Events/types.ts index 17c8c51bf5d..c0ed077a021 100644 --- a/packages/calling/src/Events/types.ts +++ b/packages/calling/src/Events/types.ts @@ -79,6 +79,8 @@ export type CallRecordSelf = { id: string; name?: string; phoneNumber?: string; + cucmDN?: string; + ucmLineNumber?: number; }; export type CallRecordListOther = { @@ -242,32 +244,38 @@ enum CALL_STATE { REMOTE_HELD = 'remoteheld', CONNECTED = 'connected', } + type eventType = string; + type callProgressData = { alerting: boolean; inbandROAP: boolean; }; + export type CallerIdInfo = { 'x-broadworks-remote-party-info'?: string; 'p-asserted-identity'?: string; from?: string; }; + type callId = string; type deviceId = string; type correlationId = string; type callUrl = string; type causecode = number; type cause = string; + type eventData = { callerId: CallerIdInfo; callState: CALL_STATE; }; + type midCallServiceData = { eventType: eventType; eventData: eventData; }; -type midCallService = Array; +type midCallService = Array; interface BaseMessage { eventType: eventType; correlationId: correlationId; @@ -275,19 +283,18 @@ interface BaseMessage { callId: callId; callUrl: callUrl; } - export interface CallSetupMessage extends BaseMessage { callerId: CallerIdInfo; trackingId: string; alertType: string; } - interface CallProgressMessage extends BaseMessage { callProgressData: callProgressData; callerId: CallerIdInfo; } export const WEBSOCKET_SCOPE = 'mobius'; + export enum WEBSOCKET_KEYS { CALL_PROGRESS = 'callprogress', CALL_CONNECTED = 'callconnected', @@ -368,7 +375,24 @@ export type EndTimeSessionId = { endTime: string; sessionId: string; }; + export type SanitizedEndTimeAndSessionId = { endTime: number; sessionId: string; }; + +export type UCMLine = { + dnorpattern: string; + index: number; + label: string | null; +}; + +export type UCMDevice = { + name: string; + model: number; + lines: UCMLine[]; +}; + +export type UCMLinesApiResponse = { + devices: UCMDevice[]; +}; diff --git a/packages/calling/src/common/Utils.ts b/packages/calling/src/common/Utils.ts index 5c5ee2434ff..4c2d8441dfe 100644 --- a/packages/calling/src/common/Utils.ts +++ b/packages/calling/src/common/Utils.ts @@ -85,7 +85,11 @@ import { URL_ENDPOINT, UTILS_FILE, } from '../CallingClient/constants'; -import {JanusResponseEvent, UpdateMissedCallsResponse} from '../CallHistory/types'; +import { + JanusResponseEvent, + UCMLinesResponse, + UpdateMissedCallsResponse, +} from '../CallHistory/types'; import { VoicemailResponseEvent, MessageInfo, @@ -691,6 +695,7 @@ export async function serviceErrorCodeHandler( | CallSettingResponse | ContactResponse | UpdateMissedCallsResponse + | UCMLinesResponse > { const errorCode = Number(err.statusCode); const failureMessage = 'FAILURE'; From 15d6e91f438b001be1654d815e6b0a91045ad463 Mon Sep 17 00:00:00 2001 From: covidal-cisco Date: Thu, 19 Sep 2024 16:13:16 +0200 Subject: [PATCH 23/48] Operational buisness metrics (#3818) --- .../src/behavioral-metrics.ts | 40 ++++ .../src/behavioral/behavioral-metrics.ts | 179 ------------------ .../src/behavioral/config.ts | 3 - .../src/business-metrics.ts | 30 +++ .../src/generic-metrics.ts | 146 ++++++++++++++ .../internal-plugin-metrics/src/index.ts | 6 +- .../src/metrics.types.ts | 45 +++-- .../src/new-metrics.ts | 85 +++++++-- .../src/operational-metrics.ts | 24 +++ .../spec/behavioral/behavioral-metrics.ts | 61 +++++- .../unit/spec/business/business-metrics.ts | 120 ++++++++++++ .../spec/operational/operational-metrics.ts | 115 +++++++++++ 12 files changed, 628 insertions(+), 226 deletions(-) create mode 100644 packages/@webex/internal-plugin-metrics/src/behavioral-metrics.ts delete mode 100644 packages/@webex/internal-plugin-metrics/src/behavioral/behavioral-metrics.ts delete mode 100644 packages/@webex/internal-plugin-metrics/src/behavioral/config.ts create mode 100644 packages/@webex/internal-plugin-metrics/src/business-metrics.ts create mode 100644 packages/@webex/internal-plugin-metrics/src/generic-metrics.ts create mode 100644 packages/@webex/internal-plugin-metrics/src/operational-metrics.ts create mode 100644 packages/@webex/internal-plugin-metrics/test/unit/spec/business/business-metrics.ts create mode 100644 packages/@webex/internal-plugin-metrics/test/unit/spec/operational/operational-metrics.ts diff --git a/packages/@webex/internal-plugin-metrics/src/behavioral-metrics.ts b/packages/@webex/internal-plugin-metrics/src/behavioral-metrics.ts new file mode 100644 index 00000000000..1377a1083e5 --- /dev/null +++ b/packages/@webex/internal-plugin-metrics/src/behavioral-metrics.ts @@ -0,0 +1,40 @@ +import {MetricEventProduct, MetricEventAgent, MetricEventVerb, EventPayload} from './metrics.types'; +import GenericMetrics from './generic-metrics'; + +/** + * @description Util class to handle Behavioral Metrics + * @export + * @class BehavioralMetrics + */ +export default class BehavioralMetrics extends GenericMetrics { + /** + * Submit a behavioral metric to our metrics endpoint. + * @param {MetricEventProduct} product the product from which the metric is being submitted, e.g. 'webex' web client, 'wxcc_desktop' + * @param {MetricEventAgent} agent the source of the action for this metric + * @param {string} target the 'thing' that this metric includes information about + * @param {MetricEventVerb} verb the action that this metric includes information about + * @param {EventPayload} payload information specific to this event. This should be flat, i.e. it should not include nested objects. + * @returns {Promise} + */ + public submitBehavioralEvent({ + product, + agent, + target, + verb, + payload, + }: { + product: MetricEventProduct; + agent: MetricEventAgent; + target: string; + verb: MetricEventVerb; + payload?: EventPayload; + }) { + const name = `${product}.${agent}.${target}.${verb}`; + const event = this.createTaggedEventObject({ + type: ['behavioral'], + name, + payload, + }); + this.submitEvent({kind: 'behavioral-events -> ', name, event}); + } +} diff --git a/packages/@webex/internal-plugin-metrics/src/behavioral/behavioral-metrics.ts b/packages/@webex/internal-plugin-metrics/src/behavioral/behavioral-metrics.ts deleted file mode 100644 index e6666678c56..00000000000 --- a/packages/@webex/internal-plugin-metrics/src/behavioral/behavioral-metrics.ts +++ /dev/null @@ -1,179 +0,0 @@ -import {merge} from 'lodash'; -import {BrowserDetection} from '@webex/common'; -import {StatelessWebexPlugin} from '@webex/webex-core'; -import {getOSNameInternal} from '../metrics'; -import {BEHAVIORAL_LOG_IDENTIFIER} from './config'; -import { - MetricEventProduct, - MetricEventAgent, - MetricEventVerb, - BehavioralEventContext, - BehavioralEvent, - BehavioralEventPayload, -} from '../metrics.types'; -import ClientMetricsBatcher from '../client-metrics-batcher'; - -const {getOSVersion, getBrowserName, getBrowserVersion} = BrowserDetection(); - -/** - * @description Util class to handle Behavioral Metrics - * @export - * @class BehavioralMetrics - */ -export default class BehavioralMetrics extends StatelessWebexPlugin { - // @ts-ignore - private clientMetricsBatcher: ClientMetricsBatcher; - private logger: any; // to avoid adding @ts-ignore everywhere - private device: any; - private version: string; - - /** - * Constructor - * @param {any[]} args - */ - constructor(...args) { - super(...args); - // @ts-ignore - this.logger = this.webex.logger; - // @ts-ignore - this.device = this.webex.internal.device; - // @ts-ignore - this.version = this.webex.version; - // @ts-ignore - this.clientMetricsBatcher = new ClientMetricsBatcher({}, {parent: this.webex}); - } - - /** - * Returns the deviceId from our registration with WDM. - * @returns {string} deviceId or empty string - */ - private getDeviceId(): string { - const {url} = this.device; - if (url && url.length !== 0) { - const n = url.lastIndexOf('/'); - if (n !== -1) { - return url.substring(n + 1); - } - } - - return ''; - } - - /** - * Returns the context object to be submitted with all behavioral metrics. - * @returns {BehavioralEventContext} - */ - private getContext(): BehavioralEventContext { - const context: BehavioralEventContext = { - app: { - version: this.version, - }, - device: { - id: this.getDeviceId(), - }, - locale: window.navigator.language, - os: { - name: getOSNameInternal(), - version: getOSVersion(), - }, - }; - - return context; - } - - /** - * Returns the default tags to be included with all behavioral metrics. - * @returns {BehavioralEventPayload} - */ - private getDefaultTags(): BehavioralEventPayload { - const tags = { - browser: getBrowserName(), - browserHeight: window.innerHeight, - browserVersion: getBrowserVersion(), - browserWidth: window.innerWidth, - domain: window.location.hostname, - inIframe: window.self !== window.top, - locale: window.navigator.language, - os: getOSNameInternal(), - }; - - return tags; - } - - /** - * Creates the object to send to our metrics endpoint for a behavioral event - * @param {MetricEventProduct} product - * @param {MetricEventAgent} agent - * @param {string} target - * @param {MetricEventVerb} verb - * @returns {BehavioralEventPayload} - */ - private createEventObject({ - product, - agent, - target, - verb, - payload, - }: { - product: MetricEventProduct; - agent: MetricEventAgent; - target: string; - verb: MetricEventVerb; - payload?: BehavioralEventPayload; - }): BehavioralEvent { - const metricName = `${product}.${agent}.${target}.${verb}`; - let allTags: BehavioralEventPayload = payload; - allTags = merge(allTags, this.getDefaultTags()); - - const event: BehavioralEvent = { - context: this.getContext(), - metricName, - tags: allTags, - timestamp: Date.now(), - type: ['behavioral'], - }; - - return event; - } - - /** - * Returns true once we're ready to submit behavioral metrics, after startup. - * @returns {boolean} true when deviceId is defined and non-empty - */ - public isReadyToSubmitBehavioralEvents(): boolean { - const deviceId = this.getDeviceId(); - - return deviceId && deviceId.length !== 0; - } - - /** - * Submit a behavioral metric to our metrics endpoint. - * @param {MetricEventProduct} product the product from which the metric is being submitted, e.g. 'webex' web client, 'wxcc_desktop' - * @param {MetricEventAgent} agent the source of the action for this metric - * @param {string} target the 'thing' that this metric includes information about - * @param {MetricEventVerb} verb the action that this metric includes information about - * @param {BehavioralEventPayload} payload information specific to this event. This should be flat, i.e. it should not include nested objects. - * @returns {Promise} - */ - public submitBehavioralEvent({ - product, - agent, - target, - verb, - payload, - }: { - product: MetricEventProduct; - agent: MetricEventAgent; - target: string; - verb: MetricEventVerb; - payload?: BehavioralEventPayload; - }) { - this.logger.log( - BEHAVIORAL_LOG_IDENTIFIER, - `BehavioralMetrics: @submitBehavioralEvent. Submit Behavioral event: ${product}.${agent}.${target}.${verb}` - ); - const behavioralEvent = this.createEventObject({product, agent, target, verb, payload}); - - return this.clientMetricsBatcher.request(behavioralEvent); - } -} diff --git a/packages/@webex/internal-plugin-metrics/src/behavioral/config.ts b/packages/@webex/internal-plugin-metrics/src/behavioral/config.ts deleted file mode 100644 index 4b5af5269c9..00000000000 --- a/packages/@webex/internal-plugin-metrics/src/behavioral/config.ts +++ /dev/null @@ -1,3 +0,0 @@ -/* eslint-disable import/prefer-default-export */ - -export const BEHAVIORAL_LOG_IDENTIFIER = 'behavioral-events -> '; diff --git a/packages/@webex/internal-plugin-metrics/src/business-metrics.ts b/packages/@webex/internal-plugin-metrics/src/business-metrics.ts new file mode 100644 index 00000000000..80b023e36d2 --- /dev/null +++ b/packages/@webex/internal-plugin-metrics/src/business-metrics.ts @@ -0,0 +1,30 @@ +import GenericMetrics from './generic-metrics'; +import {EventPayload} from './metrics.types'; + +/** + * @description Util class to handle Buisness Metrics + * @export + * @class BusinessMetrics + */ +export default class BusinessMetrics extends GenericMetrics { + /** + * Submit a buisness metric to our metrics endpoint. + * @param {string} name of the metric + * @param {EventPayload} user payload of the metric + * @returns {Promise} + */ + public submitBusinessEvent({name, payload}: {name: string; payload: EventPayload}) { + const event = { + type: ['business'], + eventPayload: { + metricName: name, + timestamp: Date.now(), + context: this.getContext(), + browserDetails: this.getBrowserDetails(), + value: payload, + }, + }; + + this.submitEvent({kind: 'buisness-events -> ', name, event}); + } +} diff --git a/packages/@webex/internal-plugin-metrics/src/generic-metrics.ts b/packages/@webex/internal-plugin-metrics/src/generic-metrics.ts new file mode 100644 index 00000000000..89b52bfb5b6 --- /dev/null +++ b/packages/@webex/internal-plugin-metrics/src/generic-metrics.ts @@ -0,0 +1,146 @@ +import {StatelessWebexPlugin} from '@webex/webex-core'; +import {BrowserDetection} from '@webex/common'; +import {merge} from 'lodash'; +import ClientMetricsBatcher from './client-metrics-batcher'; +import {getOSNameInternal} from './metrics'; +import {DeviceContext, TaggedEvent, EventPayload, MetricType} from './metrics.types'; + +const {getOSVersion, getBrowserName, getBrowserVersion} = BrowserDetection(); + +/** + * @description top-level abstract class to handle Metrics and common routines. + * @export + * @class GenericMetrics + */ +export default abstract class GenericMetrics extends StatelessWebexPlugin { + // @ts-ignore + private clientMetricsBatcher: ClientMetricsBatcher; + private logger: any; // to avoid adding @ts-ignore everywhere + private device: any; + private version: string; + private deviceId = ''; + + /** + * Constructor + * @param {any[]} args + */ + constructor(...args) { + super(...args); + // @ts-ignore + this.logger = this.webex.logger; + // @ts-ignore + this.clientMetricsBatcher = new ClientMetricsBatcher({}, {parent: this.webex}); + // @ts-ignore + this.device = this.webex.internal.device; + // @ts-ignore + this.version = this.webex.version; + } + + /** + * Submit a buisness metric to our metrics endpoint. + * @param {string} kind of metric for logging + * @param {string} name of the metric + * @param {object} event + * @returns {Promise} + */ + protected submitEvent({kind, name, event}: {kind: string; name: string; event: object}) { + this.logger.log(kind, `@submitEvent. Submit event: ${name}`); + + return this.clientMetricsBatcher.request(event); + } + + /** + * Returns the deviceId from our registration with WDM. + * @returns {string} deviceId or empty string + */ + protected getDeviceId(): string { + if (this.deviceId === '') { + const {url} = this.device; + if (url && url.length !== 0) { + const n = url.lastIndexOf('/'); + if (n !== -1) { + this.deviceId = url.substring(n + 1); + } + } + } + + return this.deviceId; + } + + /** + * Returns the context object to be submitted with all metrics. + * @returns {DeviceContext} + */ + protected getContext(): DeviceContext { + return { + app: { + version: this.version, + }, + device: { + id: this.getDeviceId(), + }, + locale: window.navigator.language, + os: { + name: getOSNameInternal(), + version: getOSVersion(), + }, + }; + } + + /** + * Returns the browser details to be included with all metrics. + * @returns {object} + */ + protected getBrowserDetails(): object { + return { + browser: getBrowserName(), + browserHeight: window.innerHeight, + browserVersion: getBrowserVersion(), + browserWidth: window.innerWidth, + domain: window.location.hostname, + inIframe: window.self !== window.top, + locale: window.navigator.language, + os: getOSNameInternal(), + }; + } + + /** + * Returns true once we have the deviceId we need to submit behavioral/operational/buisness events + * @returns {boolean} + */ + public isReadyToSubmitEvents(): boolean { + const deviceId = this.getDeviceId(); + + return deviceId && deviceId.length !== 0; + } + + /** + * Creates the object to send to our metrics endpoint for a tagged event (i.e. behavoral or operational) + * @param {[MetricType]} list of event type (i.e. ['behavioral'], ['operational', 'behavioral']) + * @param {string} metric name + * @param {EventPayload} user payload + * @returns {EventPayload} + */ + protected createTaggedEventObject({ + type, + name, + payload, + }: { + type: [MetricType]; + name: string; + payload: EventPayload; + }): TaggedEvent { + let allTags: EventPayload = payload; + allTags = merge(allTags, this.getBrowserDetails()); + + const event = { + context: this.getContext(), + metricName: name, + tags: allTags, + timestamp: Date.now(), + type, + }; + + return event; + } +} diff --git a/packages/@webex/internal-plugin-metrics/src/index.ts b/packages/@webex/internal-plugin-metrics/src/index.ts index cd7c9ad21a8..7247332be85 100644 --- a/packages/@webex/internal-plugin-metrics/src/index.ts +++ b/packages/@webex/internal-plugin-metrics/src/index.ts @@ -22,7 +22,9 @@ import * as CALL_DIAGNOSTIC_CONFIG from './call-diagnostic/config'; import * as CallDiagnosticUtils from './call-diagnostic/call-diagnostic-metrics.util'; import CallDiagnosticMetrics from './call-diagnostic/call-diagnostic-metrics'; import CallDiagnosticLatencies from './call-diagnostic/call-diagnostic-metrics-latencies'; -import BehavioralMetrics from './behavioral/behavioral-metrics'; +import BehavioralMetrics from './behavioral-metrics'; +import OperationalMetrics from './operational-metrics'; +import BusinessMetrics from './business-metrics'; registerInternalPlugin('metrics', Metrics, { config, @@ -43,6 +45,8 @@ export { CallDiagnosticLatencies, CallDiagnosticMetrics, BehavioralMetrics, + OperationalMetrics, + BusinessMetrics, }; export type { ClientEvent, diff --git a/packages/@webex/internal-plugin-metrics/src/metrics.types.ts b/packages/@webex/internal-plugin-metrics/src/metrics.types.ts index f5a9bc5b2a5..895f4703ac2 100644 --- a/packages/@webex/internal-plugin-metrics/src/metrics.types.ts +++ b/packages/@webex/internal-plugin-metrics/src/metrics.types.ts @@ -102,7 +102,7 @@ export interface ClientEvent { options?: SubmitClientEventOptions; } -export interface BehavioralEventContext { +export interface DeviceContext { app: {version: string}; device: {id: string}; locale: string; @@ -112,23 +112,36 @@ export interface BehavioralEventContext { }; } -export interface BehavioralEvent { - context: BehavioralEventContext; +export type MetricType = 'behavioral' | 'operational' | 'business'; + +type InternalEventPayload = string | number | boolean; +export type EventPayload = Record; +export type BehavioralEventPayload = EventPayload; // for compatibilty, can be remove after wxcc-desktop did change their imports. + +export interface BusinessEventPayload { metricName: string; - tags: Record; timestamp: number; - type: string[]; + context: DeviceContext; + browserDetails: EventPayload; + value: EventPayload; } -export type BehavioralEventPayload = BehavioralEvent['tags']; +export interface BusinessEvent { + type: string[]; + eventPayload: BusinessEventPayload; +} -export interface OperationalEvent { - // TODO: not implemented - name: never; - payload?: never; - options?: never; +export interface TaggedEvent { + context: DeviceContext; + metricName: string; + tags: EventPayload; + timestamp: number; + type: [MetricType]; } +export type BehavioralEvent = TaggedEvent; +export type OperationalEvent = TaggedEvent; + export interface FeatureEvent { // TODO: not implemented name: never; @@ -154,7 +167,8 @@ export type MetricEventNames = | InternalEvent['name'] | ClientEvent['name'] | BehavioralEvent['metricName'] - | OperationalEvent['name'] + | OperationalEvent['metricName'] + | BusinessEvent['eventPayload']['metricName'] | FeatureEvent['name'] | MediaQualityEvent['name']; @@ -190,7 +204,7 @@ export type SubmitBehavioralEvent = (args: { agent: MetricEventAgent; target: string; verb: MetricEventVerb; - payload?: BehavioralEventPayload; + payload?: EventPayload; }) => void; export type SubmitClientEvent = (args: { @@ -200,9 +214,8 @@ export type SubmitClientEvent = (args: { }) => Promise; export type SubmitOperationalEvent = (args: { - name: OperationalEvent['name']; - payload?: RecursivePartial; - options?: any; + name: OperationalEvent['metricName']; + payload: EventPayload; }) => void; export type SubmitMQE = (args: { diff --git a/packages/@webex/internal-plugin-metrics/src/new-metrics.ts b/packages/@webex/internal-plugin-metrics/src/new-metrics.ts index 614b2645dd1..2f0df927265 100644 --- a/packages/@webex/internal-plugin-metrics/src/new-metrics.ts +++ b/packages/@webex/internal-plugin-metrics/src/new-metrics.ts @@ -6,7 +6,9 @@ import {WebexPlugin} from '@webex/webex-core'; import CallDiagnosticMetrics from './call-diagnostic/call-diagnostic-metrics'; -import BehavioralMetrics from './behavioral/behavioral-metrics'; +import BehavioralMetrics from './behavioral-metrics'; +import OperationalMetrics from './operational-metrics'; +import BusinessMetrics from './business-metrics'; import { RecursivePartial, MetricEventProduct, @@ -14,7 +16,7 @@ import { MetricEventVerb, ClientEvent, FeatureEvent, - BehavioralEventPayload, + EventPayload, OperationalEvent, MediaQualityEvent, InternalEvent, @@ -37,6 +39,9 @@ class Metrics extends WebexPlugin { // Helper classes to handle the different types of metrics callDiagnosticMetrics: CallDiagnosticMetrics; behavioralMetrics: BehavioralMetrics; + operationalMetrics: OperationalMetrics; + businessMetrics: BusinessMetrics; + isReady = false; /** * Constructor @@ -61,8 +66,7 @@ class Metrics extends WebexPlugin { this.webex.once('ready', () => { // @ts-ignore this.callDiagnosticMetrics = new CallDiagnosticMetrics({}, {parent: this.webex}); - // @ts-ignore - this.behavioralMetrics = new BehavioralMetrics({}, {parent: this.webex}); + this.isReady = true; }); } @@ -90,7 +94,21 @@ class Metrics extends WebexPlugin { * @returns true once we have the deviceId we need to submit behavioral events to Amplitude */ isReadyToSubmitBehavioralEvents() { - return this.behavioralMetrics.isReadyToSubmitBehavioralEvents(); + return this.behavioralMetrics?.isReadyToSubmitEvents() ?? false; + } + + /** + * @returns true once we have the deviceId we need to submit operational events + */ + isReadyToSubmitOperationalEvents() { + return this.operationalMetrics?.isReadyToSubmitEvents() ?? false; + } + + /** + * @returns true once we have the deviceId we need to submit buisness events + */ + isReadyToSubmitBusinessEvents() { + return this.businessMetrics?.isReadyToSubmitEvents() ?? false; } /** @@ -108,9 +126,9 @@ class Metrics extends WebexPlugin { agent: MetricEventAgent; target: string; verb: MetricEventVerb; - payload?: BehavioralEventPayload; + payload?: EventPayload; }) { - if (!this.behavioralMetrics) { + if (!this.isReady) { // @ts-ignore this.webex.logger.log( `NewMetrics: @submitBehavioralEvent. Attempted to submit before webex.ready: ${product}.${agent}.${target}.${verb}` @@ -119,6 +137,11 @@ class Metrics extends WebexPlugin { return Promise.resolve(); } + if (!this.behavioralMetrics) { + // @ts-ignore + this.behavioralMetrics = new BehavioralMetrics({}, {parent: this.webex}); + } + return this.behavioralMetrics.submitBehavioralEvent({product, agent, target, verb, payload}); } @@ -126,16 +149,44 @@ class Metrics extends WebexPlugin { * Operational event * @param args */ - submitOperationalEvent({ - name, - payload, - options, - }: { - name: OperationalEvent['name']; - payload?: RecursivePartial; - options?: any; - }) { - throw new Error('Not implemented.'); + submitOperationalEvent({name, payload}: {name: string; payload?: EventPayload}) { + if (!this.isReady) { + // @ts-ignore + this.webex.logger.log( + `NewMetrics: @submitOperationalEvent. Attempted to submit before webex.ready: ${name}` + ); + + return Promise.resolve(); + } + + if (!this.operationalMetrics) { + // @ts-ignore + this.operationalMetrics = new OperationalMetrics({}, {parent: this.webex}); + } + + return this.operationalMetrics.submitOperationalEvent({name, payload}); + } + + /** + * Buisness event + * @param args + */ + submitBusinessEvent({name, payload}: {name: string; payload: EventPayload}) { + if (!this.isReady) { + // @ts-ignore + this.webex.logger.log( + `NewMetrics: @submitBusinessEvent. Attempted to submit before webex.ready: ${name}` + ); + + return Promise.resolve(); + } + + if (!this.businessMetrics) { + // @ts-ignore + this.businessMetrics = new BusinessMetrics({}, {parent: this.webex}); + } + + return this.businessMetrics.submitBusinessEvent({name, payload}); } /** diff --git a/packages/@webex/internal-plugin-metrics/src/operational-metrics.ts b/packages/@webex/internal-plugin-metrics/src/operational-metrics.ts new file mode 100644 index 00000000000..1c520a2000e --- /dev/null +++ b/packages/@webex/internal-plugin-metrics/src/operational-metrics.ts @@ -0,0 +1,24 @@ +import GenericMetrics from './generic-metrics'; +import {EventPayload} from './metrics.types'; + +/** + * @description Util class to handle Operational Metrics + * @export + * @class OperationalMetrics + */ +export default class OperationalMetrics extends GenericMetrics { + /** + * Submit an operational metric to our metrics endpoint. + * @param {string} name of the metric + * @param {EventPayload} user payload of the metric + * @returns {Promise} + */ + public submitOperationalEvent({name, payload}: {name: string; payload: EventPayload}) { + const event = this.createTaggedEventObject({ + type: ['operational'], + name, + payload, + }); + this.submitEvent({kind: 'operational-events -> ', name, event}); + } +} diff --git a/packages/@webex/internal-plugin-metrics/test/unit/spec/behavioral/behavioral-metrics.ts b/packages/@webex/internal-plugin-metrics/test/unit/spec/behavioral/behavioral-metrics.ts index 290b13d1f0e..73dc28455b5 100644 --- a/packages/@webex/internal-plugin-metrics/test/unit/spec/behavioral/behavioral-metrics.ts +++ b/packages/@webex/internal-plugin-metrics/test/unit/spec/behavioral/behavioral-metrics.ts @@ -74,6 +74,45 @@ describe('internal-plugin-metrics', () => { sinon.restore(); }); + describe('#sendEvent', () => { + it('should send correctly shaped behavioral event (check name building and internal tagged event building)', () => { + // For some reasons `jest` isn't available when testing form build server - so can't use `jest.fn()` here... + const requestCalls = []; + const request = function(arg) { requestCalls.push(arg) } + + behavioralMetrics.clientMetricsBatcher.request = request; + + assert.equal(requestCalls.length, 0) + behavioralMetrics.submitBehavioralEvent({ product: "webex", agent: "user", target: "foo", verb: "get", payload: {bar:"gee"} }) + assert.equal(requestCalls.length, 1) + assert.deepEqual(requestCalls[0], { + context: { + app: {version: 'webex-version'}, + device: {id: 'deviceId'}, + locale: 'language', + os: { + name: getOSNameInternal(), + version: getOSVersion(), + }, + }, + metricName: 'webex.user.foo.get', + tags: { + browser: getBrowserName(), + browserHeight: window.innerHeight, + browserVersion: getBrowserVersion(), + browserWidth: window.innerWidth, + domain: window.location.hostname, + inIframe: false, + locale: window.navigator.language, + os: getOSNameInternal(), + bar:"gee" + }, + timestamp: requestCalls[0].timestamp, // This is to bypass time check, which is correctly tested below. + type: ['behavioral'], + }); + }) + }) + describe('#getContext', () => { it('should build context correctly', () => { const res = behavioralMetrics.getContext(); @@ -96,7 +135,7 @@ describe('internal-plugin-metrics', () => { describe('#getDefaultTags', () => { it('should build tags correctly', () => { - const res = behavioralMetrics.getDefaultTags(); + const res = behavioralMetrics.getBrowserDetails(); assert.deepEqual(res, { browser: getBrowserName(), @@ -111,25 +150,27 @@ describe('internal-plugin-metrics', () => { }); }); - describe('#isReadyToSubmitBehavioralEvents', () => { + describe('#isReadyToSubmitEvents', () => { it('should return true when we have a deviceId, false when deviceId is empty or undefined', async () => { - assert.equal(true, behavioralMetrics.isReadyToSubmitBehavioralEvents()); + let deviceIdUrl = webex.internal.device.url; + // testing case w/o device id url first, as the internal deviceId cache would bypass that flow. webex.internal.device.url = ""; - assert.equal(false, behavioralMetrics.isReadyToSubmitBehavioralEvents()); + assert.equal(false, behavioralMetrics.isReadyToSubmitEvents()); delete webex.internal.device.url; - assert.equal(false, behavioralMetrics.isReadyToSubmitBehavioralEvents()); + assert.equal(false, behavioralMetrics.isReadyToSubmitEvents()); + + webex.internal.device.url = deviceIdUrl; + assert.equal(true, behavioralMetrics.isReadyToSubmitEvents()); }); }); describe('#createEventObject', () => { it('should build event object correctly', async () => { - const res = behavioralMetrics.createEventObject({ - product: 'webex', - agent: 'user', - target: 'target', - verb: 'create', + const res = behavioralMetrics.createTaggedEventObject({ + type:['behavioral'], + name:'webex.user.target.create', payload: tags, }); diff --git a/packages/@webex/internal-plugin-metrics/test/unit/spec/business/business-metrics.ts b/packages/@webex/internal-plugin-metrics/test/unit/spec/business/business-metrics.ts new file mode 100644 index 00000000000..805f4fc2092 --- /dev/null +++ b/packages/@webex/internal-plugin-metrics/test/unit/spec/business/business-metrics.ts @@ -0,0 +1,120 @@ +import sinon from 'sinon'; +import {assert} from '@webex/test-helper-chai'; +import {BrowserDetection} from '@webex/common'; +import {BusinessMetrics, config, getOSNameInternal} from '@webex/internal-plugin-metrics'; +import uuid from 'uuid'; + +//@ts-ignore +global.window = {location: {hostname: 'whatever'}, navigator: {language: 'language'}}; +process.env.NODE_ENV = 'test'; + +const {getOSVersion, getBrowserName, getBrowserVersion} = BrowserDetection(); + +describe('internal-plugin-metrics', () => { + describe('BusinessMetrics', () => { + let webex; + let now; + let businessMetrics: BusinessMetrics; + + const tags = {key: 'val'}; + + beforeEach(() => { + now = new Date(); + + webex = { + canAuthorize: true, + version: 'webex-version', + internal: { + services: { + get: () => 'locus-url', + }, + metrics: { + submitClientMetrics: sinon.stub(), + config: {...config.metrics}, + }, + newMetrics: {}, + device: { + userId: 'userId', + url: 'https://wdm-intb.ciscospark.com/wdm/api/v1/devices/deviceId', + orgId: 'orgId', + }, + }, + meetings: { + config: { + metrics: { + clientType: 'TEAMS_CLIENT', + subClientType: 'WEB_APP', + clientName: 'Cantina', + }, + }, + geoHintInfo: { + clientAddress: '1.3.4.5', + countryCode: 'UK', + }, + }, + credentials: { + isUnverifiedGuest: false, + }, + prepareFetchOptions: sinon.stub().callsFake((opts: any) => ({...opts, foo: 'bar'})), + request: sinon.stub().resolves({body: {}}), + logger: { + log: sinon.stub(), + error: sinon.stub(), + }, + }; + + sinon.createSandbox(); + sinon.useFakeTimers(now.getTime()); + businessMetrics = new BusinessMetrics({}, {parent: webex}); + sinon.stub(uuid, 'v4').returns('my-fake-id'); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('#sendEvent', () => { + it('should send correctly shaped business event (check name building and internal tagged event building)', () => { + // For some reasons `jest` isn't available when testing form build server - so can't use `jest.fn()` here... + const requestCalls = []; + const request = function(arg) { requestCalls.push(arg) } + + businessMetrics.clientMetricsBatcher.request = request; + + assert.equal(requestCalls.length, 0) + businessMetrics.submitBusinessEvent({ name: "foobar", payload: {bar:"gee"} }) + assert.equal(requestCalls.length, 1) + assert.deepEqual(requestCalls[0], { + eventPayload: { + context: { + app: {version: 'webex-version'}, + device: {id: 'deviceId'}, + locale: 'language', + os: { + name: getOSNameInternal(), + version: getOSVersion(), + }, + }, + metricName: 'foobar', + browserDetails: { + browser: getBrowserName(), + browserHeight: window.innerHeight, + browserVersion: getBrowserVersion(), + browserWidth: window.innerWidth, + domain: window.location.hostname, + inIframe: false, + locale: window.navigator.language, + os: getOSNameInternal(), + }, + timestamp: requestCalls[0].eventPayload.timestamp, // This is to bypass time check, which is checked below. + value: { + bar: "gee" + } + }, + type: ['business'], + }); + assert.isNumber(requestCalls[0].eventPayload.timestamp) + }) + }) + }); +}); diff --git a/packages/@webex/internal-plugin-metrics/test/unit/spec/operational/operational-metrics.ts b/packages/@webex/internal-plugin-metrics/test/unit/spec/operational/operational-metrics.ts new file mode 100644 index 00000000000..4e231552b61 --- /dev/null +++ b/packages/@webex/internal-plugin-metrics/test/unit/spec/operational/operational-metrics.ts @@ -0,0 +1,115 @@ +import sinon from 'sinon'; +import {assert} from '@webex/test-helper-chai'; +import {BrowserDetection} from '@webex/common'; +import {OperationalMetrics, config, getOSNameInternal} from '@webex/internal-plugin-metrics'; +import uuid from 'uuid'; + +//@ts-ignore +global.window = {location: {hostname: 'whatever'}, navigator: {language: 'language'}}; +process.env.NODE_ENV = 'test'; + +const {getOSVersion, getBrowserName, getBrowserVersion} = BrowserDetection(); + +describe('internal-plugin-metrics', () => { + describe('OperationalMetrics', () => { + let webex; + let now; + let operationalMetrics: OperationalMetrics; + + const tags = {key: 'val'}; + + beforeEach(() => { + now = new Date(); + + webex = { + canAuthorize: true, + version: 'webex-version', + internal: { + services: { + get: () => 'locus-url', + }, + metrics: { + submitClientMetrics: sinon.stub(), + config: {...config.metrics}, + }, + newMetrics: {}, + device: { + userId: 'userId', + url: 'https://wdm-intb.ciscospark.com/wdm/api/v1/devices/deviceId', + orgId: 'orgId', + }, + }, + meetings: { + config: { + metrics: { + clientType: 'TEAMS_CLIENT', + subClientType: 'WEB_APP', + clientName: 'Cantina', + }, + }, + geoHintInfo: { + clientAddress: '1.3.4.5', + countryCode: 'UK', + }, + }, + credentials: { + isUnverifiedGuest: false, + }, + prepareFetchOptions: sinon.stub().callsFake((opts: any) => ({...opts, foo: 'bar'})), + request: sinon.stub().resolves({body: {}}), + logger: { + log: sinon.stub(), + error: sinon.stub(), + }, + }; + + sinon.createSandbox(); + sinon.useFakeTimers(now.getTime()); + operationalMetrics = new OperationalMetrics({}, {parent: webex}); + sinon.stub(uuid, 'v4').returns('my-fake-id'); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('#sendEvent', () => { + it('should send correctly shaped operational event (check name building and internal tagged event building)', () => { + // For some reasons `jest` isn't available when testing form build server - so can't use `jest.fn()` here... + const requestCalls = []; + const request = function(arg) { requestCalls.push(arg) } + + operationalMetrics.clientMetricsBatcher.request = request; + + assert.equal(requestCalls.length, 0) + operationalMetrics.submitOperationalEvent({ name: "foobar", payload: {bar:"gee"} }) + assert.equal(requestCalls.length, 1) + assert.deepEqual(requestCalls[0], { + context: { + app: {version: 'webex-version'}, + device: {id: 'deviceId'}, + locale: 'language', + os: { + name: getOSNameInternal(), + version: getOSVersion(), + }, + }, + metricName: 'foobar', + tags: { + browser: getBrowserName(), + browserHeight: window.innerHeight, + browserVersion: getBrowserVersion(), + browserWidth: window.innerWidth, + domain: window.location.hostname, + inIframe: false, + locale: window.navigator.language, + os: getOSNameInternal(), + bar: "gee" + }, + timestamp: requestCalls[0].timestamp, // This is to bypass time check, which is correctly tested in behavioral-metrics tests. + type: ['operational'], + }); + }) + }) + }); +}); From 1eaba02b8de040a7ab0f7ddcadea85b713574235 Mon Sep 17 00:00:00 2001 From: Sreekanth Narayanan <131740035+sreenara@users.noreply.github.com> Date: Mon, 23 Sep 2024 18:39:16 +0530 Subject: [PATCH 24/48] feat(contacts): add the resolved field to contacts (#3848) --- .../src/Contacts/ContactsClient.test.ts | 41 +++++++++++++++++++ .../calling/src/Contacts/ContactsClient.ts | 13 ++++++ packages/calling/src/Contacts/types.ts | 4 +- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/calling/src/Contacts/ContactsClient.test.ts b/packages/calling/src/Contacts/ContactsClient.test.ts index 404023aaba4..b882d31979e 100644 --- a/packages/calling/src/Contacts/ContactsClient.test.ts +++ b/packages/calling/src/Contacts/ContactsClient.test.ts @@ -751,6 +751,47 @@ describe('ContactClient Tests', () => { ownerId: 'ownerId', phoneNumbers: undefined, sipAddresses: undefined, + resolved: true, + }, + ]); + }); + + it("test resolveContacts function when contactsDataMap list doesn't match resolved list", () => { + const mockContact = { + firstName: 'Jane', + lastName: 'Doe', + contactId: 'janeDoe', + }; + + const contact = contactClient['resolveCloudContacts']( + {userId: mockContactMinimum, janeDoe: mockContact}, + mockSCIMMinListResponse.body + ); + + expect(contact).toEqual([ + { + firstName: 'Jane', + lastName: 'Doe', + contactId: 'janeDoe', + resolved: false, + }, + { + avatarURL: '', + avatarUrlDomain: undefined, + contactId: 'userId', + contactType: 'CLOUD', + department: undefined, + displayName: undefined, + emails: undefined, + encryptionKeyUrl: 'kms://cisco.com/keys/dcf18f9d-155e-44ff-ad61-c8a69b7103ab', + firstName: undefined, + groups: ['1561977e-3443-4ccf-a591-69686275d7d2'], + lastName: undefined, + manager: undefined, + ownerId: 'ownerId', + phoneNumbers: undefined, + sipAddresses: undefined, + resolved: true, }, ]); }); diff --git a/packages/calling/src/Contacts/ContactsClient.ts b/packages/calling/src/Contacts/ContactsClient.ts index efb2add327e..0245c7461e3 100644 --- a/packages/calling/src/Contacts/ContactsClient.ts +++ b/packages/calling/src/Contacts/ContactsClient.ts @@ -266,8 +266,20 @@ export class ContactsClient implements IContacts { method: 'resolveCloudContacts', }; const finalContactList: Contact[] = []; + const resolvedList: string[] = []; try { + inputList.Resources.forEach((item) => { + resolvedList.push(item.id); + }); + + Object.values(contactsDataMap).forEach((item) => { + const isResolved = resolvedList.some((listItem) => listItem === item.contactId); + if (!isResolved) { + finalContactList.push({...item, resolved: false}); + } + }); + for (let n = 0; n < inputList.Resources.length; n += 1) { const filteredContact = inputList.Resources[n]; const {displayName, emails, phoneNumbers, photos} = filteredContact; @@ -300,6 +312,7 @@ export class ContactsClient implements IContacts { ownerId, phoneNumbers, sipAddresses, + resolved: true, }; finalContactList.push(cloudContact); diff --git a/packages/calling/src/Contacts/types.ts b/packages/calling/src/Contacts/types.ts index 443f0149034..8a0b01b3568 100644 --- a/packages/calling/src/Contacts/types.ts +++ b/packages/calling/src/Contacts/types.ts @@ -91,9 +91,9 @@ export type Contact = { */ sipAddresses?: URIAddress[]; /** - * This represents the job title of the contact. + * This field indicates whether the contact was resolved successfully. */ - title?: string; + resolved: boolean; }; export enum GroupType { From 0af709735c74970ed6038938fc2b8d0c6fd9752e Mon Sep 17 00:00:00 2001 From: CormacGCisco Date: Tue, 24 Sep 2024 10:14:22 +0100 Subject: [PATCH 25/48] feat(presence-plugin): pass label to aphelia API whilst setting status (#3847) Co-authored-by: Shreyas Sharma <72344404+Shreyas281299@users.noreply.github.com> --- .../internal-plugin-presence/src/presence.js | 1 + .../test/integration/spec/presence.js | 2 ++ .../test/unit/spec/presence.js | 23 +++++++++++++++++++ .../@webex/plugin-presence/src/presence.ts | 1 + .../test/integration/spec/presence.ts | 2 ++ .../test/unit/spec/presence.ts | 23 +++++++++++++++++++ 6 files changed, 52 insertions(+) diff --git a/packages/@webex/internal-plugin-presence/src/presence.js b/packages/@webex/internal-plugin-presence/src/presence.js index 7dce9dcd2b3..0d1440a8d2e 100644 --- a/packages/@webex/internal-plugin-presence/src/presence.js +++ b/packages/@webex/internal-plugin-presence/src/presence.js @@ -234,6 +234,7 @@ const Presence = WebexPlugin.extend({ body: { subject: this.webex.internal.device.userId, eventType: status, + label: this.webex.internal.device.userId, ttl, }, }) diff --git a/packages/@webex/internal-plugin-presence/test/integration/spec/presence.js b/packages/@webex/internal-plugin-presence/test/integration/spec/presence.js index f1cfc656eb9..2b7b0ac6fbb 100644 --- a/packages/@webex/internal-plugin-presence/test/integration/spec/presence.js +++ b/packages/@webex/internal-plugin-presence/test/integration/spec/presence.js @@ -206,8 +206,10 @@ describe.skip('plugin-presence', function () { spock.webex.internal.presence.setStatus('dnd', 1500).then((statusResponse) => { assert.property(statusResponse, 'subject'); assert.property(statusResponse, 'status'); + assert.property(statusResponse, 'label'); assert.equal(statusResponse.subject, spock.id); assert.equal(statusResponse.status, 'dnd'); + assert.equal(statusResponse.label, spock.id); })); }); }); diff --git a/packages/@webex/internal-plugin-presence/test/unit/spec/presence.js b/packages/@webex/internal-plugin-presence/test/unit/spec/presence.js index 105d3804afa..f94a375283e 100644 --- a/packages/@webex/internal-plugin-presence/test/unit/spec/presence.js +++ b/packages/@webex/internal-plugin-presence/test/unit/spec/presence.js @@ -64,6 +64,29 @@ describe.skip('plugin-presence', () => { describe('#setStatus()', () => { it('requires a status', () => assert.isRejected(webex.internal.presence.setStatus(), /A status is required/)); + + it('passes a label to the API', () => { + const testGuid = 'test-guid'; + + webex.internal.device.userId = testGuid; + + webex.request = function (options) { + return Promise.resolve({ + statusCode: 204, + body: [], + options, + }); + }; + sinon.spy(webex, 'request'); + + webex.internal.presence.setStatus('dnd'); + + assert.calledOnce(webex.request); + + const request = webex.request.getCall(0); + + assert.equal(request.args[0].body.label, testGuid); + }); }); }); }); diff --git a/packages/@webex/plugin-presence/src/presence.ts b/packages/@webex/plugin-presence/src/presence.ts index 55df070a032..f7181f3c653 100644 --- a/packages/@webex/plugin-presence/src/presence.ts +++ b/packages/@webex/plugin-presence/src/presence.ts @@ -238,6 +238,7 @@ const Presence: IPresence = WebexPlugin.extend({ body: { subject: this.webex.internal.device.userId, eventType: status, + label: this.webex.internal.device.userId, ttl, }, }) diff --git a/packages/@webex/plugin-presence/test/integration/spec/presence.ts b/packages/@webex/plugin-presence/test/integration/spec/presence.ts index d767e253d23..bd2635817af 100644 --- a/packages/@webex/plugin-presence/test/integration/spec/presence.ts +++ b/packages/@webex/plugin-presence/test/integration/spec/presence.ts @@ -203,8 +203,10 @@ describe.skip('plugin-presence', function () { spock.webex.presence.setStatus('dnd', 1500).then((statusResponse) => { assert.property(statusResponse, 'subject'); assert.property(statusResponse, 'status'); + assert.property(statusResponse, 'label'); assert.equal(statusResponse.subject, spock.id); assert.equal(statusResponse.status, 'dnd'); + assert.equal(statusResponse.subject, spock.id); })); }); }); diff --git a/packages/@webex/plugin-presence/test/unit/spec/presence.ts b/packages/@webex/plugin-presence/test/unit/spec/presence.ts index ac3b8e3baa5..137af9d2bc3 100644 --- a/packages/@webex/plugin-presence/test/unit/spec/presence.ts +++ b/packages/@webex/plugin-presence/test/unit/spec/presence.ts @@ -61,6 +61,29 @@ describe('plugin-presence', () => { describe('#setStatus()', () => { it('requires a status', () => assert.isRejected(webex.presence.setStatus(), /A status is required/)); + + it('passes a label to the API', () => { + const testGuid = 'test-guid'; + + webex.internal.device.userId = testGuid; + + webex.request = function (options) { + return Promise.resolve({ + statusCode: 204, + body: [], + options, + }); + }; + sinon.spy(webex, 'request'); + + webex.presence.setStatus('dnd'); + + assert.calledOnce(webex.request); + + const request = webex.request.getCall(0); + + assert.equal(request.args[0].body.label, testGuid); + }); }); }); }); From b93ddef390703821f320d3776941f99baf2066f7 Mon Sep 17 00:00:00 2001 From: Marcin Date: Tue, 24 Sep 2024 10:53:49 +0100 Subject: [PATCH 26/48] fix(meetings): added reachability trigger to metrics (#3850) --- .../plugin-meetings/src/meetings/index.ts | 7 +- .../plugin-meetings/src/reachability/index.ts | 8 ++- .../src/reconnection-manager/index.ts | 2 +- .../test/unit/spec/meetings/index.js | 33 +++++++++- .../test/unit/spec/reachability/index.ts | 66 +++++++++++++++---- 5 files changed, 94 insertions(+), 22 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/meetings/index.ts b/packages/@webex/plugin-meetings/src/meetings/index.ts index 52481a6aee8..ccbf479842d 100644 --- a/packages/@webex/plugin-meetings/src/meetings/index.ts +++ b/packages/@webex/plugin-meetings/src/meetings/index.ts @@ -765,7 +765,7 @@ export default class Meetings extends WebexPlugin { return Promise.all([ this.fetchUserPreferredWebexSite(), this.getGeoHint(), - this.startReachability().catch((error) => { + this.startReachability('registration').catch((error) => { LoggerProxy.logger.error(`Meetings:index#register --> GDM error, ${error.message}`); }), // @ts-ignore @@ -967,12 +967,13 @@ export default class Meetings extends WebexPlugin { /** * initializes and starts gathering reachability for Meetings + * @param {string} trigger - explains the reason for starting reachability * @returns {Promise} * @public * @memberof Meetings */ - startReachability() { - return this.getReachability().gatherReachability(); + startReachability(trigger = 'client') { + return this.getReachability().gatherReachability(trigger); } /** diff --git a/packages/@webex/plugin-meetings/src/reachability/index.ts b/packages/@webex/plugin-meetings/src/reachability/index.ts index 17b60313ab0..7b6868f04f0 100644 --- a/packages/@webex/plugin-meetings/src/reachability/index.ts +++ b/packages/@webex/plugin-meetings/src/reachability/index.ts @@ -93,6 +93,8 @@ export default class Reachability extends EventsScope { expectedResultsCount = {videoMesh: {udp: 0}, public: {udp: 0, tcp: 0, xtls: 0}}; resultsCount = {videoMesh: {udp: 0}, public: {udp: 0, tcp: 0, xtls: 0}}; + protected lastTrigger?: string; + /** * Creates an instance of Reachability. * @param {object} webex @@ -142,13 +144,16 @@ export default class Reachability extends EventsScope { /** * Gets a list of media clusters from the backend and performs reachability checks on all the clusters + * @param {string} trigger - explains the reason for starting reachability * @returns {Promise} reachability results * @public * @memberof Reachability */ - public async gatherReachability(): Promise { + public async gatherReachability(trigger: string): Promise { // Fetch clusters and measure latency try { + this.lastTrigger = trigger; + // kick off ip version detection. For now we don't await it, as we're doing it // to gather the timings and send them with our reachability metrics // @ts-ignore @@ -552,6 +557,7 @@ export default class Reachability extends EventsScope { // @ts-ignore totalTime: this.webex.internal.device.ipNetworkDetector.totalTime, }, + trigger: this.lastTrigger, }; Metrics.sendBehavioralMetric( BEHAVIORAL_METRICS.REACHABILITY_COMPLETED, diff --git a/packages/@webex/plugin-meetings/src/reconnection-manager/index.ts b/packages/@webex/plugin-meetings/src/reconnection-manager/index.ts index ea2c47f05f4..55ca4a764c6 100644 --- a/packages/@webex/plugin-meetings/src/reconnection-manager/index.ts +++ b/packages/@webex/plugin-meetings/src/reconnection-manager/index.ts @@ -342,7 +342,7 @@ export default class ReconnectionManager { } try { - await this.webex.meetings.startReachability(); + await this.webex.meetings.startReachability('reconnection'); } catch (err) { LoggerProxy.logger.info( 'ReconnectionManager:index#reconnect --> Reachability failed, continuing with reconnection attempt, err: ', diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meetings/index.js b/packages/@webex/plugin-meetings/test/unit/spec/meetings/index.js index 9775a52bb97..937fa1fba73 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meetings/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meetings/index.js @@ -79,6 +79,7 @@ describe('plugin-meetings', () => { let locusInfo; let services; let catalog; + let startReachabilityStub; describe('meetings index', () => { beforeEach(() => { @@ -129,9 +130,7 @@ describe('plugin-meetings', () => { logger, }); - Object.assign(webex.meetings, { - startReachability: sinon.stub().returns(Promise.resolve()), - }); + startReachabilityStub = sinon.stub(webex.meetings, 'startReachability').resolves(); Object.assign(webex.internal, { llm: {on: sinon.stub()}, @@ -197,6 +196,34 @@ describe('plugin-meetings', () => { assert.calledOnce(MeetingsUtil.checkH264Support); }); + describe('#startReachability', () => { + let gatherReachabilitySpy; + let fakeResult = {id: 'fake-result'}; + + beforeEach(() => { + startReachabilityStub.restore(); + gatherReachabilitySpy = sinon + .stub(webex.meetings.getReachability(), 'gatherReachability') + .resolves(fakeResult); + }); + + it('should gather reachability with default trigger value', async () => { + const result = await webex.meetings.startReachability(); + + assert.calledOnceWithExactly(gatherReachabilitySpy, 'client'); + assert.equal(result, fakeResult); + }); + + it('should gather reachability and pass custom trigger value', async () => { + const trigger = 'custom-trigger'; + + const result = await webex.meetings.startReachability(trigger); + + assert.calledOnceWithExactly(gatherReachabilitySpy, trigger); + assert.equal(result, fakeResult); + }); + }); + describe('#_toggleUnifiedMeetings', () => { it('should have toggleUnifiedMeetings', () => { assert.equal(typeof webex.meetings._toggleUnifiedMeetings, 'function'); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/reachability/index.ts b/packages/@webex/plugin-meetings/test/unit/spec/reachability/index.ts index 2e400817b5e..29b9ae371ec 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/reachability/index.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/reachability/index.ts @@ -4,7 +4,6 @@ import sinon from 'sinon'; import EventEmitter from 'events'; import testUtils from '../../../utils/testUtils'; import Reachability, { - ReachabilityResults, ReachabilityResultsForBackend, } from '@webex/plugin-meetings/src/reachability/'; import {ClusterNode} from '../../../../src/reachability/request'; @@ -507,6 +506,11 @@ describe('gatherReachability', () => { mockClusterReachabilityInstances[id] = mockInstance; return mockInstance; }); + + webex.config.meetings.experimental = { + enableTcpReachability: false, + enableTlsReachability: false, + }; }); afterEach(() => { @@ -1044,13 +1048,14 @@ describe('gatherReachability', () => { enableTlsReachability: true, }; - // the metrics related to ipver are not tested in these tests and are all the same, so setting them up here + // the metrics related to ipver and trigger are not tested in these tests and are all the same, so setting them up here const expectedMetricsFull = { ...expectedMetrics, ipver_firstIpV4: -1, ipver_firstIpV6: -1, ipver_firstMdns: -1, ipver_totalTime: -1, + trigger: 'test', }; const receivedEvents = { @@ -1082,7 +1087,7 @@ describe('gatherReachability', () => { reachability.reachabilityRequest.getClusters = sinon.stub().returns(mockGetClustersResult); - const resultPromise = reachability.gatherReachability(); + const resultPromise = reachability.gatherReachability('test'); await testUtils.flushPromises(); @@ -1142,6 +1147,38 @@ describe('gatherReachability', () => { }) ); + it('sends the trigger parameter in the metrics', async () => { + const reachability = new TestReachability(webex); + + const mockGetClustersResult = { + clusters: { + clusterA: { + udp: ['udp-url'], + tcp: [], + xtls: [], + isVideoMesh: false, + }, + }, + joinCookie: {id: 'id'}, + }; + + reachability.reachabilityRequest.getClusters = sinon.stub().returns(mockGetClustersResult); + + const resultPromise = reachability.gatherReachability('some trigger'); + + // let it time out + await testUtils.flushPromises(); + clock.tick(15000); + await resultPromise; + + // check the metric contains the right trigger value + assert.calledWith( + Metrics.sendBehavioralMetric, + 'js_sdk_reachability_completed', + sinon.match({trigger: 'some trigger'}) + ); + }); + it(`starts ip network version detection and includes the results in the metrics`, async () => { webex.config.meetings.experimental = { enableTcpReachability: true, @@ -1180,7 +1217,7 @@ describe('gatherReachability', () => { joinCookie: {id: 'id'}, }); - const resultPromise = reachability.gatherReachability(); + const resultPromise = reachability.gatherReachability('test'); await testUtils.flushPromises(); @@ -1217,6 +1254,7 @@ describe('gatherReachability', () => { ipver_firstIpV6: webex.internal.device.ipNetworkDetector.firstIpV6, ipver_firstMdns: webex.internal.device.ipNetworkDetector.firstMdns, ipver_totalTime: webex.internal.device.ipNetworkDetector.totalTime, + trigger: 'test', }); }); @@ -1248,7 +1286,7 @@ describe('gatherReachability', () => { reachability.reachabilityRequest.getClusters = sinon.stub().returns(mockGetClustersResult); - const resultPromise = reachability.gatherReachability(); + const resultPromise = reachability.gatherReachability('test'); await testUtils.flushPromises(); @@ -1341,7 +1379,7 @@ describe('gatherReachability', () => { reachability.reachabilityRequest.getClusters = sinon.stub().returns(mockGetClustersResult); - const resultPromise = reachability.gatherReachability(); + const resultPromise = reachability.gatherReachability('test'); await testUtils.flushPromises(); @@ -1382,7 +1420,7 @@ describe('gatherReachability', () => { reachability.reachabilityRequest.getClusters = sinon.stub().throws(); - const result = await reachability.gatherReachability(); + const result = await reachability.gatherReachability('test'); assert.empty(result); @@ -1400,7 +1438,7 @@ describe('gatherReachability', () => { reachability.reachabilityRequest.getClusters = sinon.stub().returns(getClustersResult); (reachability as any).performReachabilityChecks = sinon.stub().throws(); - const result = await reachability.gatherReachability(); + const result = await reachability.gatherReachability('test'); assert.empty(result); @@ -1435,7 +1473,7 @@ describe('gatherReachability', () => { reachability.reachabilityRequest.getClusters = sinon.stub().returns(getClustersResult); - const promise = reachability.gatherReachability(); + const promise = reachability.gatherReachability('test'); await simulateTimeout(); await promise; @@ -1481,7 +1519,7 @@ describe('gatherReachability', () => { reachability.reachabilityRequest.getClusters = sinon.stub().returns(getClustersResult); - const promise = reachability.gatherReachability(); + const promise = reachability.gatherReachability('test'); await simulateTimeout(); await promise; @@ -1515,7 +1553,7 @@ describe('gatherReachability', () => { reachability.reachabilityRequest.getClusters = sinon.stub().returns(getClustersResult); - const promise = reachability.gatherReachability(); + const promise = reachability.gatherReachability('test'); await simulateTimeout(); await promise; @@ -1550,7 +1588,7 @@ describe('gatherReachability', () => { reachability.reachabilityRequest.getClusters = sinon.stub().returns(getClustersResult); - const promise = reachability.gatherReachability(); + const promise = reachability.gatherReachability('test'); await simulateTimeout(); await promise; @@ -1595,7 +1633,7 @@ describe('gatherReachability', () => { return getClustersResult; }); - const promise = reachability.gatherReachability(); + const promise = reachability.gatherReachability('test'); await simulateTimeout(); await promise; @@ -1616,7 +1654,7 @@ describe('gatherReachability', () => { throw new Error('fake error'); }); - const promise = reachability.gatherReachability(); + const promise = reachability.gatherReachability('test'); await simulateTimeout(); From 8f8e8805bc9087ab218ee34de8b020456b15c567 Mon Sep 17 00:00:00 2001 From: Sreekanth Narayanan <131740035+sreenara@users.noreply.github.com> Date: Thu, 26 Sep 2024 10:22:12 +0530 Subject: [PATCH 27/48] fix(media-effects): include fix for sluggy video in background tab (#3845) --- docs/samples/browser-plugin-meetings/app.js | 1 + packages/@webex/media-helpers/package.json | 2 +- yarn.lock | 21 +++++++++++---------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/samples/browser-plugin-meetings/app.js b/docs/samples/browser-plugin-meetings/app.js index c3a0671785a..04e4d41dfd0 100644 --- a/docs/samples/browser-plugin-meetings/app.js +++ b/docs/samples/browser-plugin-meetings/app.js @@ -1406,6 +1406,7 @@ async function handleVbg() { "bgImageUrl": blurVBGImageUrl, "bgVideoUrl": blurVBGVideoUrl, env: integrationEnv.checked ? 'int' : 'prod', + preventBackgroundThrottling: true, }); handleEffectsButton(toggleVbgBtn, VBG, effect); await localMedia.cameraStream.addEffect(effect); diff --git a/packages/@webex/media-helpers/package.json b/packages/@webex/media-helpers/package.json index b086980d52d..ea9be252484 100644 --- a/packages/@webex/media-helpers/package.json +++ b/packages/@webex/media-helpers/package.json @@ -24,7 +24,7 @@ "dependencies": { "@webex/internal-media-core": "2.10.2", "@webex/ts-events": "^1.1.0", - "@webex/web-media-effects": "2.18.0" + "@webex/web-media-effects": "2.19.0" }, "browserify": { "transform": [ diff --git a/yarn.lock b/yarn.lock index 081ac9f3761..0eb3dd46be3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8489,7 +8489,7 @@ __metadata: "@webex/test-helper-chai": "workspace:*" "@webex/test-helper-mock-webex": "workspace:*" "@webex/ts-events": ^1.1.0 - "@webex/web-media-effects": 2.18.0 + "@webex/web-media-effects": 2.19.0 eslint: ^8.24.0 jsdom-global: 3.0.2 sinon: ^9.2.4 @@ -9441,16 +9441,17 @@ __metadata: languageName: node linkType: hard -"@webex/web-media-effects@npm:2.18.0": - version: 2.18.0 - resolution: "@webex/web-media-effects@npm:2.18.0" +"@webex/web-media-effects@npm:2.19.0": + version: 2.19.0 + resolution: "@webex/web-media-effects@npm:2.19.0" dependencies: - "@webex/ladon-ts": "npm:^4.3.0" - events: "npm:^3.3.0" - js-logger: "npm:^1.6.1" - typed-emitter: "npm:^1.4.0" - uuid: "npm:^9.0.1" - checksum: d2b4bcdcc2af87c0bed9c753fa06a34ac663e703c7a1e0bbf542558e23667b9a15b3c4ca594c9b4c8af1a28445f0afd7b4e622c411b0a8c567c84997f2fe44c2 + "@webex/ladon-ts": ^4.3.0 + events: ^3.3.0 + js-logger: ^1.6.1 + typed-emitter: ^1.4.0 + uuid: ^9.0.1 + worker-timers: ^8.0.2 + checksum: 04c100b8eb01fbe05cc5ee1b1898c54f2ef537a9e367c7693767f6fb1df1d085c00ba91ee42d7bb9e9df55baeccd43d4ea979dcd486223a4dc3a82c61769494f languageName: node linkType: hard From 0bf3d24251f00f1a5a3dd9b7fb22bb5c22f30ea1 Mon Sep 17 00:00:00 2001 From: Marcin Date: Thu, 26 Sep 2024 15:56:31 +0100 Subject: [PATCH 28/48] fix(meetings): webex.internal.device doesn't know when user is in a meeting (#3854) --- packages/@webex/plugin-meetings/src/meeting/index.ts | 3 +++ packages/@webex/plugin-meetings/src/meeting/util.ts | 2 ++ .../@webex/plugin-meetings/test/unit/spec/meeting/index.js | 1 + .../@webex/plugin-meetings/test/unit/spec/meeting/utils.js | 3 +++ packages/@webex/test-helper-mock-webex/src/index.js | 2 ++ 5 files changed, 11 insertions(+) diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index 8a5d0ed05f2..49f2b4e458a 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -5232,6 +5232,9 @@ export default class Meeting extends StatelessWebexPlugin { this.meetingFiniteStateMachine.join(); this.setupLocusMediaRequest(); + // @ts-ignore + this.webex.internal.device.meetingStarted(); + LoggerProxy.logger.log('Meeting:index#join --> Success'); Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.JOIN_SUCCESS, { diff --git a/packages/@webex/plugin-meetings/src/meeting/util.ts b/packages/@webex/plugin-meetings/src/meeting/util.ts index 40159ddb8a9..a67fce6dd65 100644 --- a/packages/@webex/plugin-meetings/src/meeting/util.ts +++ b/packages/@webex/plugin-meetings/src/meeting/util.ts @@ -170,6 +170,8 @@ const MeetingUtil = { }, cleanUp: (meeting) => { + meeting.getWebexObject().internal.device.meetingEnded(); + meeting.breakouts.cleanUp(); meeting.simultaneousInterpretation.cleanUp(); meeting.locusMediaRequest = undefined; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js index 85b5bd30df5..fc9048a9a57 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js @@ -1605,6 +1605,7 @@ describe('plugin-meetings', () => { const result = await join; assert.calledOnce(MeetingUtil.joinMeeting); + assert.calledOnce(webex.internal.device.meetingStarted); assert.calledOnce(meeting.setLocus); assert.equal(result, joinMeetingResult); assert.calledWith(webex.internal.llm.on, 'online', meeting.handleLLMOnline); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/utils.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/utils.js index f02c372960a..424846b1798 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/utils.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/utils.js @@ -71,6 +71,7 @@ describe('plugin-meetings', () => { assert.calledOnce(meeting.updateLLMConnection); assert.calledOnce(meeting.breakouts.cleanUp); assert.calledOnce(meeting.simultaneousInterpretation.cleanUp); + assert.calledOnce(webex.internal.device.meetingEnded); }); it('do clean up on meeting object with LLM disabled', async () => { @@ -87,6 +88,7 @@ describe('plugin-meetings', () => { assert.notCalled(meeting.updateLLMConnection); assert.calledOnce(meeting.breakouts.cleanUp); assert.calledOnce(meeting.simultaneousInterpretation.cleanUp); + assert.calledOnce(webex.internal.device.meetingEnded); }); it('do clean up on meeting object with no config', async () => { @@ -102,6 +104,7 @@ describe('plugin-meetings', () => { assert.notCalled(meeting.updateLLMConnection); assert.calledOnce(meeting.breakouts.cleanUp); assert.calledOnce(meeting.simultaneousInterpretation.cleanUp); + assert.calledOnce(webex.internal.device.meetingEnded); }); }); diff --git a/packages/@webex/test-helper-mock-webex/src/index.js b/packages/@webex/test-helper-mock-webex/src/index.js index bfd92df62c3..255afe3742f 100644 --- a/packages/@webex/test-helper-mock-webex/src/index.js +++ b/packages/@webex/test-helper-mock-webex/src/index.js @@ -268,6 +268,8 @@ function makeWebex(options) { get: sinon.stub(), }, }, + meetingEnded: sinon.stub(), + meetingStarted: sinon.stub(), registered: true, register: sinon.stub().returns(Promise.resolve()), ipNetworkDetector: { From 19c6bf5c70fdcc30e70b8c48fe172bbe28d6595f Mon Sep 17 00:00:00 2001 From: Anna Tsukanova <38460776+antsukanova@users.noreply.github.com> Date: Fri, 27 Sep 2024 07:04:07 +0200 Subject: [PATCH 29/48] fix: update internal-media-core with conditional device permissions fix (#3862) --- packages/@webex/media-helpers/package.json | 2 +- packages/@webex/plugin-meetings/package.json | 2 +- packages/calling/package.json | 2 +- yarn.lock | 76 ++++++++++---------- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/packages/@webex/media-helpers/package.json b/packages/@webex/media-helpers/package.json index ea9be252484..1943ef07360 100644 --- a/packages/@webex/media-helpers/package.json +++ b/packages/@webex/media-helpers/package.json @@ -22,7 +22,7 @@ "deploy:npm": "yarn npm publish" }, "dependencies": { - "@webex/internal-media-core": "2.10.2", + "@webex/internal-media-core": "2.11.1", "@webex/ts-events": "^1.1.0", "@webex/web-media-effects": "2.19.0" }, diff --git a/packages/@webex/plugin-meetings/package.json b/packages/@webex/plugin-meetings/package.json index e3f65d30687..367a01d99d7 100644 --- a/packages/@webex/plugin-meetings/package.json +++ b/packages/@webex/plugin-meetings/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@webex/common": "workspace:*", - "@webex/internal-media-core": "2.10.2", + "@webex/internal-media-core": "2.11.1", "@webex/internal-plugin-conversation": "workspace:*", "@webex/internal-plugin-device": "workspace:*", "@webex/internal-plugin-llm": "workspace:*", diff --git a/packages/calling/package.json b/packages/calling/package.json index 0d975f4a253..a1d969ff25c 100644 --- a/packages/calling/package.json +++ b/packages/calling/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "@types/platform": "1.3.4", - "@webex/internal-media-core": "2.10.2", + "@webex/internal-media-core": "2.11.1", "@webex/media-helpers": "workspace:*", "async-mutex": "0.4.0", "buffer": "6.0.3", diff --git a/yarn.lock b/yarn.lock index 0eb3dd46be3..cac23f71339 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7419,7 +7419,7 @@ __metadata: "@typescript-eslint/eslint-plugin": 5.38.1 "@typescript-eslint/parser": 5.38.1 "@web/dev-server": 0.4.5 - "@webex/internal-media-core": 2.10.2 + "@webex/internal-media-core": 2.11.1 "@webex/media-helpers": "workspace:*" async-mutex: 0.4.0 buffer: 6.0.3 @@ -7710,21 +7710,22 @@ __metadata: languageName: unknown linkType: soft -"@webex/internal-media-core@npm:2.10.2": - version: 2.10.2 - resolution: "@webex/internal-media-core@npm:2.10.2" +"@webex/internal-media-core@npm:2.11.1": + version: 2.11.1 + resolution: "@webex/internal-media-core@npm:2.11.1" dependencies: "@babel/runtime": ^7.18.9 "@babel/runtime-corejs2": ^7.25.0 + "@webex/rtcstats": ^1.5.0 "@webex/ts-sdp": 1.7.0 "@webex/web-capabilities": ^1.4.1 - "@webex/web-client-media-engine": 3.23.1 + "@webex/web-client-media-engine": 3.24.2 events: ^3.3.0 typed-emitter: ^2.1.0 uuid: ^8.3.2 webrtc-adapter: ^8.1.2 xstate: ^4.30.6 - checksum: 76fe4feba27b6734fde24dd0b84f0a43d24b6faf9110ced0ab0c8122746ffa27b50cd5862dde1192750432f2daff618c6ca0cb4ca9496e1b9a2ba47788a58174 + checksum: db7d9d40b355b4e5a317f5eadc15a9d7ee5ee62554e9b31f1003c4b6622c36ddc93f378a7a5a3154be2017346636ccfc8a0cb854ddaddc21e7c3b0d359f1aff0 languageName: node linkType: hard @@ -8391,10 +8392,10 @@ __metadata: languageName: unknown linkType: soft -"@webex/json-multistream@npm:2.1.3": - version: 2.1.3 - resolution: "@webex/json-multistream@npm:2.1.3" - checksum: bf95a540c0509b15798cb9ead0f31518e79766ae1633119a83f40e22d175f6cf98338c24246c204185591932a08f9ede7bd3d8d65f46f7c212e57103295200e4 +"@webex/json-multistream@npm:2.1.6": + version: 2.1.6 + resolution: "@webex/json-multistream@npm:2.1.6" + checksum: 18cd8e24151c88fc563c6224cc358c9e2e3cda78d80baddba8dd58aa3e79bf4d78ff12613b27cad5a0242856e84e3c6001e12916e404a68398c68e5439e5154b languageName: node linkType: hard @@ -8483,7 +8484,7 @@ __metadata: "@babel/preset-typescript": 7.22.11 "@webex/babel-config-legacy": "workspace:*" "@webex/eslint-config-legacy": "workspace:*" - "@webex/internal-media-core": 2.10.2 + "@webex/internal-media-core": 2.11.1 "@webex/jest-config-legacy": "workspace:*" "@webex/legacy-tools": "workspace:*" "@webex/test-helper-chai": "workspace:*" @@ -8719,7 +8720,7 @@ __metadata: "@webex/babel-config-legacy": "workspace:*" "@webex/common": "workspace:*" "@webex/eslint-config-legacy": "workspace:*" - "@webex/internal-media-core": 2.10.2 + "@webex/internal-media-core": 2.11.1 "@webex/internal-plugin-conversation": "workspace:*" "@webex/internal-plugin-device": "workspace:*" "@webex/internal-plugin-llm": "workspace:*" @@ -9011,13 +9012,13 @@ __metadata: languageName: unknown linkType: soft -"@webex/rtcstats@npm:^1.3.2": - version: 1.4.0 - resolution: "@webex/rtcstats@npm:1.4.0" +"@webex/rtcstats@npm:^1.5.0": + version: 1.5.0 + resolution: "@webex/rtcstats@npm:1.5.0" dependencies: "@types/node": ^20.14.1 uuid: ^8.3.2 - checksum: a83455d93f66b39e4c2f694d7665fca5d7da9eab33431d9c09262f438971085fecb664f7f8bb6775fec63b62af01612f3eb6b48125619e3a532a8b764019520b + checksum: d7af2b4be63a146de7eca11bbfa6478e3b4504c2d9df971315ca9ac07026c83813742ef9dcd019cbb38a9a74aebcd569a03436e1c1c35e087efcf9e682ddf8ff languageName: node linkType: hard @@ -9422,42 +9423,41 @@ __metadata: languageName: node linkType: hard -"@webex/web-client-media-engine@npm:3.23.1": - version: 3.23.1 - resolution: "@webex/web-client-media-engine@npm:3.23.1" +"@webex/web-client-media-engine@npm:3.24.2": + version: 3.24.2 + resolution: "@webex/web-client-media-engine@npm:3.24.2" dependencies: - "@webex/json-multistream": 2.1.3 - "@webex/rtcstats": ^1.3.2 + "@webex/json-multistream": 2.1.6 + "@webex/rtcstats": ^1.5.0 "@webex/ts-events": ^1.0.1 "@webex/ts-sdp": 1.7.0 "@webex/web-capabilities": ^1.4.0 - "@webex/web-media-effects": ^2.15.6 - "@webex/webrtc-core": 2.10.0 + "@webex/web-media-effects": 2.18.1 + "@webex/webrtc-core": 2.10.3 async: ^3.2.4 js-logger: ^1.6.1 typed-emitter: ^2.1.0 uuid: ^8.3.2 - checksum: e1a502725ac0ad588891e4ec0e73ffa5242d44fc612161a4c09d9f5f3a31734f22be28c8bf41838d3ae779e5f5689933f0b8deeb325c755499d3648d6e8035a5 + checksum: 26ac75ffcb519a11b3a9f2b601b13b85c4c4e4b685503da0e752d03137af65d22472329f45fbd08de138d93ebd92295645f98c9b879617c838afee61c0589df7 languageName: node linkType: hard -"@webex/web-media-effects@npm:2.19.0": - version: 2.19.0 - resolution: "@webex/web-media-effects@npm:2.19.0" +"@webex/web-media-effects@npm:2.18.1": + version: 2.18.1 + resolution: "@webex/web-media-effects@npm:2.18.1" dependencies: "@webex/ladon-ts": ^4.3.0 events: ^3.3.0 js-logger: ^1.6.1 typed-emitter: ^1.4.0 uuid: ^9.0.1 - worker-timers: ^8.0.2 - checksum: 04c100b8eb01fbe05cc5ee1b1898c54f2ef537a9e367c7693767f6fb1df1d085c00ba91ee42d7bb9e9df55baeccd43d4ea979dcd486223a4dc3a82c61769494f + checksum: c60bf96a9d3efc579101de05dce26c9b03b7d1b8629b02294408e5cb3bc17e50ceed7c33bfdd78248894ce3ab728702549cb103cc3e9c886aaa702531d026c62 languageName: node linkType: hard -"@webex/web-media-effects@npm:^2.15.6": - version: 2.20.8 - resolution: "@webex/web-media-effects@npm:2.20.8" +"@webex/web-media-effects@npm:2.19.0": + version: 2.19.0 + resolution: "@webex/web-media-effects@npm:2.19.0" dependencies: "@webex/ladon-ts": ^4.3.0 events: ^3.3.0 @@ -9465,7 +9465,7 @@ __metadata: typed-emitter: ^1.4.0 uuid: ^9.0.1 worker-timers: ^8.0.2 - checksum: 97d51ff86bcb7880f18b3615dcd745df826758bb93cfd1c076fd60357ff57c2dd00b7ba4a609b0c71fa5b7caf39f5443c0fc371f1ed4486fa2aafe42a4b50d0d + checksum: 04c100b8eb01fbe05cc5ee1b1898c54f2ef537a9e367c7693767f6fb1df1d085c00ba91ee42d7bb9e9df55baeccd43d4ea979dcd486223a4dc3a82c61769494f languageName: node linkType: hard @@ -9559,18 +9559,18 @@ __metadata: languageName: unknown linkType: soft -"@webex/webrtc-core@npm:2.10.0": - version: 2.10.0 - resolution: "@webex/webrtc-core@npm:2.10.0" +"@webex/webrtc-core@npm:2.10.3": + version: 2.10.3 + resolution: "@webex/webrtc-core@npm:2.10.3" dependencies: "@webex/ts-events": ^1.1.0 "@webex/web-capabilities": ^1.1.0 - "@webex/web-media-effects": ^2.15.6 + "@webex/web-media-effects": 2.18.1 events: ^3.3.0 js-logger: ^1.6.1 typed-emitter: ^2.1.0 webrtc-adapter: ^8.1.2 - checksum: f166a1b04e9f2c4a288174960fe04bcf8ccf23485ecbdf2a18e09a3c65fe836cc0e97370864b806115c7701e116a1873fd79021c799e04fcbb44e1ef4dbc4c17 + checksum: 9b945ffb0046082967317704d139e2f5d68b4db56bae3fc0942aaa5a9c53740edd56617596873373cb3b3ccae376b6fc5eefcff1cb293c017c0b5ace49ad11e3 languageName: node linkType: hard From ed1562614d8471c5c47b4c18b9ecc2d41e15892c Mon Sep 17 00:00:00 2001 From: Charles Burkett Date: Fri, 27 Sep 2024 07:21:34 -0400 Subject: [PATCH 30/48] feat(ca-metrics): allow override of buildType when running e2e tests against prod (#3856) --- .../call-diagnostic-metrics.util.ts | 7 +++++++ .../call-diagnostic-metrics.util.ts | 20 ++++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.util.ts b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.util.ts index a0ba6e791eb..06129964f05 100644 --- a/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.util.ts +++ b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.util.ts @@ -195,10 +195,12 @@ export const isBrowserMediaErrorName = (errorName: any) => { }; /** + * @param {Object} webex sdk instance * @param webClientDomain * @returns */ export const getBuildType = ( + webex, webClientDomain, markAsTestEvent = false ): Event['origin']['buildType'] => { @@ -207,6 +209,10 @@ export const getBuildType = ( return 'test'; } + if (webex.internal.metrics?.config?.caBuildType) { + return webex.internal.metrics.config.caBuildType; + } + if ( webClientDomain?.includes('localhost') || webClientDomain?.includes('127.0.0.1') || @@ -227,6 +233,7 @@ export const getBuildType = ( export const prepareDiagnosticMetricItem = (webex: any, item: any) => { const origin: Partial = { buildType: getBuildType( + webex, item.eventPayload?.event?.eventData?.webClientDomain, item.eventPayload?.event?.eventData?.markAsTestEvent ), diff --git a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts index 0d18c9b417a..1a1768c6e8c 100644 --- a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts +++ b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts @@ -235,6 +235,8 @@ describe('internal-plugin-metrics', () => { }); describe('getBuildType', () => { + const webex = {internal: {metrics: {config: {}}}}; + beforeEach(() => { process.env.NODE_ENV = 'production'; }); @@ -246,18 +248,26 @@ describe('internal-plugin-metrics', () => { ['https://web.webex.com', true, 'test'], ].forEach(([webClientDomain, markAsTestEvent, expected]) => { it(`returns expected result for ${webClientDomain}`, () => { - assert.deepEqual(getBuildType(webClientDomain, markAsTestEvent as any), expected); + assert.deepEqual(getBuildType(webex, webClientDomain, markAsTestEvent as any), expected); }); }); it('returns "test" for NODE_ENV "foo"', () => { process.env.NODE_ENV = 'foo'; - assert.deepEqual(getBuildType('production'), 'test'); + assert.deepEqual(getBuildType(webex, 'production'), 'test'); }); it('returns "test" for NODE_ENV "production" and markAsTestEvent = true', () => { process.env.NODE_ENV = 'production'; - assert.deepEqual(getBuildType('my.domain', true), 'test'); + assert.deepEqual(getBuildType(webex, 'my.domain', true), 'test'); + }); + + it('returns "test" for NODE_ENV "production" when webex.caBuildType = "test"', () => { + process.env.NODE_ENV = 'production'; + assert.deepEqual( + getBuildType({internal: {metrics: {config: {caBuildType: 'test'}}}}, 'my.domain'), + 'test' + ); }); }); @@ -418,8 +428,8 @@ describe('internal-plugin-metrics', () => { name: 'client.exit.app', eventData: { markAsTestEvent: true, - webClientDomain: 'https://web.webex.com' - } + webClientDomain: 'https://web.webex.com', + }, }, }, type: ['diagnostic-event'], From a4ae02d7f2369058a726bb72f62b6b8f1b496d44 Mon Sep 17 00:00:00 2001 From: akulakum <74420487+akulakum@users.noreply.github.com> Date: Fri, 27 Sep 2024 19:32:03 +0530 Subject: [PATCH 31/48] fix(voicemail): process check in metrics for voicemail (#3867) --- packages/calling/src/Metrics/index.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/calling/src/Metrics/index.ts b/packages/calling/src/Metrics/index.ts index 2f60c64bb57..85816286f5d 100644 --- a/packages/calling/src/Metrics/index.ts +++ b/packages/calling/src/Metrics/index.ts @@ -285,7 +285,10 @@ class MetricManager implements IMetricManager { }, fields: { device_url: this.deviceInfo?.device?.clientDeviceUri, - calling_sdk_version: process.env.CALLING_SDK_VERSION || VERSION, + calling_sdk_version: + process && process.env.CALLING_SDK_VERSION + ? process.env.CALLING_SDK_VERSION + : VERSION, }, type, }; From aeacec7f4dcb383ce983b480feae69934c5b76b8 Mon Sep 17 00:00:00 2001 From: covidal-cisco Date: Fri, 27 Sep 2024 17:49:14 +0200 Subject: [PATCH 32/48] feat(internal-plugin-metrics): lazy build metrics backend follow-up (#3851) Co-authored-by: chrisadubois --- .../src/business-metrics.ts | 4 +- .../src/new-metrics.ts | 51 ++++++++++++++----- .../unit/spec/business/business-metrics.ts | 6 +-- .../test/unit/spec/new-metrics.ts | 14 +++++ 4 files changed, 58 insertions(+), 17 deletions(-) diff --git a/packages/@webex/internal-plugin-metrics/src/business-metrics.ts b/packages/@webex/internal-plugin-metrics/src/business-metrics.ts index 80b023e36d2..a86d7bc9651 100644 --- a/packages/@webex/internal-plugin-metrics/src/business-metrics.ts +++ b/packages/@webex/internal-plugin-metrics/src/business-metrics.ts @@ -17,8 +17,8 @@ export default class BusinessMetrics extends GenericMetrics { const event = { type: ['business'], eventPayload: { - metricName: name, - timestamp: Date.now(), + key: name, + client_timestamp: Date.now(), context: this.getContext(), browserDetails: this.getBrowserDetails(), value: payload, diff --git a/packages/@webex/internal-plugin-metrics/src/new-metrics.ts b/packages/@webex/internal-plugin-metrics/src/new-metrics.ts index 2f0df927265..902ce86c948 100644 --- a/packages/@webex/internal-plugin-metrics/src/new-metrics.ts +++ b/packages/@webex/internal-plugin-metrics/src/new-metrics.ts @@ -90,10 +90,42 @@ class Metrics extends WebexPlugin { } } + /** + * if webex metrics is ready, build behavioral metric backend if not already done. + */ + private lazyBuildBehavioralMetrics() { + if (this.isReady && !this.behavioralMetrics) { + // @ts-ignore + this.behavioralMetrics = new BehavioralMetrics({}, {parent: this.webex}); + } + } + + /** + * if webex metrics is ready, build operational metric backend if not already done. + */ + private lazyBuildOperationalMetrics() { + if (this.isReady && !this.operationalMetrics) { + // @ts-ignore + this.operationalMetrics = new OperationalMetrics({}, {parent: this.webex}); + } + } + + /** + * if webex metrics is ready, build business metric backend if not already done. + */ + private lazyBuildBusinessMetrics() { + if (this.isReady && !this.businessMetrics) { + // @ts-ignore + this.businessMetrics = new BusinessMetrics({}, {parent: this.webex}); + } + } + /** * @returns true once we have the deviceId we need to submit behavioral events to Amplitude */ isReadyToSubmitBehavioralEvents() { + this.lazyBuildBehavioralMetrics(); + return this.behavioralMetrics?.isReadyToSubmitEvents() ?? false; } @@ -101,6 +133,8 @@ class Metrics extends WebexPlugin { * @returns true once we have the deviceId we need to submit operational events */ isReadyToSubmitOperationalEvents() { + this.lazyBuildOperationalMetrics(); + return this.operationalMetrics?.isReadyToSubmitEvents() ?? false; } @@ -108,6 +142,8 @@ class Metrics extends WebexPlugin { * @returns true once we have the deviceId we need to submit buisness events */ isReadyToSubmitBusinessEvents() { + this.lazyBuildBusinessMetrics(); + return this.businessMetrics?.isReadyToSubmitEvents() ?? false; } @@ -137,10 +173,7 @@ class Metrics extends WebexPlugin { return Promise.resolve(); } - if (!this.behavioralMetrics) { - // @ts-ignore - this.behavioralMetrics = new BehavioralMetrics({}, {parent: this.webex}); - } + this.lazyBuildBehavioralMetrics(); return this.behavioralMetrics.submitBehavioralEvent({product, agent, target, verb, payload}); } @@ -159,10 +192,7 @@ class Metrics extends WebexPlugin { return Promise.resolve(); } - if (!this.operationalMetrics) { - // @ts-ignore - this.operationalMetrics = new OperationalMetrics({}, {parent: this.webex}); - } + this.lazyBuildOperationalMetrics(); return this.operationalMetrics.submitOperationalEvent({name, payload}); } @@ -181,10 +211,7 @@ class Metrics extends WebexPlugin { return Promise.resolve(); } - if (!this.businessMetrics) { - // @ts-ignore - this.businessMetrics = new BusinessMetrics({}, {parent: this.webex}); - } + this.lazyBuildBusinessMetrics(); return this.businessMetrics.submitBusinessEvent({name, payload}); } diff --git a/packages/@webex/internal-plugin-metrics/test/unit/spec/business/business-metrics.ts b/packages/@webex/internal-plugin-metrics/test/unit/spec/business/business-metrics.ts index 805f4fc2092..f0d30b24579 100644 --- a/packages/@webex/internal-plugin-metrics/test/unit/spec/business/business-metrics.ts +++ b/packages/@webex/internal-plugin-metrics/test/unit/spec/business/business-metrics.ts @@ -95,7 +95,7 @@ describe('internal-plugin-metrics', () => { version: getOSVersion(), }, }, - metricName: 'foobar', + key: 'foobar', browserDetails: { browser: getBrowserName(), browserHeight: window.innerHeight, @@ -106,14 +106,14 @@ describe('internal-plugin-metrics', () => { locale: window.navigator.language, os: getOSNameInternal(), }, - timestamp: requestCalls[0].eventPayload.timestamp, // This is to bypass time check, which is checked below. + client_timestamp: requestCalls[0].eventPayload.client_timestamp, // This is to bypass time check, which is checked below. value: { bar: "gee" } }, type: ['business'], }); - assert.isNumber(requestCalls[0].eventPayload.timestamp) + assert.isNumber(requestCalls[0].eventPayload.client_timestamp) }) }) }); diff --git a/packages/@webex/internal-plugin-metrics/test/unit/spec/new-metrics.ts b/packages/@webex/internal-plugin-metrics/test/unit/spec/new-metrics.ts index d300c91a39b..5ec40c27c85 100644 --- a/packages/@webex/internal-plugin-metrics/test/unit/spec/new-metrics.ts +++ b/packages/@webex/internal-plugin-metrics/test/unit/spec/new-metrics.ts @@ -73,6 +73,20 @@ describe('internal-plugin-metrics', () => { sinon.restore(); }) + it('lazy metrics backend initialization when checking if backend ready', () => { + assert.isUndefined(webex.internal.newMetrics.behavioralMetrics); + webex.internal.newMetrics.isReadyToSubmitBehavioralEvents(); + assert.isDefined(webex.internal.newMetrics.behavioralMetrics); + + assert.isUndefined(webex.internal.newMetrics.operationalMetrics); + webex.internal.newMetrics.isReadyToSubmitOperationalEvents(); + assert.isDefined(webex.internal.newMetrics.operationalMetrics); + + assert.isUndefined(webex.internal.newMetrics.businessMetrics) + webex.internal.newMetrics.isReadyToSubmitBusinessEvents(); + assert.isDefined(webex.internal.newMetrics.businessMetrics); + }) + it('submits Client Event successfully', () => { webex.internal.newMetrics.submitClientEvent({ name: 'client.alert.displayed', From 32e11370e890e6a67efd7cc644ff0d406213fd84 Mon Sep 17 00:00:00 2001 From: chrisadubois Date: Fri, 27 Sep 2024 09:46:39 -0700 Subject: [PATCH 33/48] feat(metrics): add session correlation id identifier (#3865) --- .../internal-plugin-metrics/package.json | 2 +- .../call-diagnostic-metrics.ts | 19 +++++++++++++-- .../src/metrics.types.ts | 1 + .../call-diagnostic-metrics.ts | 14 ++++++++++- yarn.lock | 24 +++++++++---------- 5 files changed, 44 insertions(+), 16 deletions(-) diff --git a/packages/@webex/internal-plugin-metrics/package.json b/packages/@webex/internal-plugin-metrics/package.json index 5b9ac214b8f..002feb20183 100644 --- a/packages/@webex/internal-plugin-metrics/package.json +++ b/packages/@webex/internal-plugin-metrics/package.json @@ -37,7 +37,7 @@ "dependencies": { "@webex/common": "workspace:*", "@webex/common-timers": "workspace:*", - "@webex/event-dictionary-ts": "^1.0.1406", + "@webex/event-dictionary-ts": "^1.0.1546", "@webex/internal-plugin-metrics": "workspace:*", "@webex/test-helper-chai": "workspace:*", "@webex/test-helper-mock-webex": "workspace:*", diff --git a/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.ts b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.ts index d1f0e1721eb..7ae50cb572d 100644 --- a/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.ts +++ b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.ts @@ -75,6 +75,7 @@ type GetIdentifiersOptions = { meeting?: any; mediaConnections?: any[]; correlationId?: string; + sessionCorrelationId?: string; preLoginId?: string; globalMeetingId?: string; webexConferenceIdStr?: string; @@ -285,6 +286,7 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin { webexConferenceIdStr, globalMeetingId, preLoginId, + sessionCorrelationId, } = options; const identifiers: Event['event']['identifiers'] = { correlationId: 'unknown', @@ -294,6 +296,10 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin { identifiers.correlationId = meeting.correlationId; } + if (sessionCorrelationId) { + identifiers.sessionCorrelationId = sessionCorrelationId; + } + if (correlationId) { identifiers.correlationId = correlationId; } @@ -646,7 +652,13 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin { options?: SubmitClientEventOptions; errors?: ClientEventPayloadError; }) { - const {meetingId, mediaConnections, globalMeetingId, webexConferenceIdStr} = options; + const { + meetingId, + mediaConnections, + globalMeetingId, + webexConferenceIdStr, + sessionCorrelationId, + } = options; // @ts-ignore const meeting = this.webex.meetings.meetingCollection.get(meetingId); @@ -673,6 +685,7 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin { mediaConnections: meeting?.mediaConnections || mediaConnections, webexConferenceIdStr, globalMeetingId, + sessionCorrelationId, }); // create client event object @@ -714,11 +727,13 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin { options?: SubmitClientEventOptions; errors?: ClientEventPayloadError; }) { - const {correlationId, globalMeetingId, webexConferenceIdStr, preLoginId} = options; + const {correlationId, globalMeetingId, webexConferenceIdStr, preLoginId, sessionCorrelationId} = + options; // grab identifiers const identifiers = this.getIdentifiers({ correlationId, + sessionCorrelationId, preLoginId, globalMeetingId, webexConferenceIdStr, diff --git a/packages/@webex/internal-plugin-metrics/src/metrics.types.ts b/packages/@webex/internal-plugin-metrics/src/metrics.types.ts index 895f4703ac2..d8a44836dda 100644 --- a/packages/@webex/internal-plugin-metrics/src/metrics.types.ts +++ b/packages/@webex/internal-plugin-metrics/src/metrics.types.ts @@ -61,6 +61,7 @@ export type SubmitClientEventOptions = { mediaConnections?: any[]; rawError?: any; correlationId?: string; + sessionCorrelationId?: string; preLoginId?: string; environment?: EnvironmentType; newEnvironmentType?: NewEnvironmentType; diff --git a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts index dd2bcebb648..521a1222e29 100644 --- a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +++ b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts @@ -372,6 +372,7 @@ describe('internal-plugin-metrics', () => { {mediaAgentAlias: 'mediaAgentAlias', mediaAgentGroupId: 'mediaAgentGroupId'}, ], webexConferenceIdStr: 'webexConferenceIdStr', + sessionCorrelationId: 'sessionCorrelationId', globalMeetingId: 'globalMeetingId', meeting: { ...fakeMeeting, @@ -386,6 +387,7 @@ describe('internal-plugin-metrics', () => { assert.deepEqual(res, { correlationId: 'correlationId', webexConferenceIdStr: 'webexConferenceIdStr1', + sessionCorrelationId: 'sessionCorrelationId', globalMeetingId: 'globalMeetingId1', deviceId: 'deviceUrl', locusId: 'url', @@ -608,6 +610,7 @@ describe('internal-plugin-metrics', () => { meeting: fakeMeeting, mediaConnections: [{mediaAgentAlias: 'alias', mediaAgentGroupId: '1'}], webexConferenceIdStr: undefined, + sessionCorrelationId: undefined, globalMeetingId: undefined, }); assert.notCalled(generateClientEventErrorPayloadSpy); @@ -762,7 +765,7 @@ describe('internal-plugin-metrics', () => { ]); }); - it('should submit client event successfully with correlationId, webexConferenceIdStr and globalMeetingId', () => { + it('should submit client event successfully with correlationId, webexConferenceIdStr, sessionCorrelationId, and globalMeetingId', () => { const prepareDiagnosticEventSpy = sinon.spy(cd, 'prepareDiagnosticEvent'); const submitToCallDiagnosticsSpy = sinon.spy(cd, 'submitToCallDiagnostics'); const generateClientEventErrorPayloadSpy = sinon.spy(cd, 'generateClientEventErrorPayload'); @@ -773,6 +776,7 @@ describe('internal-plugin-metrics', () => { correlationId: 'correlationId', webexConferenceIdStr: 'webexConferenceIdStr1', globalMeetingId: 'globalMeetingId1', + sessionCorrelationId: 'sessionCorrelationId1' }; cd.submitClientEvent({ @@ -784,6 +788,7 @@ describe('internal-plugin-metrics', () => { correlationId: 'correlationId', webexConferenceIdStr: 'webexConferenceIdStr1', globalMeetingId: 'globalMeetingId1', + sessionCorrelationId: 'sessionCorrelationId1', preLoginId: undefined, }); @@ -798,6 +803,7 @@ describe('internal-plugin-metrics', () => { identifiers: { correlationId: 'correlationId', webexConferenceIdStr: 'webexConferenceIdStr1', + sessionCorrelationId: 'sessionCorrelationId1', globalMeetingId: 'globalMeetingId1', deviceId: 'deviceUrl', locusUrl: 'locus-url', @@ -818,6 +824,7 @@ describe('internal-plugin-metrics', () => { identifiers: { correlationId: 'correlationId', webexConferenceIdStr: 'webexConferenceIdStr1', + sessionCorrelationId: 'sessionCorrelationId1', globalMeetingId: 'globalMeetingId1', deviceId: 'deviceUrl', locusUrl: 'locus-url', @@ -863,6 +870,7 @@ describe('internal-plugin-metrics', () => { webexConferenceIdStr: 'webexConferenceIdStr1', globalMeetingId: 'globalMeetingId1', preLoginId: 'myPreLoginId', + sessionCorrelationId: 'sessionCorrelationId1' }; cd.submitClientEvent({ @@ -875,6 +883,7 @@ describe('internal-plugin-metrics', () => { webexConferenceIdStr: 'webexConferenceIdStr1', globalMeetingId: 'globalMeetingId1', preLoginId: 'myPreLoginId', + sessionCorrelationId: 'sessionCorrelationId1' }); assert.notCalled(generateClientEventErrorPayloadSpy); @@ -887,6 +896,7 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId', + sessionCorrelationId: 'sessionCorrelationId1', webexConferenceIdStr: 'webexConferenceIdStr1', globalMeetingId: 'globalMeetingId1', deviceId: 'deviceUrl', @@ -913,6 +923,7 @@ describe('internal-plugin-metrics', () => { canProceed: true, identifiers: { correlationId: 'correlationId', + sessionCorrelationId: 'sessionCorrelationId1', userId: 'myPreLoginId', deviceId: 'deviceUrl', orgId: 'orgId', @@ -1417,6 +1428,7 @@ describe('internal-plugin-metrics', () => { meetingId: fakeMeeting.id, webexConferenceIdStr: 'webexConferenceIdStr1', globalMeetingId: 'globalMeetingId1', + sessionCorrelationId: 'sessionCorrelationId1' }; cd.submitMQE({ diff --git a/yarn.lock b/yarn.lock index cac23f71339..fab27faf1d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7618,17 +7618,17 @@ __metadata: languageName: unknown linkType: soft -"@webex/event-dictionary-ts@npm:^1.0.1406": - version: 1.0.1406 - resolution: "@webex/event-dictionary-ts@npm:1.0.1406" - dependencies: - amf-client-js: "npm:^5.2.6" - json-schema-to-typescript: "npm:^12.0.0" - minimist: "npm:^1.2.8" - ramldt2jsonschema: "npm:^1.2.3" - shelljs: "npm:^0.8.5" - webapi-parser: "npm:^0.5.0" - checksum: 2188a6368758001a0d839e934f4131100a40f16911a931abf0be61e7a98b001c4fe2c3108b7bd657f6d1f768c3c4aedeea9480296939ca4a7837db264df2a589 +"@webex/event-dictionary-ts@npm:^1.0.1546": + version: 1.0.1546 + resolution: "@webex/event-dictionary-ts@npm:1.0.1546" + dependencies: + amf-client-js: ^5.2.6 + json-schema-to-typescript: ^12.0.0 + minimist: ^1.2.8 + ramldt2jsonschema: ^1.2.3 + shelljs: ^0.8.5 + webapi-parser: ^0.5.0 + checksum: d938300584c5dcdeb5924a072c20aac85e9826c9631961e604c0e1dd577172f5d11a34c0b52dbb49f86386a80ecb842446a368172e5e64c273d06903f5843fa4 languageName: node linkType: hard @@ -8126,7 +8126,7 @@ __metadata: "@webex/common": "workspace:*" "@webex/common-timers": "workspace:*" "@webex/eslint-config-legacy": "workspace:*" - "@webex/event-dictionary-ts": ^1.0.1406 + "@webex/event-dictionary-ts": ^1.0.1546 "@webex/internal-plugin-metrics": "workspace:*" "@webex/jest-config-legacy": "workspace:*" "@webex/legacy-tools": "workspace:*" From 1a8e37626eed2f266e498b3a894e342693d28177 Mon Sep 17 00:00:00 2001 From: Shreyas Sharma <72344404+Shreyas281299@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:33:26 +0530 Subject: [PATCH 34/48] SPARK-351232 - Add timers to handle missed events during a call (#3839) Co-authored-by: Shreyas Sharma Co-authored-by: Sreekanth Narayanan <131740035+sreenara@users.noreply.github.com> --- .../src/CallingClient/calling/call.test.ts | 102 ++++++++++++++++++ .../calling/src/CallingClient/calling/call.ts | 60 +++++++++++ 2 files changed, 162 insertions(+) diff --git a/packages/calling/src/CallingClient/calling/call.test.ts b/packages/calling/src/CallingClient/calling/call.test.ts index b15c416981f..c95fe05c9f4 100644 --- a/packages/calling/src/CallingClient/calling/call.test.ts +++ b/packages/calling/src/CallingClient/calling/call.test.ts @@ -1955,6 +1955,108 @@ describe('State Machine handler tests', () => { method: 'handleCallHold', }); }); + + describe('Call event timers tests', () => { + let callManager; + beforeEach(() => { + jest.useFakeTimers(); + callManager = getCallManager(webex, defaultServiceIndicator); + }); + + afterEach(() => { + jest.clearAllTimers(); + }); + + it('times out if the next event is not received - 60 seconds timeout', async () => { + const statusPayload = ({ + statusCode: 200, + body: mockStatusBody, + }); + const dummyEvent = { + type: 'E_SEND_CALL_SETUP', + data: undefined as any, + }; + const logSpy = jest.spyOn(log, 'warn'); + const emitSpy = jest.spyOn(call, 'emit'); + const deleteSpy = jest.spyOn(call as any, 'delete'); + callManager.callCollection = {}; + + webex.request.mockReturnValue(statusPayload); + + // handleOutgoingCallSetup is asynchronous + await call.sendCallStateMachineEvt(dummyEvent as CallEvent); + expect(call['callStateMachine'].state.value).toBe('S_SEND_CALL_SETUP'); + + dummyEvent.type = 'E_RECV_CALL_PROGRESS'; + call.sendCallStateMachineEvt(dummyEvent as CallEvent); + expect(call['callStateMachine'].state.value).toBe('S_RECV_CALL_PROGRESS'); + + // Media setup for the call + dummyEvent.type = 'E_SEND_ROAP_OFFER'; + call.sendMediaStateMachineEvt(dummyEvent as RoapEvent); + + dummyEvent.data = { + seq: 1, + messageType: 'OFFER', + sdp: 'sdp', + }; + call.sendMediaStateMachineEvt(dummyEvent as RoapEvent); + + dummyEvent.type = 'E_RECV_ROAP_ANSWER'; + call.sendMediaStateMachineEvt(dummyEvent as RoapEvent); + + const dummyOkEvent = { + type: 'E_ROAP_OK', + data: { + received: false, + message: { + seq: 1, + messageType: 'OK', + }, + }, + }; + call.sendMediaStateMachineEvt(dummyOkEvent as RoapEvent); + dummyEvent.type = 'E_RECV_ROAP_OFFER_REQUEST'; + call.sendMediaStateMachineEvt(dummyEvent as RoapEvent); + dummyEvent.type = 'E_SEND_ROAP_OFFER'; + call.sendMediaStateMachineEvt(dummyEvent as RoapEvent); + dummyEvent.type = 'E_RECV_ROAP_ANSWER'; + logSpy.mockClear(); + jest.advanceTimersByTime(60000); + expect(logSpy.mock.calls[0][0]).toBe('Call timed out'); + expect(emitSpy).toHaveBeenCalledWith(CALL_EVENT_KEYS.DISCONNECT, call.getCorrelationId()); + expect(deleteSpy).toHaveBeenCalledTimes(1); + expect(callManager.callCollection).toStrictEqual({}); + }); + + it('times out if the next event is not received - 10 seconds timeout', async () => { + const statusPayload = ({ + statusCode: 200, + body: mockStatusBody, + }); + const dummyEvent = { + type: 'E_SEND_CALL_SETUP', + data: undefined as any, + }; + callManager.callCollection = {}; + const call = callManager.createCall(dest, CallDirection.OUTBOUND, deviceId, mockLineId); + const emitSpy = jest.spyOn(call, 'emit'); + const deleteSpy = jest.spyOn(call as any, 'delete'); + const logSpy = jest.spyOn(log, 'warn'); + webex.request.mockReturnValue(statusPayload); + expect(Object.keys(callManager.callCollection)[0]).toBe(call.getCorrelationId()); + + // handleOutgoingCallSetup is asynchronous + await call.sendCallStateMachineEvt(dummyEvent as CallEvent); + expect(call['callStateMachine'].state.value).toBe('S_SEND_CALL_SETUP'); + logSpy.mockClear(); + jest.advanceTimersByTime(10000); + expect(logSpy.mock.calls[0][0]).toBe('Call timed out'); + expect(emitSpy).toHaveBeenCalledWith(CALL_EVENT_KEYS.DISCONNECT, call.getCorrelationId()); + expect(deleteSpy).toHaveBeenCalledTimes(1); + expect(callManager.callCollection).toStrictEqual({}); + }); + }); }); describe('Supplementary Services tests', () => { diff --git a/packages/calling/src/CallingClient/calling/call.ts b/packages/calling/src/CallingClient/calling/call.ts index fd7e64718e1..57c9f75eaee 100644 --- a/packages/calling/src/CallingClient/calling/call.ts +++ b/packages/calling/src/CallingClient/calling/call.ts @@ -282,6 +282,12 @@ export class Call extends Eventing implements ICall { /* CALL SETUP */ S_RECV_CALL_SETUP: { + after: { + 10000: { + target: 'S_CALL_CLEARED', + actions: ['triggerTimeout'], + }, + }, on: { E_SEND_CALL_ALERTING: { target: 'S_SEND_CALL_PROGRESS', @@ -302,6 +308,12 @@ export class Call extends Eventing implements ICall { }, }, S_SEND_CALL_SETUP: { + after: { + 10000: { + target: 'S_CALL_CLEARED', + actions: ['triggerTimeout'], + }, + }, on: { E_RECV_CALL_PROGRESS: { target: 'S_RECV_CALL_PROGRESS', @@ -328,6 +340,12 @@ export class Call extends Eventing implements ICall { /* CALL_PROGRESS */ S_RECV_CALL_PROGRESS: { + after: { + 60000: { + target: 'S_CALL_CLEARED', + actions: ['triggerTimeout'], + }, + }, on: { E_RECV_CALL_CONNECT: { target: 'S_RECV_CALL_CONNECT', @@ -353,6 +371,12 @@ export class Call extends Eventing implements ICall { }, }, S_SEND_CALL_PROGRESS: { + after: { + 60000: { + target: 'S_CALL_CLEARED', + actions: ['triggerTimeout'], + }, + }, on: { E_SEND_CALL_CONNECT: { target: 'S_SEND_CALL_CONNECT', @@ -375,6 +399,12 @@ export class Call extends Eventing implements ICall { /* CALL_CONNECT */ S_RECV_CALL_CONNECT: { + after: { + 10000: { + target: 'S_CALL_CLEARED', + actions: ['triggerTimeout'], + }, + }, on: { E_CALL_ESTABLISHED: { target: 'S_CALL_ESTABLISHED', @@ -395,6 +425,12 @@ export class Call extends Eventing implements ICall { }, }, S_SEND_CALL_CONNECT: { + after: { + 10000: { + target: 'S_CALL_CLEARED', + actions: ['triggerTimeout'], + }, + }, on: { E_CALL_ESTABLISHED: { target: 'S_CALL_ESTABLISHED', @@ -606,6 +642,10 @@ export class Call extends Eventing implements ICall { * @param event */ unknownState: (context, event: CallEvent) => this.handleUnknownState(event), + /** + * + */ + triggerTimeout: () => this.handleTimeout(), }, } ); @@ -2794,6 +2834,26 @@ export class Call extends Eventing implements ICall { getCallRtpStats(): Promise { return this.getCallStats(); } + + /** + * Handle timeout for the missed events + * @param expectedStates - An array of next expected states + * @param errorMessage - Error message to be emitted if the call is not in the expected state in expected time + */ + private async handleTimeout() { + log.warn(`Call timed out`, { + file: CALL_FILE, + method: 'handleTimeout', + }); + this.deleteCb(this.getCorrelationId()); + this.emit(CALL_EVENT_KEYS.DISCONNECT, this.getCorrelationId()); + const response = await this.delete(); + + log.log(`handleTimeout: Response code: ${response.statusCode}`, { + file: CALL_FILE, + method: this.handleTimeout.name, + }); + } } /** From bf7c1babda0e38228139df4b4a95a7ddc311310e Mon Sep 17 00:00:00 2001 From: akulakum <74420487+akulakum@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:20:02 +0530 Subject: [PATCH 35/48] fix(voicemail): process-check-for-ms-teams-integration (#3872) --- packages/calling/src/Metrics/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/calling/src/Metrics/index.ts b/packages/calling/src/Metrics/index.ts index 85816286f5d..6e2ef686c08 100644 --- a/packages/calling/src/Metrics/index.ts +++ b/packages/calling/src/Metrics/index.ts @@ -286,7 +286,7 @@ class MetricManager implements IMetricManager { fields: { device_url: this.deviceInfo?.device?.clientDeviceUri, calling_sdk_version: - process && process.env.CALLING_SDK_VERSION + typeof process !== 'undefined' && process.env.CALLING_SDK_VERSION ? process.env.CALLING_SDK_VERSION : VERSION, }, @@ -306,7 +306,10 @@ class MetricManager implements IMetricManager { }, fields: { device_url: this.deviceInfo?.device?.clientDeviceUri, - calling_sdk_version: process.env.CALLING_SDK_VERSION || VERSION, + calling_sdk_version: + typeof process !== 'undefined' && process.env.CALLING_SDK_VERSION + ? process.env.CALLING_SDK_VERSION + : VERSION, }, type, }; From 2a9f66ced423455342499484bcf93bda3f5c0ff2 Mon Sep 17 00:00:00 2001 From: Bharath Balan <62698609+bhabalan@users.noreply.github.com> Date: Tue, 1 Oct 2024 07:30:04 +0530 Subject: [PATCH 36/48] fix(mobius): mobius cluster should be inferred from serviceUrl in hostCatalog (#3858) Co-authored-by: Shreyas Sharma <72344404+Shreyas281299@users.noreply.github.com> --- .../src/CallingClient/CallingClient.test.ts | 35 +++++++++++++++++++ .../src/CallingClient/CallingClient.ts | 14 ++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/calling/src/CallingClient/CallingClient.test.ts b/packages/calling/src/CallingClient/CallingClient.test.ts index 6432f0f0642..d39aad1ee23 100644 --- a/packages/calling/src/CallingClient/CallingClient.test.ts +++ b/packages/calling/src/CallingClient/CallingClient.test.ts @@ -41,6 +41,7 @@ import { mockCatalogUSInt, mockCatalogUS, mockCatalogEUInt, + mockUSServiceHosts, } from './callingClientFixtures'; import Line from './line'; import {filterMobiusUris} from '../common/Utils'; @@ -67,6 +68,40 @@ describe('CallingClient Tests', () => { }); } + describe('CallingClient pick Mobius cluster using Service Host Tests', () => { + afterAll(() => { + callManager.removeAllListeners(); + webex.internal.services['_serviceUrls']['mobius'] = + 'https://mobius.aintgen-a-1.int.infra.webex.com/api/v1'; + webex.internal.services['_hostCatalog'] = mockCatalogUS; + }); + + it('should set mobiusServiceHost correctly when URL is valid', async () => { + webex.internal.services._hostCatalog = mockCatalogEU; + webex.internal.services['_serviceUrls']['mobius'] = + 'https://mobius-eu-central-1.prod.infra.webex.com/api/v1'; + const urlSpy = jest.spyOn(window, 'URL').mockImplementation((url) => new window.URL(url)); + const callingClient = await createClient(webex, {logger: {level: LOGGER.INFO}}); + + expect(urlSpy).toHaveBeenCalledWith( + 'https://mobius-eu-central-1.prod.infra.webex.com/api/v1' + ); + + expect(callingClient['mobiusClusters']).toStrictEqual(mockEUServiceHosts); + + urlSpy.mockRestore(); + }); + + it('should use default mobius service host when Service URL is invalid', async () => { + webex.internal.services._hostCatalog = mockCatalogUS; + webex.internal.services._serviceUrls.mobius = 'invalid-url'; + + const callingClient = await createClient(webex, {logger: {level: LOGGER.INFO}}); + + expect(callingClient['mobiusClusters']).toStrictEqual(mockUSServiceHosts); + }); + }); + describe('ServiceData tests', () => { let callingClient: ICallingClient | undefined; diff --git a/packages/calling/src/CallingClient/CallingClient.ts b/packages/calling/src/CallingClient/CallingClient.ts index 5b7dba62f97..c14822aa0e7 100644 --- a/packages/calling/src/CallingClient/CallingClient.ts +++ b/packages/calling/src/CallingClient/CallingClient.ts @@ -106,6 +106,7 @@ export class CallingClient extends Eventing implements : {indicator: ServiceIndicator.CALLING, domain: ''}; const logLevel = this.sdkConfig?.logger?.level ? this.sdkConfig.logger.level : LOGGER.ERROR; + log.setLogger(logLevel, CALLING_CLIENT_FILE); validateServiceData(serviceData); this.callManager = getCallManager(this.webex, serviceData.indicator); @@ -115,7 +116,18 @@ export class CallingClient extends Eventing implements this.primaryMobiusUris = []; this.backupMobiusUris = []; + let mobiusServiceHost = ''; + try { + mobiusServiceHost = new URL(this.webex.internal.services._serviceUrls.mobius).host; + } catch (error) { + log.warn(`Failed to parse mobius service URL`, { + file: CALLING_CLIENT_FILE, + method: this.constructor.name, + }); + } + this.mobiusClusters = + (mobiusServiceHost && this.webex.internal.services._hostCatalog[mobiusServiceHost]) || this.webex.internal.services._hostCatalog[MOBIUS_US_PROD] || this.webex.internal.services._hostCatalog[MOBIUS_EU_PROD] || this.webex.internal.services._hostCatalog[MOBIUS_US_INT] || @@ -124,8 +136,6 @@ export class CallingClient extends Eventing implements this.registerSessionsListener(); - log.setLogger(logLevel, CALLING_CLIENT_FILE); - this.registerCallsClearedListener(); } From dd839883489c4d76b1ab6547fc78161675f824de Mon Sep 17 00:00:00 2001 From: Parimala032 <156060538+Parimala032@users.noreply.github.com> Date: Tue, 1 Oct 2024 19:51:47 +0530 Subject: [PATCH 37/48] fix(plugin-meetings): Eliminate Unnecessary Camera Prompts for Disabled Video (#3855) --- .../plugin-meetings/src/meeting/index.ts | 17 ++++++++++---- .../test/unit/spec/meeting/index.js | 22 +++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index 49f2b4e458a..daf9ed31b96 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -6521,12 +6521,21 @@ export default class Meeting extends StatelessWebexPlugin { * * @private * @static + * @param {boolean} isAudioEnabled + * @param {boolean} isVideoEnabled * @returns {Promise} */ - private static async handleDeviceLogging(): Promise { - try { - const devices = await getDevices(); + private static async handleDeviceLogging(isAudioEnabled, isVideoEnabled): Promise { + try { + let devices = []; + if (isVideoEnabled && isAudioEnabled) { + devices = await getDevices(); + } else if (isVideoEnabled) { + devices = await getDevices(Media.DeviceKind.VIDEO_INPUT); + } else if (isAudioEnabled) { + devices = await getDevices(Media.DeviceKind.AUDIO_INPUT); + } MeetingUtil.handleDeviceLogging(devices); } catch { // getDevices may fail if we don't have browser permissions, that's ok, we still can have a media connection @@ -7019,7 +7028,7 @@ export default class Meeting extends StatelessWebexPlugin { ); if (audioEnabled || videoEnabled) { - await Meeting.handleDeviceLogging(); + await Meeting.handleDeviceLogging(audioEnabled, videoEnabled); } else { LoggerProxy.logger.info(`${LOG_HEADER} device logging not required`); } diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js index fc9048a9a57..99bcb3a9cd9 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js @@ -4279,6 +4279,20 @@ describe('plugin-meetings', () => { assert.calledTwice(locusMediaRequestStub); }); + it('addMedia() works correctly when media is disabled with no streams to publish', async () => { + const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging'); + await meeting.addMedia({audioEnabled: false}); + //calling handleDeviceLogging with audioEnaled as true adn videoEnabled as false + assert.calledWith(handleDeviceLoggingSpy,false,true); + }); + + it('addMedia() works correctly when video is disabled with no streams to publish', async () => { + const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging'); + await meeting.addMedia({videoEnabled: false}); + //calling handleDeviceLogging audioEnabled as true videoEnabled as false + assert.calledWith(handleDeviceLoggingSpy,true,false); + }); + it('addMedia() works correctly when video is disabled with no streams to publish', async () => { await meeting.addMedia({videoEnabled: false}); await simulateRoapOffer(); @@ -4345,6 +4359,14 @@ describe('plugin-meetings', () => { assert.calledTwice(locusMediaRequestStub); }); + + it('addMedia() works correctly when both shareAudio and shareVideo is disabled with no streams publish', async () => { + const handleDeviceLoggingSpy = sinon.spy(Meeting, 'handleDeviceLogging'); + await meeting.addMedia({shareAudioEnabled: false, shareVideoEnabled: false}); + //calling handleDeviceLogging with audioEnabled true and videoEnabled as true + assert.calledWith(handleDeviceLoggingSpy,true,true); + }); + describe('publishStreams()/unpublishStreams() calls', () => { [ {mediaEnabled: true, expected: {direction: 'sendrecv', localMuteSentValue: false}}, From 9c6820e714de689ff33065672440959cb4790862 Mon Sep 17 00:00:00 2001 From: Anna Tsukanova <38460776+antsukanova@users.noreply.github.com> Date: Wed, 2 Oct 2024 13:08:14 +0200 Subject: [PATCH 38/48] test: fix window undefined issue for tests (#3874) Co-authored-by: Anna Tsukanova --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .../spec/converged-space-meetings.js | 2 +- .../test/unit/spec/breakouts/index.ts | 1 + .../test/unit/spec/interceptors/locusRetry.ts | 21 ++++++++++--------- .../unit/spec/media/MediaConnectionAwaiter.ts | 1 + .../test/unit/spec/media/index.ts | 1 + .../test/unit/spec/media/properties.ts | 2 +- .../unit/spec/meeting-info/meetinginfov2.js | 21 +++++++++---------- .../test/unit/spec/meeting-info/request.js | 2 +- .../spec/meeting/connectionStateHandler.ts | 1 + .../unit/spec/meeting/locusMediaRequest.ts | 5 +++-- .../test/unit/spec/meeting/request.js | 1 + .../test/unit/spec/meeting/utils.js | 1 + .../test/unit/spec/members/request.js | 3 ++- .../spec/multistream/mediaRequestManager.ts | 1 + .../test/unit/spec/multistream/receiveSlot.ts | 1 + .../spec/multistream/receiveSlotManager.ts | 1 + .../test/unit/spec/multistream/remoteMedia.ts | 1 + .../unit/spec/multistream/remoteMediaGroup.ts | 1 + .../spec/multistream/remoteMediaManager.ts | 1 + .../unit/spec/multistream/sendSlotManager.ts | 1 + .../personal-meeting-room.js | 1 - .../test/unit/spec/reachability/request.js | 1 + .../test/unit/spec/roap/request.ts | 1 + 24 files changed, 45 insertions(+), 29 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 09ad26be824..3818755bf01 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -32,7 +32,7 @@ This is for compliance purposes with FedRAMP program. - [ ] Tooling change - [ ] Internal code refactor -## The following scenarios where tested +## The following scenarios were tested < ENUMERATE TESTS PERFORMED, WHETHER MANUAL OR AUTOMATED > diff --git a/packages/@webex/plugin-meetings/test/integration/spec/converged-space-meetings.js b/packages/@webex/plugin-meetings/test/integration/spec/converged-space-meetings.js index 76981c3c60e..7206ec01b83 100644 --- a/packages/@webex/plugin-meetings/test/integration/spec/converged-space-meetings.js +++ b/packages/@webex/plugin-meetings/test/integration/spec/converged-space-meetings.js @@ -1,5 +1,5 @@ -import { config } from 'dotenv'; import 'jsdom-global/register'; +import {config} from 'dotenv'; import {assert} from '@webex/test-helper-chai'; import {skipInNode} from '@webex/test-helper-mocha'; import BrowserDetection from '@webex/plugin-meetings/dist/common/browser-detection'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/breakouts/index.ts b/packages/@webex/plugin-meetings/test/unit/spec/breakouts/index.ts index bdc6e8da972..ac4c2e18321 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/breakouts/index.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/breakouts/index.ts @@ -1,3 +1,4 @@ +import 'jsdom-global/register'; import {assert, expect} from '@webex/test-helper-chai'; import Breakouts from '@webex/plugin-meetings/src/breakouts'; import LoggerProxy from '@webex/plugin-meetings/src/common/logs/logger-proxy'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/interceptors/locusRetry.ts b/packages/@webex/plugin-meetings/test/unit/spec/interceptors/locusRetry.ts index ac94b6e5c39..c4853cfc187 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/interceptors/locusRetry.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/interceptors/locusRetry.ts @@ -3,6 +3,7 @@ */ /* eslint-disable camelcase */ +import 'jsdom-global/register'; import {assert} from '@webex/test-helper-chai'; import { expect } from "@webex/test-helper-chai"; import MockWebex from '@webex/test-helper-mock-webex'; @@ -13,7 +14,7 @@ import sinon from 'sinon'; describe('plugin-meetings', () => { describe('Interceptors', () => { - describe('LocusRetryStatusInterceptor', () => { + describe('LocusRetryStatusInterceptor', () => { let interceptor, webex; beforeEach(() => { webex = new MockWebex({ @@ -24,7 +25,7 @@ describe('plugin-meetings', () => { interceptor = Reflect.apply(LocusRetryStatusInterceptor.create, { sessionId: 'mock-webex_uuid', }, []); - }); + }); describe('#onResponseError', () => { const options = { method: 'POST', @@ -41,7 +42,7 @@ describe('plugin-meetings', () => { headers: { trackingid: 'test', 'retry-after': 1000, - }, + }, uri: `https://locus-test.webex.com/locus/api/v1/loci/call`, }, body: { @@ -54,7 +55,7 @@ describe('plugin-meetings', () => { headers: { trackingid: 'test', 'retry-after': 1000, - }, + }, uri: `https://locus-test.webex.com/locus/api/v1/loci/call`, }, body: { @@ -73,7 +74,7 @@ describe('plugin-meetings', () => { return interceptor.onResponseError(options, reason2).then(() => { expect(handleRetryStub.calledWith(options, 1000)).to.be.true; - + }); }); }); @@ -92,7 +93,7 @@ describe('plugin-meetings', () => { it('returns the correct resolved value when the request is successful', () => { const mockResponse = 'mock response' interceptor.webex.request = sinon.stub().returns(Promise.resolve(mockResponse)); - + return interceptor.handleRetryRequestLocusServiceError(options, retryAfterTime) .then((response) => { expect(response).to.equal(mockResponse); @@ -101,9 +102,9 @@ describe('plugin-meetings', () => { it('rejects the promise when the request is unsuccessful', () => { const rejectionReason = 'Service Unavaialble after retry'; - + interceptor.webex.request = sinon.stub().returns(Promise.reject(rejectionReason)); - + return interceptor.handleRetryRequestLocusServiceError(options, retryAfterTime) .catch((error) => { expect(error).to.equal(rejectionReason); @@ -114,10 +115,10 @@ describe('plugin-meetings', () => { let clock; clock = sinon.useFakeTimers(); const mockResponse = 'mock response' - + interceptor.webex.request = sinon.stub().returns(Promise.resolve(mockResponse)); const promise = interceptor.handleRetryRequestLocusServiceError(options, retryAfterTime); - + clock.tick(retryAfterTime); return promise.then(() => { diff --git a/packages/@webex/plugin-meetings/test/unit/spec/media/MediaConnectionAwaiter.ts b/packages/@webex/plugin-meetings/test/unit/spec/media/MediaConnectionAwaiter.ts index e4e554e9961..7813f7d30f1 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/media/MediaConnectionAwaiter.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/media/MediaConnectionAwaiter.ts @@ -1,3 +1,4 @@ +import 'jsdom-global/register'; import {assert} from '@webex/test-helper-chai'; import sinon from 'sinon'; import {ConnectionState, MediaConnectionEventNames} from '@webex/internal-media-core'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/media/index.ts b/packages/@webex/plugin-meetings/test/unit/spec/media/index.ts index 096577011de..8378ebb9131 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/media/index.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/media/index.ts @@ -1,3 +1,4 @@ +import 'jsdom-global/register'; import * as InternalMediaCoreModule from '@webex/internal-media-core'; import Media from '@webex/plugin-meetings/src/media/index'; import {assert} from '@webex/test-helper-chai'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/media/properties.ts b/packages/@webex/plugin-meetings/test/unit/spec/media/properties.ts index f96118d90db..1507ec8708e 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/media/properties.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/media/properties.ts @@ -1,8 +1,8 @@ +import 'jsdom-global/register'; import {assert} from '@webex/test-helper-chai'; import sinon from 'sinon'; import {ConnectionState} from '@webex/internal-media-core'; import MediaProperties from '@webex/plugin-meetings/src/media/properties'; -import testUtils from '../../../utils/testUtils'; import {Defer} from '@webex/common'; import MediaConnectionAwaiter from '../../../../src/media/MediaConnectionAwaiter'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting-info/meetinginfov2.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting-info/meetinginfov2.js index df94e5be38d..6beb9ae9ffe 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting-info/meetinginfov2.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting-info/meetinginfov2.js @@ -1,7 +1,7 @@ /*! * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file. */ - +import 'jsdom-global/register'; import {assert} from '@webex/test-helper-chai'; import sinon from 'sinon'; import MockWebex from '@webex/test-helper-mock-webex'; @@ -23,7 +23,6 @@ import MeetingInfoUtil from '@webex/plugin-meetings/src/meeting-info/utilv2'; import Metrics from '@webex/plugin-meetings/src/metrics'; import BEHAVIORAL_METRICS from '@webex/plugin-meetings/src/metrics/constants'; import {forEach} from 'lodash'; -import { request } from 'express'; describe('plugin-meetings', () => { const conversation = { @@ -433,7 +432,7 @@ describe('plugin-meetings', () => { assert.deepEqual(submitInternalEventCalls[1].args[0], { name: 'internal.client.meetinginfo.response', }); - + assert.deepEqual(submitClientEventCalls[1].args[0], { name: 'client.meetinginfo.response', payload: { @@ -484,9 +483,9 @@ describe('plugin-meetings', () => { requestResponse.body.confIdStr = confIdStr; } const extraParams = {mtid: 'm9fe0afd8c435e892afcce9ea25b97046', joinTXId: 'TSmrX61wNF'} - + webex.request.resolves(requestResponse); - + const result = await meetingInfo.fetchMeetingInfo( '1234323', DESTINATION_TYPE.MEETING_ID, @@ -497,7 +496,7 @@ describe('plugin-meetings', () => { extraParams, {meetingId, sendCAevents} ); - + assert.calledWith(webex.request, { method: 'POST', service: WBXAPPAPI_SERVICE, @@ -515,7 +514,7 @@ describe('plugin-meetings', () => { Metrics.sendBehavioralMetric, BEHAVIORAL_METRICS.FETCH_MEETING_INFO_V1_SUCCESS ); - + const submitInternalEventCalls = webex.internal.newMetrics.submitInternalEvent.getCalls(); const submitClientEventCalls = webex.internal.newMetrics.submitClientEvent.getCalls(); @@ -529,7 +528,7 @@ describe('plugin-meetings', () => { meetingId, } }); - + assert.deepEqual(submitInternalEventCalls[1].args[0], { name: 'internal.client.meetinginfo.response', }); @@ -591,7 +590,7 @@ describe('plugin-meetings', () => { const submitInternalEventCalls = webex.internal.newMetrics.submitInternalEvent.getCalls(); const submitClientEventCalls = webex.internal.newMetrics.submitClientEvent.getCalls(); - + assert.deepEqual(submitInternalEventCalls[0].args[0], { name: 'internal.client.meetinginfo.request', }); @@ -601,7 +600,7 @@ describe('plugin-meetings', () => { meetingId: 'meetingId', } }); - + assert.deepEqual(submitInternalEventCalls[1].args[0], { name: 'internal.client.meetinginfo.response', }); @@ -629,7 +628,7 @@ describe('plugin-meetings', () => { it(`should not send CA metric if meetingId is not provided disregarding if sendCAevents is ${sendCAevents}`, async () => { const message = 'a message'; const meetingInfoData = 'meeting info'; - + webex.request = sinon.stub().rejects({ statusCode: 403, body: {message, code: 403102, data: {meetingInfo: meetingInfoData}}, diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting-info/request.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting-info/request.js index 3d2d2bd7bc3..f3dd2e0c384 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting-info/request.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting-info/request.js @@ -1,7 +1,7 @@ /*! * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file. */ - +import 'jsdom-global/register'; import {assert} from '@webex/test-helper-chai'; import sinon from 'sinon'; import MockWebex from '@webex/test-helper-mock-webex'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/connectionStateHandler.ts b/packages/@webex/plugin-meetings/test/unit/spec/meeting/connectionStateHandler.ts index a4f0460be35..b0278e370ad 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/connectionStateHandler.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/connectionStateHandler.ts @@ -1,3 +1,4 @@ +import 'jsdom-global/register' import sinon from 'sinon'; import {assert} from '@webex/test-helper-chai'; import { diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/locusMediaRequest.ts b/packages/@webex/plugin-meetings/test/unit/spec/meeting/locusMediaRequest.ts index b20d76c20ef..e0e12e728e5 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/locusMediaRequest.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/locusMediaRequest.ts @@ -1,6 +1,7 @@ +import 'jsdom-global/register'; import sinon from 'sinon'; import {assert} from '@webex/test-helper-chai'; -import { cloneDeep, defer } from 'lodash'; +import { cloneDeep } from 'lodash'; import MockWebex from '@webex/test-helper-mock-webex'; import Meetings from '@webex/plugin-meetings'; @@ -495,4 +496,4 @@ describe('LocusMediaRequest.send()', () => { }); }); -}) \ No newline at end of file +}) diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/request.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/request.js index 8d9e844e18c..5e53406d31c 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/request.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/request.js @@ -1,3 +1,4 @@ +import 'jsdom-global/register'; import sinon from 'sinon'; import {assert} from '@webex/test-helper-chai'; import MockWebex from '@webex/test-helper-mock-webex'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/utils.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/utils.js index 424846b1798..f418a256d06 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/utils.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/utils.js @@ -1,3 +1,4 @@ +import 'jsdom-global/register'; import sinon from 'sinon'; import {assert} from '@webex/test-helper-chai'; import Meetings from '@webex/plugin-meetings'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/members/request.js b/packages/@webex/plugin-meetings/test/unit/spec/members/request.js index 833d95629a1..e1fe2b3606c 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/members/request.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/members/request.js @@ -1,3 +1,4 @@ +import 'jsdom-global/register'; import sinon from 'sinon'; import chai from 'chai'; import uuid from 'uuid'; @@ -131,7 +132,7 @@ describe('plugin-meetings', () => { locusUrl: url1, memberIds: ['1', '2'], }; - + await membersRequest.admitMember(options) checkRequest({ diff --git a/packages/@webex/plugin-meetings/test/unit/spec/multistream/mediaRequestManager.ts b/packages/@webex/plugin-meetings/test/unit/spec/multistream/mediaRequestManager.ts index 3f0c70170f5..229a6625410 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/multistream/mediaRequestManager.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/multistream/mediaRequestManager.ts @@ -1,3 +1,4 @@ +import 'jsdom-global/register'; import {MediaRequestManager} from '@webex/plugin-meetings/src/multistream/mediaRequestManager'; import {ReceiveSlot} from '@webex/plugin-meetings/src/multistream/receiveSlot'; import sinon from 'sinon'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/multistream/receiveSlot.ts b/packages/@webex/plugin-meetings/test/unit/spec/multistream/receiveSlot.ts index 88fa907ee4f..860173568d6 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/multistream/receiveSlot.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/multistream/receiveSlot.ts @@ -1,4 +1,5 @@ /* eslint-disable require-jsdoc */ +import 'jsdom-global/register'; import EventEmitter from 'events'; import {MediaType, ReceiveSlotEvents as WcmeReceiveSlotEvents} from '@webex/internal-media-core'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/multistream/receiveSlotManager.ts b/packages/@webex/plugin-meetings/test/unit/spec/multistream/receiveSlotManager.ts index ded0488f8f4..6ee1128e86b 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/multistream/receiveSlotManager.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/multistream/receiveSlotManager.ts @@ -1,3 +1,4 @@ +import 'jsdom-global/register'; import sinon from 'sinon'; import {assert} from '@webex/test-helper-chai'; import {MediaType} from '@webex/internal-media-core'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/multistream/remoteMedia.ts b/packages/@webex/plugin-meetings/test/unit/spec/multistream/remoteMedia.ts index 2bb059e27ff..be5b51042a0 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/multistream/remoteMedia.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/multistream/remoteMedia.ts @@ -1,4 +1,5 @@ /* eslint-disable require-jsdoc */ +import 'jsdom-global/register'; import EventEmitter from 'events'; import {MediaType} from '@webex/internal-media-core'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/multistream/remoteMediaGroup.ts b/packages/@webex/plugin-meetings/test/unit/spec/multistream/remoteMediaGroup.ts index cbde1e3968c..4eb3bc71ed5 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/multistream/remoteMediaGroup.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/multistream/remoteMediaGroup.ts @@ -1,3 +1,4 @@ +import 'jsdom-global/register'; import EventEmitter from 'events'; import {MediaType} from '@webex/internal-media-core'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/multistream/remoteMediaManager.ts b/packages/@webex/plugin-meetings/test/unit/spec/multistream/remoteMediaManager.ts index 5a36879de61..b37c34df752 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/multistream/remoteMediaManager.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/multistream/remoteMediaManager.ts @@ -1,4 +1,5 @@ /* eslint-disable require-jsdoc */ +import 'jsdom-global/register'; import EventEmitter from 'events'; import {MediaType} from '@webex/internal-media-core'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/multistream/sendSlotManager.ts b/packages/@webex/plugin-meetings/test/unit/spec/multistream/sendSlotManager.ts index 6f74035285b..f4ccf80f7c9 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/multistream/sendSlotManager.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/multistream/sendSlotManager.ts @@ -1,3 +1,4 @@ +import 'jsdom-global/register'; import SendSlotManager from '@webex/plugin-meetings/src/multistream/sendSlotManager'; import { LocalStream, MediaType, MultistreamRoapMediaConnection } from "@webex/internal-media-core"; import {expect} from '@webex/test-helper-chai'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/personal-meeting-room/personal-meeting-room.js b/packages/@webex/plugin-meetings/test/unit/spec/personal-meeting-room/personal-meeting-room.js index e87df2a397c..43fa8eb17c2 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/personal-meeting-room/personal-meeting-room.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/personal-meeting-room/personal-meeting-room.js @@ -1,7 +1,6 @@ /*! * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file. */ - import 'jsdom-global/register'; import {assert} from '@webex/test-helper-chai'; import sinon from 'sinon'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/reachability/request.js b/packages/@webex/plugin-meetings/test/unit/spec/reachability/request.js index e13a48dbce9..372c398b4b4 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/reachability/request.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/reachability/request.js @@ -1,3 +1,4 @@ +import 'jsdom-global/register'; import sinon from 'sinon'; import {assert} from '@webex/test-helper-chai'; import MockWebex from '@webex/test-helper-mock-webex'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/roap/request.ts b/packages/@webex/plugin-meetings/test/unit/spec/roap/request.ts index 155f27c96be..70cb8df3bbd 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/roap/request.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/roap/request.ts @@ -1,3 +1,4 @@ +import 'jsdom-global/register'; import sinon from 'sinon'; import {assert} from '@webex/test-helper-chai'; import MockWebex from '@webex/test-helper-mock-webex'; From b4e565c241374273e4da326c82d226822d5bcdb1 Mon Sep 17 00:00:00 2001 From: chrisadubois Date: Wed, 2 Oct 2024 16:29:09 -0700 Subject: [PATCH 39/48] feat(meetings): add meeting join time marker (#3877) --- .../plugin-meetings/src/meeting/index.ts | 23 +++++++++++++++++++ .../test/unit/spec/meeting/index.js | 5 ++++ 2 files changed, 28 insertions(+) diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index daf9ed31b96..650f5cb193e 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -538,6 +538,7 @@ export default class Meeting extends StatelessWebexPlugin { id: string; isMultistream: boolean; locusUrl: string; + #isoLocalClientMeetingJoinTime?: string; mediaConnections: any[]; mediaId?: string; meetingFiniteStateMachine: any; @@ -1521,6 +1522,17 @@ export default class Meeting extends StatelessWebexPlugin { * @memberof Meeting */ this.iceCandidatesCount = 0; + + /** + * Start time of meeting as an ISO string + * based on browser time, so can only be used to compute durations client side + * undefined if meeting has not been joined, set once on meeting join, and not updated again + * @instance + * @type {string} + * @private + * @memberof Meeting + */ + this.#isoLocalClientMeetingJoinTime = undefined; } /** @@ -1569,6 +1581,15 @@ export default class Meeting extends StatelessWebexPlugin { this.callStateForMetrics.correlationId = correlationId; } + /** + * Getter - Returns isoLocalClientMeetingJoinTime + * This will be set once on meeting join, and not updated again + * @returns {string | undefined} + */ + get isoLocalClientMeetingJoinTime(): string | undefined { + return this.#isoLocalClientMeetingJoinTime; + } + /** * Set meeting info and trigger `MEETING_INFO_AVAILABLE` event * @param {any} info @@ -5235,6 +5256,8 @@ export default class Meeting extends StatelessWebexPlugin { // @ts-ignore this.webex.internal.device.meetingStarted(); + this.#isoLocalClientMeetingJoinTime = new Date().toISOString(); + LoggerProxy.logger.log('Meeting:index#join --> Success'); Metrics.sendBehavioralMetric(BEHAVIORAL_METRICS.JOIN_SUCCESS, { diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js index 99bcb3a9cd9..2b947cfbdb6 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js @@ -331,6 +331,7 @@ describe('plugin-meetings', () => { assert.isNull(meeting.partner); assert.isNull(meeting.type); assert.isNull(meeting.owner); + assert.isUndefined(meeting.isoLocalClientMeetingJoinTime); assert.isNull(meeting.hostId); assert.isNull(meeting.policy); assert.instanceOf(meeting.meetingRequest, MeetingRequest); @@ -1587,6 +1588,10 @@ describe('plugin-meetings', () => { sandbox.stub(MeetingUtil, 'joinMeeting').returns(Promise.resolve(joinMeetingResult)); }); + afterEach(() => { + assert.exists(meeting.isoLocalClientMeetingJoinTime); + }); + it('should join the meeting and return promise', async () => { const join = meeting.join({pstnAudioType: 'dial-in'}); meeting.config.enableAutomaticLLM = true; From 83a3b1726a8518356b03bb5f61da8751288a83e5 Mon Sep 17 00:00:00 2001 From: chrisadubois Date: Thu, 3 Oct 2024 01:40:48 -0700 Subject: [PATCH 40/48] fix(mercury): fix logout mercury delete (#3878) --- .../internal-plugin-mercury/src/config.js | 6 + .../internal-plugin-mercury/src/index.js | 2 +- .../internal-plugin-mercury/src/mercury.js | 18 ++- .../src/socket/socket-base.js | 23 +++- .../test/unit/spec/mercury.js | 50 ++++++++ .../test/unit/spec/socket.js | 108 +++++++++++++++++- 6 files changed, 196 insertions(+), 11 deletions(-) diff --git a/packages/@webex/internal-plugin-mercury/src/config.js b/packages/@webex/internal-plugin-mercury/src/config.js index 33d5db52868..a685b21ed44 100644 --- a/packages/@webex/internal-plugin-mercury/src/config.js +++ b/packages/@webex/internal-plugin-mercury/src/config.js @@ -30,5 +30,11 @@ export default { * @type {[type]} */ forceCloseDelay: process.env.MERCURY_FORCE_CLOSE_DELAY || 2000, + /** + * When logging out, use default reason which can trigger a reconnect, + * or set to something else, like `done (permanent)` to prevent reconnect + * @type {String} + */ + beforeLogoutOptionsCloseReason: process.env.MERCURY_LOGOUT_REASON || 'done (forced)', }, }; diff --git a/packages/@webex/internal-plugin-mercury/src/index.js b/packages/@webex/internal-plugin-mercury/src/index.js index 73aff3d62e4..693bd3b8fad 100644 --- a/packages/@webex/internal-plugin-mercury/src/index.js +++ b/packages/@webex/internal-plugin-mercury/src/index.js @@ -14,7 +14,7 @@ import config from './config'; registerInternalPlugin('mercury', Mercury, { config, onBeforeLogout() { - return this.disconnect(); + return this.logout(); }, }); diff --git a/packages/@webex/internal-plugin-mercury/src/mercury.js b/packages/@webex/internal-plugin-mercury/src/mercury.js index c74615ba33a..ed7b706130c 100644 --- a/packages/@webex/internal-plugin-mercury/src/mercury.js +++ b/packages/@webex/internal-plugin-mercury/src/mercury.js @@ -93,8 +93,17 @@ const Mercury = WebexPlugin.extend({ }); }, + logout() { + return this.disconnect( + this.config.beforeLogoutOptionsCloseReason && + !normalReconnectReasons.includes(this.config.beforeLogoutOptionsCloseReason) + ? {code: 1050, reason: this.config.beforeLogoutOptionsCloseReason} + : undefined + ); + }, + @oneFlight - disconnect() { + disconnect(options) { return new Promise((resolve) => { if (this.backoffCall) { this.logger.info(`${this.namespace}: aborting connection`); @@ -104,7 +113,7 @@ const Mercury = WebexPlugin.extend({ if (this.socket) { this.socket.removeAllListeners('message'); this.once('offline', resolve); - resolve(this.socket.close()); + resolve(this.socket.close(options || undefined)); } resolve(); @@ -446,6 +455,7 @@ const Mercury = WebexPlugin.extend({ // if (code == 1011 && reason !== ping error) metric: unexpected disconnect break; case 1000: + case 1050: // 1050 indicates logout form of closure, default to old behavior, use config reason defined by consumer to proceed with the permanent block if (normalReconnectReasons.includes(reason)) { this.logger.info(`${this.namespace}: socket disconnected; reconnecting`); this._emit('offline.transient', event); @@ -453,7 +463,9 @@ const Mercury = WebexPlugin.extend({ // metric: disconnect // if (reason === done forced) metric: force closure } else { - this.logger.info(`${this.namespace}: socket disconnected; will not reconnect`); + this.logger.info( + `${this.namespace}: socket disconnected; will not reconnect: ${event.reason}` + ); this._emit('offline.permanent', event); } break; diff --git a/packages/@webex/internal-plugin-mercury/src/socket/socket-base.js b/packages/@webex/internal-plugin-mercury/src/socket/socket-base.js index aaf953d4ac1..02bbe25d5a1 100644 --- a/packages/@webex/internal-plugin-mercury/src/socket/socket-base.js +++ b/packages/@webex/internal-plugin-mercury/src/socket/socket-base.js @@ -122,8 +122,15 @@ export default class Socket extends EventEmitter { } options = options || {}; - if (options.code && options.code !== 1000 && (options.code < 3000 || options.code > 4999)) { - reject(new Error('`options.code` must be 1000 or between 3000 and 4999 (inclusive)')); + if ( + options.code && + options.code !== 1000 && + options.code !== 1050 && + (options.code < 3000 || options.code > 4999) + ) { + reject( + new Error('`options.code` must be 1000 or 1050 or between 3000 and 4999 (inclusive)') + ); return; } @@ -137,10 +144,14 @@ export default class Socket extends EventEmitter { try { this.logger.info(`socket,${this._domain}: no close event received, forcing closure`); resolve( - this.onclose({ - code: 1000, - reason: 'Done (forced)', - }) + this.onclose( + options.code === 1050 + ? {code: 1050, reason: options.reason} + : { + code: 1000, + reason: 'Done (forced)', + } + ) ); } catch (error) { this.logger.warn(`socket,${this._domain}: force-close failed`, error); diff --git a/packages/@webex/internal-plugin-mercury/test/unit/spec/mercury.js b/packages/@webex/internal-plugin-mercury/test/unit/spec/mercury.js index adf4f527a61..6e772c301b9 100644 --- a/packages/@webex/internal-plugin-mercury/test/unit/spec/mercury.js +++ b/packages/@webex/internal-plugin-mercury/test/unit/spec/mercury.js @@ -503,6 +503,35 @@ describe('plugin-mercury', () => { }); }); + describe('#logout()', () => { + it('calls disconnect', () => { + sinon.stub(mercury, 'disconnect'); + mercury.logout(); + assert.called(mercury.disconnect); + }); + + it('uses the config.beforeLogoutOptionsCloseReason to disconnect and will send code 1050 for logout', () => { + sinon.stub(mercury, 'disconnect'); + mercury.config.beforeLogoutOptionsCloseReason = 'done (permanent)'; + mercury.logout(); + assert.calledWith(mercury.disconnect, {code: 1050, reason: 'done (permanent)'}); + }); + + it('uses the config.beforeLogoutOptionsCloseReason to disconnect and will send code 1050 for logout if the reason is different than standard', () => { + sinon.stub(mercury, 'disconnect'); + mercury.config.beforeLogoutOptionsCloseReason = 'test'; + mercury.logout(); + assert.calledWith(mercury.disconnect, {code: 1050, reason: 'test'}); + }); + + it('uses the config.beforeLogoutOptionsCloseReason to disconnect and will send undefined for logout if the reason is same as standard', () => { + sinon.stub(mercury, 'disconnect'); + mercury.config.beforeLogoutOptionsCloseReason = 'done (forced)'; + mercury.logout(); + assert.calledWith(mercury.disconnect, undefined); + }); + }); + describe('#disconnect()', () => { it('disconnects the WebSocket', () => mercury @@ -525,6 +554,27 @@ describe('plugin-mercury', () => { assert.isUndefined(mercury.mockWebSocket, 'Mercury does not have a mockWebSocket'); })); + it('disconnects the WebSocket with code 1050', () => + mercury + .connect() + .then(() => { + assert.isTrue(mercury.connected, 'Mercury is connected'); + assert.isFalse(mercury.connecting, 'Mercury is not connecting'); + const promise = mercury.disconnect(); + + mockWebSocket.emit('close', { + code: 1050, + reason: 'done (permanent)', + }); + + return promise; + }) + .then(() => { + assert.isFalse(mercury.connected, 'Mercury is not connected'); + assert.isFalse(mercury.connecting, 'Mercury is not connecting'); + assert.isUndefined(mercury.mockWebSocket, 'Mercury does not have a mockWebSocket'); + })); + it('stops emitting message events', () => { const spy = sinon.spy(); diff --git a/packages/@webex/internal-plugin-mercury/test/unit/spec/socket.js b/packages/@webex/internal-plugin-mercury/test/unit/spec/socket.js index b34f09bdc82..39b4f9f3da1 100644 --- a/packages/@webex/internal-plugin-mercury/test/unit/spec/socket.js +++ b/packages/@webex/internal-plugin-mercury/test/unit/spec/socket.js @@ -452,7 +452,7 @@ describe('plugin-mercury', () => { Promise.all([ assert.isRejected( socket.close({code: 1001}), - /`options.code` must be 1000 or between 3000 and 4999 \(inclusive\)/ + /`options.code` must be 1000 or 1050 or between 3000 and 4999 \(inclusive\)/ ), socket.close({code: 1000}), ])); @@ -465,6 +465,14 @@ describe('plugin-mercury', () => { }) .then(() => assert.calledWith(mockWebSocket.close, 3001, 'Custom Normal'))); + it('accepts the logout reason', () => + socket + .close({ + code: 1050, + reason: 'done (permanent)', + }) + .then(() => assert.calledWith(mockWebSocket.close, 1050, 'done (permanent)'))); + it('can safely be called called multiple times', () => { const p1 = socket.close(); @@ -513,6 +521,84 @@ describe('plugin-mercury', () => { }); }); + it('signals closure if no close frame is received within the specified window, but uses the initial options as 1050 if specified by options call', () => { + const socket = new Socket(); + const promise = socket.open('ws://example.com', mockoptions); + + mockWebSocket.readyState = 1; + mockWebSocket.emit('open'); + mockWebSocket.emit('message', { + data: JSON.stringify({ + id: uuid.v4(), + data: { + eventType: 'mercury.buffer_state', + }, + }), + }); + + return promise.then(() => { + const spy = sinon.spy(); + + socket.on('close', spy); + mockWebSocket.close = () => + new Promise(() => { + /* eslint no-inline-comments: [0] */ + }); + mockWebSocket.removeAllListeners('close'); + + const promise = socket.close({code: 1050, reason: 'done (permanent)'}); + + clock.tick(mockoptions.forceCloseDelay); + + return promise.then(() => { + assert.called(spy); + assert.calledWith(spy, { + code: 1050, + reason: 'done (permanent)', + }); + }); + }); + }); + + it('signals closure if no close frame is received within the specified window, and uses default options as 1000 if the code is not 1050', () => { + const socket = new Socket(); + const promise = socket.open('ws://example.com', mockoptions); + + mockWebSocket.readyState = 1; + mockWebSocket.emit('open'); + mockWebSocket.emit('message', { + data: JSON.stringify({ + id: uuid.v4(), + data: { + eventType: 'mercury.buffer_state', + }, + }), + }); + + return promise.then(() => { + const spy = sinon.spy(); + + socket.on('close', spy); + mockWebSocket.close = () => + new Promise(() => { + /* eslint no-inline-comments: [0] */ + }); + mockWebSocket.removeAllListeners('close'); + + const promise = socket.close({code: 1000, reason: 'test'}); + + clock.tick(mockoptions.forceCloseDelay); + + return promise.then(() => { + assert.called(spy); + assert.calledWith(spy, { + code: 1000, + reason: 'Done (forced)', + }); + }); + }); + }); + it('cancels any outstanding ping/pong timers', () => { mockWebSocket.send = sinon.stub(); socket._ping.resetHistory(); @@ -618,6 +704,26 @@ describe('plugin-mercury', () => { } ); }); + + describe('when it receives close code 1050', () => { + it(`emits code 1050 for code 1050`, () => { + const code = 1050; + const reason = 'done (permanent)'; + const spy = sinon.spy(); + + socket.on('close', spy); + + mockWebSocket.emit('close', { + code, + reason, + }); + assert.called(spy); + assert.calledWith(spy, { + code, + reason, + }); + }); + }); }); describe('#onmessage()', () => { From 48dcc507580bd48bfb68ebf8e751b8aa488dc34d Mon Sep 17 00:00:00 2001 From: chrisadubois Date: Mon, 7 Oct 2024 23:20:21 -0700 Subject: [PATCH 41/48] feat(ca): add saveable sessionCorrelationId (#3884) --- .../call-diagnostic-metrics.ts | 2 + .../call-diagnostic-metrics.ts | 153 ++++++++++++++++++ .../plugin-meetings/src/meeting/index.ts | 34 ++++ .../plugin-meetings/src/meetings/index.ts | 8 +- .../test/unit/spec/meeting/index.js | 45 +++++- .../test/unit/spec/meetings/index.js | 12 +- 6 files changed, 245 insertions(+), 9 deletions(-) diff --git a/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.ts b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.ts index 7ae50cb572d..2ee5f18b6d7 100644 --- a/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.ts +++ b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.ts @@ -290,10 +290,12 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin { } = options; const identifiers: Event['event']['identifiers'] = { correlationId: 'unknown', + sessionCorrelationId: 'unknown', }; if (meeting) { identifiers.correlationId = meeting.correlationId; + identifiers.sessionCorrelationId = meeting.sessionCorrelationId; } if (sessionCorrelationId) { diff --git a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts index 521a1222e29..64a9313aa86 100644 --- a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +++ b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts @@ -30,6 +30,7 @@ describe('internal-plugin-metrics', () => { const fakeMeeting = { id: '1', correlationId: 'correlationId', + sessionCorrelationId: undefined, callStateForMetrics: {}, environment: 'meeting_evn', locusUrl: 'locus/url', @@ -354,6 +355,36 @@ describe('internal-plugin-metrics', () => { assert.deepEqual(res, { correlationId: 'correlationId', + sessionCorrelationId: undefined, + deviceId: 'deviceUrl', + locusId: 'url', + locusStartTime: 'lastActive', + locusUrl: 'locus/url', + machineId: 'installationId', + mediaAgentAlias: 'mediaAgentAlias', + mediaAgentGroupId: 'mediaAgentGroupId', + orgId: 'orgId', + userId: 'userId', + }); + }); + + it('should build identifiers correctly', () => { + cd.device = { + ...cd.device, + config: {installationId: 'installationId'}, + }; + + const res = cd.getIdentifiers({ + mediaConnections: [ + {mediaAgentAlias: 'mediaAgentAlias', mediaAgentGroupId: 'mediaAgentGroupId'}, + ], + meeting: fakeMeeting, + sessionCorrelationId: 'sessionCorrelationId', + }); + + assert.deepEqual(res, { + correlationId: 'correlationId', + sessionCorrelationId: 'sessionCorrelationId', deviceId: 'deviceUrl', locusId: 'url', locusStartTime: 'lastActive', @@ -419,6 +450,7 @@ describe('internal-plugin-metrics', () => { assert.deepEqual(res, { correlationId: 'correlationId', + sessionCorrelationId: undefined, webexConferenceIdStr: 'webexConferenceIdStr1', globalMeetingId: 'globalMeetingId1', deviceId: 'deviceUrl', @@ -452,6 +484,43 @@ describe('internal-plugin-metrics', () => { assert.deepEqual(res, { correlationId: 'correlationId', + sessionCorrelationId: undefined, + webexConferenceIdStr: 'webexConferenceIdStr1', + globalMeetingId: 'globalMeetingId1', + deviceId: 'deviceUrl', + locusId: 'url', + locusStartTime: 'lastActive', + locusUrl: 'locus/url', + mediaAgentAlias: 'mediaAgentAlias', + mediaAgentGroupId: 'mediaAgentGroupId', + orgId: 'orgId', + userId: 'userId', + webexSiteName: 'siteName1', + }); + }); + + it('should build identifiers correctly with a meeting that has sessionCorrelationId', () => { + const res = cd.getIdentifiers({ + mediaConnections: [ + {mediaAgentAlias: 'mediaAgentAlias', mediaAgentGroupId: 'mediaAgentGroupId'}, + ], + webexConferenceIdStr: 'webexConferenceIdStr', + globalMeetingId: 'globalMeetingId', + meeting: { + ...fakeMeeting, + sessionCorrelationId: 'sessionCorrelationId1', + meetingInfo: { + ...fakeMeeting.meetingInfo, + confIdStr: 'webexConferenceIdStr1', + meetingId: 'globalMeetingId1', + siteName: 'siteName1', + }, + }, + }); + + assert.deepEqual(res, { + correlationId: 'correlationId', + sessionCorrelationId: 'sessionCorrelationId1', webexConferenceIdStr: 'webexConferenceIdStr1', globalMeetingId: 'globalMeetingId1', deviceId: 'deviceUrl', @@ -474,6 +543,7 @@ describe('internal-plugin-metrics', () => { assert.deepEqual(res, { correlationId: 'correlationId', + sessionCorrelationId: 'unknown', webexConferenceIdStr: 'webexConferenceIdStr1', deviceId: 'deviceUrl', locusUrl: 'locus-url', @@ -490,6 +560,7 @@ describe('internal-plugin-metrics', () => { assert.deepEqual(res, { correlationId: 'correlationId', + sessionCorrelationId: 'unknown', globalMeetingId: 'globalMeetingId1', deviceId: 'deviceUrl', locusUrl: 'locus-url', @@ -505,6 +576,23 @@ describe('internal-plugin-metrics', () => { assert.deepEqual(res, { correlationId: 'correlationId', + sessionCorrelationId: 'unknown', + deviceId: 'deviceUrl', + locusUrl: 'locus-url', + orgId: 'orgId', + userId: 'userId', + }); + }); + + it('should build identifiers correctly given sessionCorrelationId', () => { + const res = cd.getIdentifiers({ + correlationId: 'correlationId', + sessionCorrelationId: 'sessionCorrelationId', + }); + + assert.deepEqual(res, { + correlationId: 'correlationId', + sessionCorrelationId: 'sessionCorrelationId', deviceId: 'deviceUrl', locusUrl: 'locus-url', orgId: 'orgId', @@ -533,6 +621,7 @@ describe('internal-plugin-metrics', () => { assert.deepEqual(res, { correlationId: 'correlationId', + sessionCorrelationId: 'unknown', locusUrl: 'locus-url', deviceId: 'deviceUrl', orgId: 'orgId', @@ -623,6 +712,7 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId', + sessionCorrelationId: undefined, deviceId: 'deviceUrl', locusId: 'url', locusStartTime: 'lastActive', @@ -648,6 +738,7 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId', + sessionCorrelationId: undefined, deviceId: 'deviceUrl', locusId: 'url', locusStartTime: 'lastActive', @@ -684,6 +775,7 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId', + sessionCorrelationId: undefined, deviceId: 'deviceUrl', locusId: 'url', locusStartTime: 'lastActive', @@ -960,6 +1052,58 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId2', + sessionCorrelationId: undefined, + deviceId: 'deviceUrl', + locusId: 'url', + locusStartTime: 'lastActive', + locusUrl: 'locus/url', + mediaAgentAlias: 'alias', + mediaAgentGroupId: '1', + orgId: 'orgId', + userId: 'userId', + }, + loginType: 'fakeLoginType', + name: 'client.alert.displayed', + userType: 'host', + isConvergedArchitectureEnabled: undefined, + webexSubServiceType: undefined, + }, + eventId: 'my-fake-id', + origin: { + origin: 'fake-origin', + }, + originTime: { + sent: 'not_defined_yet', + triggered: now.toISOString(), + }, + senderCountryCode: 'UK', + version: 1, + }); + }); + + it('should use meeting loginType if present and meetingId provided, with sessionCorrelationId', () => { + const submitToCallDiagnosticsSpy = sinon.spy(cd, 'submitToCallDiagnostics'); + sinon.stub(cd, 'getOrigin').returns({origin: 'fake-origin'}); + const options = { + meetingId: fakeMeeting2.id, + mediaConnections: [{mediaAgentAlias: 'alias', mediaAgentGroupId: '1'}], + sessionCorrelationId: 'sessionCorrelationId1' + }; + + cd.submitClientEvent({ + name: 'client.alert.displayed', + options, + }); + + assert.calledWith(submitToCallDiagnosticsSpy, { + event: { + canProceed: true, + eventData: { + webClientDomain: 'whatever', + }, + identifiers: { + correlationId: 'correlationId2', + sessionCorrelationId: 'sessionCorrelationId1', deviceId: 'deviceUrl', locusId: 'url', locusStartTime: 'lastActive', @@ -1017,6 +1161,7 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId', + sessionCorrelationId: undefined, webexConferenceIdStr: 'webexConferenceIdStr1', globalMeetingId: 'globalMeetingId1', deviceId: 'deviceUrl', @@ -1095,6 +1240,7 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId', + sessionCorrelationId: undefined, deviceId: 'deviceUrl', locusId: 'url', locusStartTime: 'lastActive', @@ -1170,6 +1316,7 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId', + sessionCorrelationId: 'unknown', deviceId: 'deviceUrl', locusUrl: 'locus-url', orgId: 'orgId', @@ -1243,6 +1390,7 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId', + sessionCorrelationId: 'unknown', deviceId: 'deviceUrl', locusUrl: 'locus-url', orgId: 'orgId', @@ -1322,6 +1470,7 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId', + sessionCorrelationId: undefined, deviceId: 'deviceUrl', locusId: 'url', locusStartTime: 'lastActive', @@ -1454,6 +1603,7 @@ describe('internal-plugin-metrics', () => { canProceed: true, identifiers: { correlationId: 'correlationId', + sessionCorrelationId: undefined, webexConferenceIdStr: 'webexConferenceIdStr1', globalMeetingId: 'globalMeetingId1', userId: 'userId', @@ -1493,6 +1643,7 @@ describe('internal-plugin-metrics', () => { canProceed: true, identifiers: { correlationId: 'correlationId', + sessionCorrelationId: undefined, webexConferenceIdStr: 'webexConferenceIdStr1', globalMeetingId: 'globalMeetingId1', userId: 'userId', @@ -1538,6 +1689,7 @@ describe('internal-plugin-metrics', () => { locusUrl: 'locus/url', locusId: 'url', locusStartTime: 'lastActive', + sessionCorrelationId: undefined, }, eventData: {webClientDomain: 'whatever'}, intervals: [{}], @@ -2291,6 +2443,7 @@ describe('internal-plugin-metrics', () => { locusUrl: 'locus/url', orgId: 'orgId', userId: 'userId', + sessionCorrelationId: undefined, }, loginType: 'login-ci', name: 'client.exit.app', diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index 650f5cb193e..5b2d1cab5a5 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -227,6 +227,7 @@ export type AddMediaOptions = { export type CallStateForMetrics = { correlationId?: string; + sessionCorrelationId?: string; joinTrigger?: string; loginType?: string; }; @@ -742,12 +743,29 @@ export default class Meeting extends StatelessWebexPlugin { */ this.callStateForMetrics = attrs.callStateForMetrics || {}; const correlationId = attrs.correlationId || attrs.callStateForMetrics?.correlationId; + const sessionCorrelationId = + attrs.sessionCorrelationId || attrs.callStateForMetrics?.sessionCorrelationId; + if (sessionCorrelationId) { + LoggerProxy.logger.log( + `Meetings:index#constructor --> Initializing the meeting object with session correlation id from app ${correlationId}` + ); + this.callStateForMetrics.sessionCorrelationId = sessionCorrelationId; + } else { + LoggerProxy.logger.log( + `Meetings:index#constructor --> No session correlation id supplied. None will be generated and this field will remain blank` + ); + // TODO: supply a session from the meetings instance + this.callStateForMetrics.sessionCorrelationId = ''; + } if (correlationId) { LoggerProxy.logger.log( `Meetings:index#constructor --> Initializing the meeting object with correlation id from app ${correlationId}` ); this.callStateForMetrics.correlationId = correlationId; } else { + LoggerProxy.logger.log( + `Meetings:index#constructor --> Initializing the meeting object with generated correlation id from sdk ${this.id}` + ); this.callStateForMetrics.correlationId = this.id; } /** @@ -1581,6 +1599,22 @@ export default class Meeting extends StatelessWebexPlugin { this.callStateForMetrics.correlationId = correlationId; } + /** + * Getter - Returns callStateForMetrics.sessionCorrelationId + * @returns {string} + */ + get sessionCorrelationId() { + return this.callStateForMetrics.correlationId; + } + + /** + * Setter - sets callStateForMetrics.sessionCorrelationId + * @param {string} sessionCorrelationId + */ + set sessionCorrelationId(sessionCorrelationId: string) { + this.callStateForMetrics.sessionCorrelationId = sessionCorrelationId; + } + /** * Getter - Returns isoLocalClientMeetingJoinTime * This will be set once on meeting join, and not updated again diff --git a/packages/@webex/plugin-meetings/src/meetings/index.ts b/packages/@webex/plugin-meetings/src/meetings/index.ts index ccbf479842d..a9b79c52364 100644 --- a/packages/@webex/plugin-meetings/src/meetings/index.ts +++ b/packages/@webex/plugin-meetings/src/meetings/index.ts @@ -1081,6 +1081,7 @@ export default class Meetings extends WebexPlugin { * @param {CallStateForMetrics} callStateForMetrics - information about call state for metrics * @param {Object} [meetingInfo] - Pre-fetched complete meeting info * @param {String} [meetingLookupUrl] - meeting info prefetch url + * @param {string} sessionCorrelationId - the optional specified sessionCorrelationId (callStateForMetrics.sessionCorrelationId) can be provided instead * @returns {Promise} A new Meeting. * @public * @memberof Meetings @@ -1094,7 +1095,8 @@ export default class Meetings extends WebexPlugin { failOnMissingMeetingInfo = false, callStateForMetrics: CallStateForMetrics = undefined, meetingInfo = undefined, - meetingLookupUrl = undefined + meetingLookupUrl = undefined, + sessionCorrelationId: string = undefined ) { // Validate meeting information based on the provided destination and // type. This must be performed prior to determining if the meeting is @@ -1105,6 +1107,10 @@ export default class Meetings extends WebexPlugin { callStateForMetrics = {...(callStateForMetrics || {}), correlationId}; } + if (sessionCorrelationId) { + callStateForMetrics = {...(callStateForMetrics || {}), sessionCorrelationId}; + } + return ( this.meetingInfo .fetchInfoOptions(destination, type) diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js index 2b947cfbdb6..dd234046488 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js @@ -307,7 +307,7 @@ describe('plugin-meetings', () => { assert.equal(meeting.resource, uuid2); assert.equal(meeting.deviceUrl, uuid3); assert.equal(meeting.correlationId, correlationId); - assert.deepEqual(meeting.callStateForMetrics, {correlationId}); + assert.deepEqual(meeting.callStateForMetrics, {correlationId, sessionCorrelationId: ''}); assert.deepEqual(meeting.meetingInfo, {}); assert.instanceOf(meeting.members, Members); assert.calledOnceWithExactly( @@ -376,7 +376,7 @@ describe('plugin-meetings', () => { } ); assert.equal(newMeeting.correlationId, newMeeting.id); - assert.deepEqual(newMeeting.callStateForMetrics, {correlationId: newMeeting.id}); + assert.deepEqual(newMeeting.callStateForMetrics, {correlationId: newMeeting.id, sessionCorrelationId: ''}); }); it('correlationId can be provided in callStateForMetrics', () => { @@ -403,6 +403,36 @@ describe('plugin-meetings', () => { correlationId: uuid4, joinTrigger: 'fake-join-trigger', loginType: 'fake-login-type', + sessionCorrelationId: '', + }); + }); + + it('sessionCorrelationId can be provided in callStateForMetrics', () => { + const newMeeting = new Meeting( + { + userId: uuid1, + resource: uuid2, + deviceUrl: uuid3, + locus: {url: url1}, + destination: testDestination, + destinationType: DESTINATION_TYPE.MEETING_ID, + callStateForMetrics: { + correlationId: uuid4, + sessionCorrelationId: uuid1, + joinTrigger: 'fake-join-trigger', + loginType: 'fake-login-type', + }, + }, + { + parent: webex, + } + ); + assert.exists(newMeeting.sessionCorrelationId); + assert.deepEqual(newMeeting.callStateForMetrics, { + correlationId: uuid4, + sessionCorrelationId: uuid1, + joinTrigger: 'fake-join-trigger', + loginType: 'fake-login-type', }); }); @@ -6927,33 +6957,36 @@ describe('plugin-meetings', () => { describe('#setCorrelationId', () => { it('should set the correlationId and return undefined', () => { assert.equal(meeting.correlationId, correlationId); - assert.deepEqual(meeting.callStateForMetrics, {correlationId}); + assert.deepEqual(meeting.callStateForMetrics, {correlationId, sessionCorrelationId: ''}); meeting.setCorrelationId(uuid1); assert.equal(meeting.correlationId, uuid1); - assert.deepEqual(meeting.callStateForMetrics, {correlationId: uuid1}); + assert.deepEqual(meeting.callStateForMetrics, {correlationId: uuid1, sessionCorrelationId: ''}); }); }); describe('#updateCallStateForMetrics', () => { it('should update the callState, overriding existing values', () => { - assert.deepEqual(meeting.callStateForMetrics, {correlationId}); + assert.deepEqual(meeting.callStateForMetrics, {correlationId, sessionCorrelationId: ''}); meeting.updateCallStateForMetrics({ correlationId: uuid1, + sessionCorrelationId: uuid3, joinTrigger: 'jt', loginType: 'lt', }); assert.deepEqual(meeting.callStateForMetrics, { correlationId: uuid1, + sessionCorrelationId: uuid3, joinTrigger: 'jt', loginType: 'lt', }); }); it('should update the callState, keeping non-supplied values', () => { - assert.deepEqual(meeting.callStateForMetrics, {correlationId}); + assert.deepEqual(meeting.callStateForMetrics, {correlationId, sessionCorrelationId: ''}); meeting.updateCallStateForMetrics({joinTrigger: 'jt', loginType: 'lt'}); assert.deepEqual(meeting.callStateForMetrics, { correlationId, + sessionCorrelationId: '', joinTrigger: 'jt', loginType: 'lt', }); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meetings/index.js b/packages/@webex/plugin-meetings/test/unit/spec/meetings/index.js index 937fa1fba73..21629c5f398 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meetings/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meetings/index.js @@ -753,7 +753,9 @@ describe('plugin-meetings', () => { const FAKE_USE_RANDOM_DELAY = true; const correlationId = 'my-correlationId'; + const sessionCorrelationId = 'my-session-correlationId'; const callStateForMetrics = { + sessionCorrelationId: 'my-session-correlationId2', correlationId: 'my-correlationId2', joinTrigger: 'my-join-trigger', loginType: 'my-login-type', @@ -769,11 +771,15 @@ describe('plugin-meetings', () => { {}, correlationId, true, - callStateForMetrics + callStateForMetrics, + undefined, + undefined, + sessionCorrelationId ); assert.calledOnceWithExactly(fakeMeeting.setCallStateForMetrics, { ...callStateForMetrics, correlationId, + sessionCorrelationId, }); }); @@ -814,13 +820,14 @@ describe('plugin-meetings', () => { undefined, meetingInfo, 'meetingLookupURL', + sessionCorrelationId ], [ test1, test2, FAKE_USE_RANDOM_DELAY, {}, - {correlationId}, + {correlationId, sessionCorrelationId}, true, meetingInfo, 'meetingLookupURL', @@ -1719,6 +1726,7 @@ describe('plugin-meetings', () => { const expectedMeetingData = { correlationId: 'my-correlationId', callStateForMetrics: { + sessionCorrelationId: '', correlationId: 'my-correlationId', joinTrigger: 'my-join-trigger', loginType: 'my-login-type', From 541189f2f90bcf467426f8b0045b192cf4809091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edmond=20Vuji=C4=87i?= <67634227+edvujic@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:11:05 +0200 Subject: [PATCH 42/48] feat: add `upgradeChannel` metric (#3873) Co-authored-by: evujici --- .../call-diagnostic-metrics.util.ts | 16 +++++++++++----- .../call-diagnostic-metrics-batcher.ts | 3 ++- .../call-diagnostic/call-diagnostic-metrics.ts | 1 + .../call-diagnostic-metrics.util.ts | 10 +++++++--- .../test/unit/spec/prelogin-metrics-batcher.ts | 4 +++- 5 files changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.util.ts b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.util.ts index 06129964f05..e125a706952 100644 --- a/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.util.ts +++ b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.util.ts @@ -231,13 +231,19 @@ export const getBuildType = ( * @returns {Object} prepared item */ export const prepareDiagnosticMetricItem = (webex: any, item: any) => { + const buildType = getBuildType( + webex, + item.eventPayload?.event?.eventData?.webClientDomain, + item.eventPayload?.event?.eventData?.markAsTestEvent + ); + + // Set upgradeChannel to 'gold' if buildType is 'prod', otherwise to the buildType value + const upgradeChannel = buildType === 'prod' ? 'gold' : buildType; + const origin: Partial = { - buildType: getBuildType( - webex, - item.eventPayload?.event?.eventData?.webClientDomain, - item.eventPayload?.event?.eventData?.markAsTestEvent - ), + buildType, networkType: 'unknown', + upgradeChannel, }; // check event names and append latencies? diff --git a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics-batcher.ts b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics-batcher.ts index 37ff23f1d22..6ae22c73e3f 100644 --- a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics-batcher.ts +++ b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics-batcher.ts @@ -441,7 +441,7 @@ describe('plugin-metrics', () => { // item also gets assigned a delay property but the key is a Symbol and haven't been able to test that.. assert.deepEqual(calls.args[0].eventPayload, { event: 'my.event', - origin: {buildType: 'test', networkType: 'unknown'}, + origin: {buildType: 'test', networkType: 'unknown', upgradeChannel: 'test'}, }); assert.deepEqual(calls.args[0].type, ['diagnostic-event']); @@ -455,6 +455,7 @@ describe('plugin-metrics', () => { origin: { buildType: 'test', networkType: 'unknown', + upgradeChannel: 'test', }, }); assert.deepEqual(prepareDiagnosticMetricItemCalls[0].args[1].type, ['diagnostic-event']); diff --git a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts index 64a9313aa86..ed64b813493 100644 --- a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +++ b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts @@ -2467,6 +2467,7 @@ describe('internal-plugin-metrics', () => { environment: 'meeting_evn', name: 'endpoint', networkType: 'unknown', + upgradeChannel: 'test', userAgent, }, originTime: { diff --git a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts index 1a1768c6e8c..5a85c1ec019 100644 --- a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts +++ b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts @@ -274,7 +274,7 @@ describe('internal-plugin-metrics', () => { describe('prepareDiagnosticMetricItem', () => { let webex: any; - const check = (eventName: string, expectedEvent: any) => { + const check = (eventName: string, expectedEvent: any, expectedUpgradeChannel: string) => { const eventPayload = {event: {name: eventName}}; const item = prepareDiagnosticMetricItem(webex, { eventPayload, @@ -286,6 +286,7 @@ describe('internal-plugin-metrics', () => { origin: { buildType: 'prod', networkType: 'unknown', + upgradeChannel: expectedUpgradeChannel }, event: {name: eventName, ...expectedEvent}, }, @@ -417,11 +418,11 @@ describe('internal-plugin-metrics', () => { ], ].forEach(([eventName, expectedEvent]) => { it(`returns expected result for ${eventName}`, () => { - check(eventName as string, expectedEvent); + check(eventName as string, expectedEvent, 'gold'); }); }); - it('getBuildType returns correct value', () => { + it('sets buildType and upgradeChannel correctly', () => { const item: any = { eventPayload: { event: { @@ -438,11 +439,14 @@ describe('internal-plugin-metrics', () => { // just submit any event prepareDiagnosticMetricItem(webex, item); assert.deepEqual(item.eventPayload.origin.buildType, 'test'); + assert.deepEqual(item.eventPayload.origin.upgradeChannel, 'test'); delete item.eventPayload.origin.buildType; + delete item.eventPayload.origin.upgradeChannel; item.eventPayload.event.eventData.markAsTestEvent = false; prepareDiagnosticMetricItem(webex, item); assert.deepEqual(item.eventPayload.origin.buildType, 'prod'); + assert.deepEqual(item.eventPayload.origin.upgradeChannel, 'gold'); }); }); diff --git a/packages/@webex/internal-plugin-metrics/test/unit/spec/prelogin-metrics-batcher.ts b/packages/@webex/internal-plugin-metrics/test/unit/spec/prelogin-metrics-batcher.ts index 1a5a0d2bf39..889ac210242 100644 --- a/packages/@webex/internal-plugin-metrics/test/unit/spec/prelogin-metrics-batcher.ts +++ b/packages/@webex/internal-plugin-metrics/test/unit/spec/prelogin-metrics-batcher.ts @@ -82,6 +82,7 @@ describe('internal-plugin-metrics', () => { origin: { buildType: 'test', networkType: 'unknown', + upgradeChannel: 'test', }, originTime: { sent: dateAfterBatcherWait.toISOString(), @@ -211,7 +212,7 @@ describe('internal-plugin-metrics', () => { // item also gets assigned a delay property but the key is a Symbol and haven't been able to test that.. assert.deepEqual(calls.args[0].eventPayload, { event: 'my.event', - origin: {buildType: 'test', networkType: 'unknown'}, + origin: {buildType: 'test', networkType: 'unknown', upgradeChannel: 'test'}, }); assert.deepEqual(calls.args[0].type, ['diagnostic-event']); @@ -225,6 +226,7 @@ describe('internal-plugin-metrics', () => { origin: { buildType: 'test', networkType: 'unknown', + upgradeChannel: 'test', }, }); assert.deepEqual(prepareDiagnosticMetricItemCalls[0].args[1].type, ['diagnostic-event']); From cd5a0868c8bf62da74a77c7052c0a1090a4ef625 Mon Sep 17 00:00:00 2001 From: xiaolin5207 <142188008+xiaolin5207@users.noreply.github.com> Date: Tue, 8 Oct 2024 23:22:14 +0800 Subject: [PATCH 43/48] feat(policy): add supportPollingAndQA policy for slido (#3889) --- packages/@webex/plugin-meetings/src/constants.ts | 1 + .../@webex/plugin-meetings/src/meeting/in-meeting-actions.ts | 3 +++ packages/@webex/plugin-meetings/src/meeting/index.ts | 4 ++++ .../test/unit/spec/meeting/in-meeting-actions.ts | 2 ++ .../@webex/plugin-meetings/test/unit/spec/meeting/index.js | 5 +++++ 5 files changed, 15 insertions(+) diff --git a/packages/@webex/plugin-meetings/src/constants.ts b/packages/@webex/plugin-meetings/src/constants.ts index 26fbfcd6c7b..424e831a597 100644 --- a/packages/@webex/plugin-meetings/src/constants.ts +++ b/packages/@webex/plugin-meetings/src/constants.ts @@ -879,6 +879,7 @@ export enum SELF_POLICY { SUPPORT_HDV = 'supportHDV', SUPPORT_PARTICIPANT_LIST = 'supportParticipantList', SUPPORT_VOIP = 'supportVoIP', + SUPPORT_POLLING_AND_QA = 'supportPollingAndQA', } export const DISPLAY_HINTS = { diff --git a/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts b/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts index 3c6648b733c..161e90a9fbe 100644 --- a/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts +++ b/packages/@webex/plugin-meetings/src/meeting/in-meeting-actions.ts @@ -82,6 +82,7 @@ interface IInMeetingActions { supportHDV?: boolean; canShareWhiteBoard?: boolean; enforceVirtualBackground?: boolean; + canPollingAndQA?: boolean; } /** @@ -236,6 +237,7 @@ export default class InMeetingActions implements IInMeetingActions { canShareWhiteBoard = null; + canPollingAndQA = null; /** * Returns all meeting action options * @returns {Object} @@ -314,6 +316,7 @@ export default class InMeetingActions implements IInMeetingActions { supportHQV: this.supportHQV, supportHDV: this.supportHDV, canShareWhiteBoard: this.canShareWhiteBoard, + canPollingAndQA: this.canPollingAndQA, }); /** diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index 5b2d1cab5a5..66dfda5ca9b 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -3826,6 +3826,10 @@ export default class Meeting extends StatelessWebexPlugin { requiredPolicies: [SELF_POLICY.SUPPORT_CHAT], policies: this.selfUserPolicies, }), + canPollingAndQA: ControlsOptionsUtil.hasPolicies({ + requiredPolicies: [SELF_POLICY.SUPPORT_POLLING_AND_QA], + policies: this.selfUserPolicies, + }), canShareApplication: (ControlsOptionsUtil.hasHints({ requiredHints: [DISPLAY_HINTS.SHARE_APPLICATION], diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/in-meeting-actions.ts b/packages/@webex/plugin-meetings/test/unit/spec/meeting/in-meeting-actions.ts index 1ae094cdb0c..9fe27480635 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/in-meeting-actions.ts +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/in-meeting-actions.ts @@ -78,6 +78,7 @@ describe('plugin-meetings', () => { supportHDV: null, canShareWhiteBoard: null, enforceVirtualBackground: null, + canPollingAndQA: null, ...expected, }; @@ -161,6 +162,7 @@ describe('plugin-meetings', () => { 'supportHDV', 'canShareWhiteBoard', 'enforceVirtualBackground', + 'canPollingAndQA', ].forEach((key) => { it(`get and set for ${key} work as expected`, () => { const inMeetingActions = new InMeetingActions(); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js index dd234046488..1aed077cb43 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js @@ -9870,6 +9870,11 @@ describe('plugin-meetings', () => { requiredDisplayHints: [], requiredPolicies: [SELF_POLICY.SUPPORT_ANNOTATION], }, + { + actionName: 'canPollingAndQA', + requiredDisplayHints: [], + requiredPolicies: [SELF_POLICY.SUPPORT_POLLING_AND_QA], + }, ], ({ actionName, From ad8d241f8442c4d75dd61da96ae10a44e1f360e4 Mon Sep 17 00:00:00 2001 From: chrisadubois Date: Tue, 8 Oct 2024 09:13:54 -0700 Subject: [PATCH 44/48] fix(ca): typo (#3892) --- packages/@webex/plugin-meetings/src/meeting/index.ts | 2 +- packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index 66dfda5ca9b..526fd355080 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -1604,7 +1604,7 @@ export default class Meeting extends StatelessWebexPlugin { * @returns {string} */ get sessionCorrelationId() { - return this.callStateForMetrics.correlationId; + return this.callStateForMetrics.sessionCorrelationId; } /** diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js index 1aed077cb43..294a61a6846 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js @@ -428,6 +428,7 @@ describe('plugin-meetings', () => { } ); assert.exists(newMeeting.sessionCorrelationId); + assert.equal(newMeeting.sessionCorrelationId, uuid1); assert.deepEqual(newMeeting.callStateForMetrics, { correlationId: uuid4, sessionCorrelationId: uuid1, From d78f855d393933e0465274cbaff6d8e62c386af0 Mon Sep 17 00:00:00 2001 From: Anna Tsukanova <38460776+antsukanova@users.noreply.github.com> Date: Wed, 9 Oct 2024 11:44:10 +0200 Subject: [PATCH 45/48] fix: update media-core version with roap fix (#3894) Co-authored-by: Anna Tsukanova --- packages/@webex/media-helpers/package.json | 2 +- packages/@webex/plugin-meetings/package.json | 2 +- packages/calling/package.json | 2 +- yarn.lock | 14 +++++++------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/@webex/media-helpers/package.json b/packages/@webex/media-helpers/package.json index 1943ef07360..8a91c6266bf 100644 --- a/packages/@webex/media-helpers/package.json +++ b/packages/@webex/media-helpers/package.json @@ -22,7 +22,7 @@ "deploy:npm": "yarn npm publish" }, "dependencies": { - "@webex/internal-media-core": "2.11.1", + "@webex/internal-media-core": "2.11.3", "@webex/ts-events": "^1.1.0", "@webex/web-media-effects": "2.19.0" }, diff --git a/packages/@webex/plugin-meetings/package.json b/packages/@webex/plugin-meetings/package.json index 367a01d99d7..9772e87d059 100644 --- a/packages/@webex/plugin-meetings/package.json +++ b/packages/@webex/plugin-meetings/package.json @@ -62,7 +62,7 @@ }, "dependencies": { "@webex/common": "workspace:*", - "@webex/internal-media-core": "2.11.1", + "@webex/internal-media-core": "2.11.3", "@webex/internal-plugin-conversation": "workspace:*", "@webex/internal-plugin-device": "workspace:*", "@webex/internal-plugin-llm": "workspace:*", diff --git a/packages/calling/package.json b/packages/calling/package.json index a1d969ff25c..b5684f56853 100644 --- a/packages/calling/package.json +++ b/packages/calling/package.json @@ -37,7 +37,7 @@ }, "dependencies": { "@types/platform": "1.3.4", - "@webex/internal-media-core": "2.11.1", + "@webex/internal-media-core": "2.11.3", "@webex/media-helpers": "workspace:*", "async-mutex": "0.4.0", "buffer": "6.0.3", diff --git a/yarn.lock b/yarn.lock index fab27faf1d9..a0fe1d26df7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7419,7 +7419,7 @@ __metadata: "@typescript-eslint/eslint-plugin": 5.38.1 "@typescript-eslint/parser": 5.38.1 "@web/dev-server": 0.4.5 - "@webex/internal-media-core": 2.11.1 + "@webex/internal-media-core": 2.11.3 "@webex/media-helpers": "workspace:*" async-mutex: 0.4.0 buffer: 6.0.3 @@ -7710,9 +7710,9 @@ __metadata: languageName: unknown linkType: soft -"@webex/internal-media-core@npm:2.11.1": - version: 2.11.1 - resolution: "@webex/internal-media-core@npm:2.11.1" +"@webex/internal-media-core@npm:2.11.3": + version: 2.11.3 + resolution: "@webex/internal-media-core@npm:2.11.3" dependencies: "@babel/runtime": ^7.18.9 "@babel/runtime-corejs2": ^7.25.0 @@ -7725,7 +7725,7 @@ __metadata: uuid: ^8.3.2 webrtc-adapter: ^8.1.2 xstate: ^4.30.6 - checksum: db7d9d40b355b4e5a317f5eadc15a9d7ee5ee62554e9b31f1003c4b6622c36ddc93f378a7a5a3154be2017346636ccfc8a0cb854ddaddc21e7c3b0d359f1aff0 + checksum: b95c917890c98ded1346d093656a8c54cb4ae7c0a8a93ccccaf39e72913f3c2c8a53829d257eb5ac74a087ee2c4583c37ed3752acfbab179e6533761eb9a5ed0 languageName: node linkType: hard @@ -8484,7 +8484,7 @@ __metadata: "@babel/preset-typescript": 7.22.11 "@webex/babel-config-legacy": "workspace:*" "@webex/eslint-config-legacy": "workspace:*" - "@webex/internal-media-core": 2.11.1 + "@webex/internal-media-core": 2.11.3 "@webex/jest-config-legacy": "workspace:*" "@webex/legacy-tools": "workspace:*" "@webex/test-helper-chai": "workspace:*" @@ -8720,7 +8720,7 @@ __metadata: "@webex/babel-config-legacy": "workspace:*" "@webex/common": "workspace:*" "@webex/eslint-config-legacy": "workspace:*" - "@webex/internal-media-core": 2.11.1 + "@webex/internal-media-core": 2.11.3 "@webex/internal-plugin-conversation": "workspace:*" "@webex/internal-plugin-device": "workspace:*" "@webex/internal-plugin-llm": "workspace:*" From 3abb5c9de2734e60381f0fbc3046f8e657366e54 Mon Sep 17 00:00:00 2001 From: Jordan Rowan <86778628+jor-row@users.noreply.github.com> Date: Wed, 9 Oct 2024 16:44:02 +0100 Subject: [PATCH 46/48] fix(internal-plugin-metrics): restricted region wdm error marked as expected (#3897) --- .../src/call-diagnostic/config.ts | 12 +++++++ .../call-diagnostic-metrics.ts | 34 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/packages/@webex/internal-plugin-metrics/src/call-diagnostic/config.ts b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/config.ts index f3b7ea3b048..4faa788dc05 100644 --- a/packages/@webex/internal-plugin-metrics/src/call-diagnostic/config.ts +++ b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/config.ts @@ -129,6 +129,7 @@ export const ERROR_DESCRIPTIONS = { ICE_AND_REACHABILITY_FAILED: 'ICEAndReachabilityFailed', SDP_OFFER_CREATION_ERROR: 'SdpOfferCreationError', SDP_OFFER_CREATION_ERROR_MISSING_CODEC: 'SdpOfferCreationErrorMissingCodec', + WDM_RESTRICTED_REGION: 'WdmRestrictedRegion', }; export const SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP = { @@ -288,6 +289,12 @@ export const SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP = { 100005: 4103, // Depracated because of an issue in the UCF Clients // If both email-hash and domain-hash are null or undefined. 100004: 4103, + + // ---- WDM ---- + // WDM_BLOCKED_ACCESS_BY_COUNTRY_CODE_BANNED_COUNTRY_ERROR_CODE + 4404002: 13000, + // WDM_BLOCKED_ACCESS_BY_COUNTRY_CODE_RESTRICTED_COUNTRY_ERROR_CODE + 4404003: 13000, }; export const CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD: Record> = { @@ -687,6 +694,11 @@ export const CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD: Record { }); }); + it('should generate event error payload correctly for wdm error 4404002', () => { + const res = cd.generateClientEventErrorPayload({ + body: {errorCode: 4404002}, + message: 'Operation denied due to region restriction', + }); + assert.deepEqual(res, { + category: 'expected', + errorDescription: 'WdmRestrictedRegion', + fatal: true, + name: 'other', + shownToUser: false, + serviceErrorCode: 4404002, + errorCode: 13000, + rawErrorMessage: 'Operation denied due to region restriction', + }); + }); + + it('should generate event error payload correctly for wdm error 4404003', () => { + const res = cd.generateClientEventErrorPayload({ + body: {errorCode: 4404003}, + message: 'Operation denied due to region restriction', + }); + assert.deepEqual(res, { + category: 'expected', + errorDescription: 'WdmRestrictedRegion', + fatal: true, + name: 'other', + shownToUser: false, + serviceErrorCode: 4404003, + errorCode: 13000, + rawErrorMessage: 'Operation denied due to region restriction', + }); + }); + describe('httpStatusCode', () => { it('should include httpStatusCode for browser media errors', () => { const res = cd.generateClientEventErrorPayload({ From 4a00a436045425038ad9b2e1cfd619e0e3ba6803 Mon Sep 17 00:00:00 2001 From: chrisadubois Date: Wed, 9 Oct 2024 14:09:51 -0700 Subject: [PATCH 47/48] fix(ca): fix session correlation id unknown (#3900) --- .../call-diagnostic-metrics.ts | 9 +- .../call-diagnostic-metrics.ts | 229 ++++++++++++++++-- 2 files changed, 212 insertions(+), 26 deletions(-) diff --git a/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.ts b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.ts index 2ee5f18b6d7..8a3abeff368 100644 --- a/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.ts +++ b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.ts @@ -289,13 +289,14 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin { sessionCorrelationId, } = options; const identifiers: Event['event']['identifiers'] = { - correlationId: 'unknown', - sessionCorrelationId: 'unknown', + correlationId: 'unknown', // concerned with setting this to unknown. This will fail diagnostic events parsing because it's not a uuid pattern }; if (meeting) { identifiers.correlationId = meeting.correlationId; - identifiers.sessionCorrelationId = meeting.sessionCorrelationId; + if (meeting.sessionCorrelationId) { + identifiers.sessionCorrelationId = meeting.sessionCorrelationId; + } } if (sessionCorrelationId) { @@ -306,6 +307,8 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin { identifiers.correlationId = correlationId; } + // TODO: should we use patterns.uuid to validate correlationId and session correlation id? they will fail the diagnostic events validation pipeline if improperly formatted + if (this.device) { const {device} = this; const {installationId} = device?.config || {}; diff --git a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts index 38ef5ed6849..496de75ce26 100644 --- a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +++ b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts @@ -30,7 +30,6 @@ describe('internal-plugin-metrics', () => { const fakeMeeting = { id: '1', correlationId: 'correlationId', - sessionCorrelationId: undefined, callStateForMetrics: {}, environment: 'meeting_evn', locusUrl: 'locus/url', @@ -53,9 +52,17 @@ describe('internal-plugin-metrics', () => { callStateForMetrics: {loginType: 'fakeLoginType'}, }; + const fakeMeeting3 = { + ...fakeMeeting, + id: '3', + correlationId: 'correlationId3', + sessionCorrelationId: 'sessionCorrelationId3', + } + const fakeMeetings = { 1: fakeMeeting, 2: fakeMeeting2, + 3: fakeMeeting3, }; let webex; @@ -355,7 +362,6 @@ describe('internal-plugin-metrics', () => { assert.deepEqual(res, { correlationId: 'correlationId', - sessionCorrelationId: undefined, deviceId: 'deviceUrl', locusId: 'url', locusStartTime: 'lastActive', @@ -368,7 +374,37 @@ describe('internal-plugin-metrics', () => { }); }); - it('should build identifiers correctly', () => { + [undefined, null, '', false, 0].forEach((sessionCorrelationId) => { + it(`should build identifiers correctly and not add session correlation id if it is falsy: ${sessionCorrelationId}`, () => { + cd.device = { + ...cd.device, + config: {installationId: 'installationId'}, + }; + + const res = cd.getIdentifiers({ + mediaConnections: [ + {mediaAgentAlias: 'mediaAgentAlias', mediaAgentGroupId: 'mediaAgentGroupId'}, + ], + meeting: {...fakeMeeting, sessionCorrelationId}, + sessionCorrelationId: sessionCorrelationId as any, + }); + + assert.deepEqual(res, { + correlationId: 'correlationId', + deviceId: 'deviceUrl', + locusId: 'url', + locusStartTime: 'lastActive', + locusUrl: 'locus/url', + machineId: 'installationId', + mediaAgentAlias: 'mediaAgentAlias', + mediaAgentGroupId: 'mediaAgentGroupId', + orgId: 'orgId', + userId: 'userId', + }); + }); + }); + + it('should build identifiers correctly with sessionCorrelationID as a param', () => { cd.device = { ...cd.device, config: {installationId: 'installationId'}, @@ -397,6 +433,35 @@ describe('internal-plugin-metrics', () => { }); }); + it('should build identifiers correctly with sessionCorrelationID as a param and a meeting with session correlation id, and the param should take precedence', () => { + cd.device = { + ...cd.device, + config: {installationId: 'installationId'}, + }; + + const res = cd.getIdentifiers({ + mediaConnections: [ + {mediaAgentAlias: 'mediaAgentAlias', mediaAgentGroupId: 'mediaAgentGroupId'}, + ], + meeting: {...fakeMeeting, sessionCorrelationId: 'sessionCorrelationId1'}, + sessionCorrelationId: 'sessionCorrelationId', + }); + + assert.deepEqual(res, { + correlationId: 'correlationId', + sessionCorrelationId: 'sessionCorrelationId', + deviceId: 'deviceUrl', + locusId: 'url', + locusStartTime: 'lastActive', + locusUrl: 'locus/url', + machineId: 'installationId', + mediaAgentAlias: 'mediaAgentAlias', + mediaAgentGroupId: 'mediaAgentGroupId', + orgId: 'orgId', + userId: 'userId', + }); + }); + it('should build identifiers correctly with a meeting that has meetingInfo with a webexConferenceIdStr and globalMeetingId, and that should take precedence over the options passed to it', () => { const res = cd.getIdentifiers({ mediaConnections: [ @@ -450,7 +515,6 @@ describe('internal-plugin-metrics', () => { assert.deepEqual(res, { correlationId: 'correlationId', - sessionCorrelationId: undefined, webexConferenceIdStr: 'webexConferenceIdStr1', globalMeetingId: 'globalMeetingId1', deviceId: 'deviceUrl', @@ -484,7 +548,6 @@ describe('internal-plugin-metrics', () => { assert.deepEqual(res, { correlationId: 'correlationId', - sessionCorrelationId: undefined, webexConferenceIdStr: 'webexConferenceIdStr1', globalMeetingId: 'globalMeetingId1', deviceId: 'deviceUrl', @@ -543,7 +606,6 @@ describe('internal-plugin-metrics', () => { assert.deepEqual(res, { correlationId: 'correlationId', - sessionCorrelationId: 'unknown', webexConferenceIdStr: 'webexConferenceIdStr1', deviceId: 'deviceUrl', locusUrl: 'locus-url', @@ -560,7 +622,6 @@ describe('internal-plugin-metrics', () => { assert.deepEqual(res, { correlationId: 'correlationId', - sessionCorrelationId: 'unknown', globalMeetingId: 'globalMeetingId1', deviceId: 'deviceUrl', locusUrl: 'locus-url', @@ -576,7 +637,6 @@ describe('internal-plugin-metrics', () => { assert.deepEqual(res, { correlationId: 'correlationId', - sessionCorrelationId: 'unknown', deviceId: 'deviceUrl', locusUrl: 'locus-url', orgId: 'orgId', @@ -621,7 +681,6 @@ describe('internal-plugin-metrics', () => { assert.deepEqual(res, { correlationId: 'correlationId', - sessionCorrelationId: 'unknown', locusUrl: 'locus-url', deviceId: 'deviceUrl', orgId: 'orgId', @@ -695,12 +754,13 @@ describe('internal-plugin-metrics', () => { options, }); + assert.called(getIdentifiersSpy); assert.calledWith(getIdentifiersSpy, { meeting: fakeMeeting, mediaConnections: [{mediaAgentAlias: 'alias', mediaAgentGroupId: '1'}], webexConferenceIdStr: undefined, - sessionCorrelationId: undefined, globalMeetingId: undefined, + sessionCorrelationId: undefined, }); assert.notCalled(generateClientEventErrorPayloadSpy); assert.calledWith( @@ -712,7 +772,6 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId', - sessionCorrelationId: undefined, deviceId: 'deviceUrl', locusId: 'url', locusStartTime: 'lastActive', @@ -738,7 +797,6 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId', - sessionCorrelationId: undefined, deviceId: 'deviceUrl', locusId: 'url', locusStartTime: 'lastActive', @@ -775,7 +833,142 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId', - sessionCorrelationId: undefined, + deviceId: 'deviceUrl', + locusId: 'url', + locusStartTime: 'lastActive', + locusUrl: 'locus/url', + mediaAgentAlias: 'alias', + mediaAgentGroupId: '1', + orgId: 'orgId', + userId: 'userId', + }, + loginType: 'login-ci', + name: 'client.alert.displayed', + userType: 'host', + isConvergedArchitectureEnabled: undefined, + webexSubServiceType: undefined, + }, + eventId: 'my-fake-id', + origin: { + origin: 'fake-origin', + }, + originTime: { + sent: 'not_defined_yet', + triggered: now.toISOString(), + }, + senderCountryCode: 'UK', + version: 1, + }, + }); + + const webexLoggerLogCalls = webex.logger.log.getCalls(); + assert.deepEqual(webexLoggerLogCalls[1].args, [ + 'call-diagnostic-events -> ', + 'CallDiagnosticMetrics: @submitClientEvent. Submit Client Event CA event.', + `name: client.alert.displayed`, + ]); + }); + + it('should submit client event successfully with meetingId which has a sessionCorrelationId', () => { + const prepareDiagnosticEventSpy = sinon.spy(cd, 'prepareDiagnosticEvent'); + const submitToCallDiagnosticsSpy = sinon.spy(cd, 'submitToCallDiagnostics'); + const generateClientEventErrorPayloadSpy = sinon.spy(cd, 'generateClientEventErrorPayload'); + const getIdentifiersSpy = sinon.spy(cd, 'getIdentifiers'); + const getSubServiceTypeSpy = sinon.spy(cd, 'getSubServiceType'); + sinon.stub(cd, 'getOrigin').returns({origin: 'fake-origin'}); + const validatorSpy = sinon.spy(cd, 'validator'); + const options = { + meetingId: fakeMeeting3.id, + mediaConnections: [{mediaAgentAlias: 'alias', mediaAgentGroupId: '1'}], + }; + + cd.submitClientEvent({ + name: 'client.alert.displayed', + options, + }); + + assert.called(getIdentifiersSpy); + assert.calledWith(getIdentifiersSpy, { + meeting: {...fakeMeeting3, sessionCorrelationId: 'sessionCorrelationId3'}, + mediaConnections: [{mediaAgentAlias: 'alias', mediaAgentGroupId: '1'}], + webexConferenceIdStr: undefined, + globalMeetingId: undefined, + sessionCorrelationId: undefined, + }); + assert.notCalled(generateClientEventErrorPayloadSpy); + assert.calledWith( + prepareDiagnosticEventSpy, + { + canProceed: true, + eventData: { + webClientDomain: 'whatever', + }, + identifiers: { + correlationId: 'correlationId3', + sessionCorrelationId: 'sessionCorrelationId3', + deviceId: 'deviceUrl', + locusId: 'url', + locusStartTime: 'lastActive', + locusUrl: 'locus/url', + mediaAgentAlias: 'alias', + mediaAgentGroupId: '1', + orgId: 'orgId', + userId: 'userId', + }, + loginType: 'login-ci', + name: 'client.alert.displayed', + userType: 'host', + isConvergedArchitectureEnabled: undefined, + webexSubServiceType: undefined, + }, + options + ); + assert.calledWith(submitToCallDiagnosticsSpy, { + event: { + canProceed: true, + eventData: { + webClientDomain: 'whatever', + }, + identifiers: { + correlationId: 'correlationId3', + sessionCorrelationId: 'sessionCorrelationId3', + deviceId: 'deviceUrl', + locusId: 'url', + locusStartTime: 'lastActive', + locusUrl: 'locus/url', + mediaAgentAlias: 'alias', + mediaAgentGroupId: '1', + orgId: 'orgId', + userId: 'userId', + }, + loginType: 'login-ci', + name: 'client.alert.displayed', + userType: 'host', + isConvergedArchitectureEnabled: undefined, + webexSubServiceType: undefined, + }, + eventId: 'my-fake-id', + origin: { + origin: 'fake-origin', + }, + originTime: { + sent: 'not_defined_yet', + triggered: now.toISOString(), + }, + senderCountryCode: 'UK', + version: 1, + }); + assert.calledWith(validatorSpy, { + type: 'ce', + event: { + event: { + canProceed: true, + eventData: { + webClientDomain: 'whatever', + }, + identifiers: { + correlationId: 'correlationId3', + sessionCorrelationId: 'sessionCorrelationId3', deviceId: 'deviceUrl', locusId: 'url', locusStartTime: 'lastActive', @@ -1052,7 +1245,6 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId2', - sessionCorrelationId: undefined, deviceId: 'deviceUrl', locusId: 'url', locusStartTime: 'lastActive', @@ -1161,7 +1353,6 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId', - sessionCorrelationId: undefined, webexConferenceIdStr: 'webexConferenceIdStr1', globalMeetingId: 'globalMeetingId1', deviceId: 'deviceUrl', @@ -1240,7 +1431,6 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId', - sessionCorrelationId: undefined, deviceId: 'deviceUrl', locusId: 'url', locusStartTime: 'lastActive', @@ -1316,7 +1506,6 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId', - sessionCorrelationId: 'unknown', deviceId: 'deviceUrl', locusUrl: 'locus-url', orgId: 'orgId', @@ -1390,7 +1579,6 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId', - sessionCorrelationId: 'unknown', deviceId: 'deviceUrl', locusUrl: 'locus-url', orgId: 'orgId', @@ -1470,7 +1658,6 @@ describe('internal-plugin-metrics', () => { }, identifiers: { correlationId: 'correlationId', - sessionCorrelationId: undefined, deviceId: 'deviceUrl', locusId: 'url', locusStartTime: 'lastActive', @@ -1603,7 +1790,6 @@ describe('internal-plugin-metrics', () => { canProceed: true, identifiers: { correlationId: 'correlationId', - sessionCorrelationId: undefined, webexConferenceIdStr: 'webexConferenceIdStr1', globalMeetingId: 'globalMeetingId1', userId: 'userId', @@ -1643,7 +1829,6 @@ describe('internal-plugin-metrics', () => { canProceed: true, identifiers: { correlationId: 'correlationId', - sessionCorrelationId: undefined, webexConferenceIdStr: 'webexConferenceIdStr1', globalMeetingId: 'globalMeetingId1', userId: 'userId', @@ -1689,7 +1874,6 @@ describe('internal-plugin-metrics', () => { locusUrl: 'locus/url', locusId: 'url', locusStartTime: 'lastActive', - sessionCorrelationId: undefined, }, eventData: {webClientDomain: 'whatever'}, intervals: [{}], @@ -2477,7 +2661,6 @@ describe('internal-plugin-metrics', () => { locusUrl: 'locus/url', orgId: 'orgId', userId: 'userId', - sessionCorrelationId: undefined, }, loginType: 'login-ci', name: 'client.exit.app', From 988ebd583db6f54a93c4c8edcad0236eaec43ece Mon Sep 17 00:00:00 2001 From: Rajesh Kumar <131742425+rarajes2@users.noreply.github.com> Date: Thu, 10 Oct 2024 08:12:17 +0530 Subject: [PATCH 48/48] refactor(internal-plugin-metrics): move rtcMetrics from plugin-meetings (#3871) --- .../@webex/internal-plugin-metrics/src/index.ts | 2 ++ .../src/rtcMetrics/constants.ts | 0 .../src/rtcMetrics/index.ts | 2 +- .../spec/call-diagnostic/call-diagnostic-metrics.ts | 11 ++++++++--- .../test/unit/spec/rtcMetrics/index.ts | 3 ++- packages/@webex/plugin-meetings/src/media/index.ts | 2 +- .../@webex/plugin-meetings/src/meeting/index.ts | 2 +- .../plugin-meetings/test/unit/spec/meeting/index.js | 13 +++++-------- 8 files changed, 20 insertions(+), 15 deletions(-) rename packages/@webex/{plugin-meetings => internal-plugin-metrics}/src/rtcMetrics/constants.ts (100%) rename packages/@webex/{plugin-meetings => internal-plugin-metrics}/src/rtcMetrics/index.ts (98%) rename packages/@webex/{plugin-meetings => internal-plugin-metrics}/test/unit/spec/rtcMetrics/index.ts (98%) diff --git a/packages/@webex/internal-plugin-metrics/src/index.ts b/packages/@webex/internal-plugin-metrics/src/index.ts index 7247332be85..551d7ca4453 100644 --- a/packages/@webex/internal-plugin-metrics/src/index.ts +++ b/packages/@webex/internal-plugin-metrics/src/index.ts @@ -25,6 +25,7 @@ import CallDiagnosticLatencies from './call-diagnostic/call-diagnostic-metrics-l import BehavioralMetrics from './behavioral-metrics'; import OperationalMetrics from './operational-metrics'; import BusinessMetrics from './business-metrics'; +import RtcMetrics from './rtcMetrics'; registerInternalPlugin('metrics', Metrics, { config, @@ -47,6 +48,7 @@ export { BehavioralMetrics, OperationalMetrics, BusinessMetrics, + RtcMetrics, }; export type { ClientEvent, diff --git a/packages/@webex/plugin-meetings/src/rtcMetrics/constants.ts b/packages/@webex/internal-plugin-metrics/src/rtcMetrics/constants.ts similarity index 100% rename from packages/@webex/plugin-meetings/src/rtcMetrics/constants.ts rename to packages/@webex/internal-plugin-metrics/src/rtcMetrics/constants.ts diff --git a/packages/@webex/plugin-meetings/src/rtcMetrics/index.ts b/packages/@webex/internal-plugin-metrics/src/rtcMetrics/index.ts similarity index 98% rename from packages/@webex/plugin-meetings/src/rtcMetrics/index.ts rename to packages/@webex/internal-plugin-metrics/src/rtcMetrics/index.ts index 6e9f6790011..cc1f3f6b415 100644 --- a/packages/@webex/plugin-meetings/src/rtcMetrics/index.ts +++ b/packages/@webex/internal-plugin-metrics/src/rtcMetrics/index.ts @@ -1,6 +1,6 @@ /* eslint-disable class-methods-use-this */ -import {CallDiagnosticUtils} from '@webex/internal-plugin-metrics'; import uuid from 'uuid'; +import * as CallDiagnosticUtils from '../call-diagnostic/call-diagnostic-metrics.util'; import RTC_METRICS from './constants'; const parseJsonPayload = (payload: any[]): any | null => { diff --git a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts index 496de75ce26..6a08680e0e8 100644 --- a/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +++ b/packages/@webex/internal-plugin-metrics/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts @@ -1,7 +1,9 @@ import sinon from 'sinon'; +import bowser from 'bowser'; import {assert} from '@webex/test-helper-chai'; import {WebexHttpError} from '@webex/webex-core'; import {BrowserDetection} from '@webex/common'; +import window from 'global/window'; import { CallDiagnosticLatencies, CallDiagnosticMetrics, @@ -1012,6 +1014,9 @@ describe('internal-plugin-metrics', () => { const getIdentifiersSpy = sinon.spy(cd, 'getIdentifiers'); const getSubServiceTypeSpy = sinon.spy(cd, 'getSubServiceType'); const validatorSpy = sinon.spy(cd, 'validator'); + sinon.stub(window.navigator, 'userAgent').get(() => userAgent); + sinon.stub(bowser, 'getParser').returns(userAgent); + const options = { meetingId: fakeMeeting.id, mediaConnections: [{mediaAgentAlias: 'alias', mediaAgentGroupId: '1'}], @@ -1040,7 +1045,7 @@ describe('internal-plugin-metrics', () => { assert.deepEqual(webexLoggerLogCalls[2].args, [ 'call-diagnostic-events -> ', 'CallDiagnosticMetrics: @createClientEventObjectInMeeting => collected browser data', - '{"error":"unable to access window.navigator.userAgent"}', + `${JSON.stringify(userAgent)}`, ]); assert.deepEqual(webexLoggerLogCalls[3].args, [ @@ -2768,11 +2773,11 @@ describe('internal-plugin-metrics', () => { // The method is called in beforeEach itself. We are just testing it here it('sets the received deviceInfo to call-diagnostics', () => { const webexLoggerLogCalls = webex.logger.log.getCalls(); - const device = { userId: 'userId', url: 'deviceUrl', orgId: 'orgId' }; + const device = {userId: 'userId', url: 'deviceUrl', orgId: 'orgId'}; assert.deepEqual(webexLoggerLogCalls[0].args, [ 'CallDiagnosticMetrics: @setDeviceInfo called', - device + device, ]); assert.deepEqual(cd.device, device); diff --git a/packages/@webex/plugin-meetings/test/unit/spec/rtcMetrics/index.ts b/packages/@webex/internal-plugin-metrics/test/unit/spec/rtcMetrics/index.ts similarity index 98% rename from packages/@webex/plugin-meetings/test/unit/spec/rtcMetrics/index.ts rename to packages/@webex/internal-plugin-metrics/test/unit/spec/rtcMetrics/index.ts index cdcc25f3430..12eebc0e31c 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/rtcMetrics/index.ts +++ b/packages/@webex/internal-plugin-metrics/test/unit/spec/rtcMetrics/index.ts @@ -1,5 +1,5 @@ import 'jsdom-global/register'; -import RtcMetrics from '@webex/plugin-meetings/src/rtcMetrics'; +import RtcMetrics from '../../../../src/rtcMetrics'; import MockWebex from '@webex/test-helper-mock-webex'; import {assert} from '@webex/test-helper-chai'; import sinon from 'sinon'; @@ -25,6 +25,7 @@ describe('RtcMetrics', () => { beforeEach(() => { clock = sinon.useFakeTimers(); + window.setInterval = setInterval; webex = new MockWebex(); metrics = new RtcMetrics(webex, 'mock-meeting-id', 'mock-correlation-id'); anonymizeIpSpy = sandbox.spy(metrics, 'anonymizeIp'); diff --git a/packages/@webex/plugin-meetings/src/media/index.ts b/packages/@webex/plugin-meetings/src/media/index.ts index eac32f8e533..28c014e4852 100644 --- a/packages/@webex/plugin-meetings/src/media/index.ts +++ b/packages/@webex/plugin-meetings/src/media/index.ts @@ -15,12 +15,12 @@ import { LocalSystemAudioStream, LocalMicrophoneStream, } from '@webex/media-helpers'; +import {RtcMetrics} from '@webex/internal-plugin-metrics'; import LoggerProxy from '../common/logs/logger-proxy'; import {MEDIA_TRACK_CONSTRAINT} from '../constants'; import Config from '../config'; import StaticConfig from '../common/config'; import BrowserDetection from '../common/browser-detection'; -import RtcMetrics from '../rtcMetrics'; const {isBrowser} = BrowserDetection(); diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index 526fd355080..c1d14b2dc48 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -10,6 +10,7 @@ import { ClientEventLeaveReason, CallDiagnosticUtils, CALL_DIAGNOSTIC_CONFIG, + RtcMetrics, } from '@webex/internal-plugin-metrics'; import {ClientEvent as RawClientEvent} from '@webex/event-dictionary-ts'; @@ -155,7 +156,6 @@ import ControlsOptionsManager from '../controls-options-manager'; import PermissionError from '../common/errors/permission'; import {LocusMediaRequest} from './locusMediaRequest'; import {ConnectionStateHandler, ConnectionStateEvent} from './connectionStateHandler'; -import RtcMetrics from '../rtcMetrics'; // default callback so we don't call an undefined function, but in practice it should never be used const DEFAULT_ICE_PHASE_CALLBACK = () => 'JOIN_MEETING_FINAL'; diff --git a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js index 294a61a6846..f9bc7b813dc 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js @@ -5,7 +5,6 @@ import 'jsdom-global/register'; import {cloneDeep, forEach, isEqual, isUndefined} from 'lodash'; import sinon from 'sinon'; import * as InternalMediaCoreModule from '@webex/internal-media-core'; -import * as RtcMetricsModule from '@webex/plugin-meetings/src/rtcMetrics'; import * as RemoteMediaManagerModule from '@webex/plugin-meetings/src/multistream/remoteMediaManager'; import StateMachine from 'javascript-state-machine'; import uuid from 'uuid'; @@ -2487,8 +2486,8 @@ describe('plugin-meetings', () => { }); it('should create rtcMetrics and pass them to Media.createMediaConnection()', async () => { - const fakeRtcMetrics = {id: 'fake rtc metrics object'}; - const rtcMetricsCtor = sinon.stub(RtcMetricsModule, 'default').returns(fakeRtcMetrics); + const setIntervalOriginal = window.setInterval; + window.setInterval = sinon.stub().returns(1); // setup the minimum mocks required for multistream connection fakeMediaConnection.createSendSlot = sinon.stub().returns({ @@ -2509,8 +2508,6 @@ describe('plugin-meetings', () => { mediaSettings: {}, }); - assert.calledOnceWithExactly(rtcMetricsCtor, webex, meeting.id, meeting.correlationId); - // check that rtcMetrics was passed to Media.createMediaConnection assert.calledOnce(Media.createMediaConnection); assert.calledWith( @@ -2518,10 +2515,10 @@ describe('plugin-meetings', () => { true, meeting.getMediaConnectionDebugId(), meeting.id, - sinon.match({ - rtcMetrics: fakeRtcMetrics, - }) + sinon.match.hasNested('rtcMetrics.webex', webex) ); + + window.setInterval = setIntervalOriginal; }); it('should pass the turn server info to the peer connection', async () => {