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/docs/samples/browser-plugin-meetings/app.js b/docs/samples/browser-plugin-meetings/app.js
index 2e4db88b0eb..04e4d41dfd0 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;
@@ -1387,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/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 @@
+
NOTE: The destination shall be "Person ID" or "Email".
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/@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()', () => {
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()', () => {
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/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..a86d7bc9651
--- /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: {
+ key: name,
+ client_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/call-diagnostic/call-diagnostic-metrics.ts b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.ts
index 2da21fa487a..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
@@ -75,6 +75,7 @@ type GetIdentifiersOptions = {
meeting?: any;
mediaConnections?: any[];
correlationId?: string;
+ sessionCorrelationId?: string;
preLoginId?: string;
globalMeetingId?: string;
webexConferenceIdStr?: string;
@@ -285,19 +286,29 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
webexConferenceIdStr,
globalMeetingId,
preLoginId,
+ sessionCorrelationId,
} = options;
const identifiers: Event['event']['identifiers'] = {
- correlationId: '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;
+ if (meeting.sessionCorrelationId) {
+ identifiers.sessionCorrelationId = meeting.sessionCorrelationId;
+ }
+ }
+
+ if (sessionCorrelationId) {
+ identifiers.sessionCorrelationId = sessionCorrelationId;
}
if (correlationId) {
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 || {};
@@ -455,6 +466,9 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
},
intervals: payload.intervals,
callingServiceType: 'LOCUS',
+ meetingJoinInfo: {
+ clientSignallingProtocol: 'WebRTC',
+ },
sourceMetadata: {
applicationSoftwareType: CLIENT_NAME,
// @ts-ignore
@@ -643,7 +657,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);
@@ -670,6 +690,7 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
mediaConnections: meeting?.mediaConnections || mediaConnections,
webexConferenceIdStr,
globalMeetingId,
+ sessionCorrelationId,
});
// create client event object
@@ -711,11 +732,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/call-diagnostic/call-diagnostic-metrics.util.ts b/packages/@webex/internal-plugin-metrics/src/call-diagnostic/call-diagnostic-metrics.util.ts
index a0ba6e791eb..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
@@ -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') ||
@@ -225,12 +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(
- 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/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}
+ */
+ 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..551d7ca4453 100644
--- a/packages/@webex/internal-plugin-metrics/src/index.ts
+++ b/packages/@webex/internal-plugin-metrics/src/index.ts
@@ -22,7 +22,10 @@ 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';
+import RtcMetrics from './rtcMetrics';
registerInternalPlugin('metrics', Metrics, {
config,
@@ -43,6 +46,9 @@ export {
CallDiagnosticLatencies,
CallDiagnosticMetrics,
BehavioralMetrics,
+ OperationalMetrics,
+ BusinessMetrics,
+ RtcMetrics,
};
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..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;
@@ -102,7 +103,7 @@ export interface ClientEvent {
options?: SubmitClientEventOptions;
}
-export interface BehavioralEventContext {
+export interface DeviceContext {
app: {version: string};
device: {id: string};
locale: string;
@@ -112,23 +113,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 +168,8 @@ export type MetricEventNames =
| InternalEvent['name']
| ClientEvent['name']
| BehavioralEvent['metricName']
- | OperationalEvent['name']
+ | OperationalEvent['metricName']
+ | BusinessEvent['eventPayload']['metricName']
| FeatureEvent['name']
| MediaQualityEvent['name'];
@@ -190,7 +205,7 @@ export type SubmitBehavioralEvent = (args: {
agent: MetricEventAgent;
target: string;
verb: MetricEventVerb;
- payload?: BehavioralEventPayload;
+ payload?: EventPayload;
}) => void;
export type SubmitClientEvent = (args: {
@@ -200,9 +215,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..902ce86c948 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;
});
}
@@ -86,11 +90,61 @@ 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() {
- return this.behavioralMetrics.isReadyToSubmitBehavioralEvents();
+ this.lazyBuildBehavioralMetrics();
+
+ return this.behavioralMetrics?.isReadyToSubmitEvents() ?? false;
+ }
+
+ /**
+ * @returns true once we have the deviceId we need to submit operational events
+ */
+ isReadyToSubmitOperationalEvents() {
+ this.lazyBuildOperationalMetrics();
+
+ return this.operationalMetrics?.isReadyToSubmitEvents() ?? false;
+ }
+
+ /**
+ * @returns true once we have the deviceId we need to submit buisness events
+ */
+ isReadyToSubmitBusinessEvents() {
+ this.lazyBuildBusinessMetrics();
+
+ return this.businessMetrics?.isReadyToSubmitEvents() ?? false;
}
/**
@@ -108,9 +162,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 +173,8 @@ class Metrics extends WebexPlugin {
return Promise.resolve();
}
+ this.lazyBuildBehavioralMetrics();
+
return this.behavioralMetrics.submitBehavioralEvent({product, agent, target, verb, payload});
}
@@ -126,16 +182,38 @@ 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();
+ }
+
+ this.lazyBuildOperationalMetrics();
+
+ 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();
+ }
+
+ this.lazyBuildBusinessMetrics();
+
+ 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/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 79%
rename from packages/@webex/plugin-meetings/src/rtcMetrics/index.ts
rename to packages/@webex/internal-plugin-metrics/src/rtcMetrics/index.ts
index a86c3eb9f33..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 => {
@@ -34,6 +34,8 @@ export default class RtcMetrics {
connectionId: string;
+ shouldSendMetricsOnNextStatsReport: 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();
}
/**
@@ -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,6 +91,13 @@ export default class RtcMetrics {
this.metricsQueue.push(data);
+ 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.shouldSendMetricsOnNextStatsReport = false;
+ }
+
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 +107,7 @@ export default class RtcMetrics {
parsedPayload.value === 'failed'
) {
this.sendMetricsInQueue();
- this.setNewConnectionId();
+ this.resetConnection();
}
} catch (e) {
console.error(e);
@@ -130,8 +149,9 @@ export default class RtcMetrics {
*
* @returns {void}
*/
- private setNewConnectionId() {
+ private resetConnection() {
this.connectionId = uuid.v4();
+ this.shouldSendMetricsOnNextStatsReport = true;
}
/**
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..f0d30b24579
--- /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(),
+ },
+ },
+ key: 'foobar',
+ browserDetails: {
+ browser: getBrowserName(),
+ browserHeight: window.innerHeight,
+ browserVersion: getBrowserVersion(),
+ browserWidth: window.innerWidth,
+ domain: window.location.hostname,
+ inIframe: false,
+ locale: window.navigator.language,
+ os: getOSNameInternal(),
+ },
+ 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.client_timestamp)
+ })
+ })
+ });
+});
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 ea534d02cca..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,
@@ -52,9 +54,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;
@@ -366,12 +376,101 @@ describe('internal-plugin-metrics', () => {
});
});
+ [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'},
+ };
+
+ 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',
+ locusUrl: 'locus/url',
+ machineId: 'installationId',
+ mediaAgentAlias: 'mediaAgentAlias',
+ mediaAgentGroupId: 'mediaAgentGroupId',
+ orgId: 'orgId',
+ userId: 'userId',
+ });
+ });
+
+ 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: [
{mediaAgentAlias: 'mediaAgentAlias', mediaAgentGroupId: 'mediaAgentGroupId'},
],
webexConferenceIdStr: 'webexConferenceIdStr',
+ sessionCorrelationId: 'sessionCorrelationId',
globalMeetingId: 'globalMeetingId',
meeting: {
...fakeMeeting,
@@ -386,6 +485,7 @@ describe('internal-plugin-metrics', () => {
assert.deepEqual(res, {
correlationId: 'correlationId',
webexConferenceIdStr: 'webexConferenceIdStr1',
+ sessionCorrelationId: 'sessionCorrelationId',
globalMeetingId: 'globalMeetingId1',
deviceId: 'deviceUrl',
locusId: 'url',
@@ -464,6 +564,42 @@ describe('internal-plugin-metrics', () => {
});
});
+ 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',
+ locusId: 'url',
+ locusStartTime: 'lastActive',
+ locusUrl: 'locus/url',
+ mediaAgentAlias: 'mediaAgentAlias',
+ mediaAgentGroupId: 'mediaAgentGroupId',
+ orgId: 'orgId',
+ userId: 'userId',
+ webexSiteName: 'siteName1',
+ });
+ });
+
it('should build identifiers correctly given webexConferenceIdStr', () => {
const res = cd.getIdentifiers({
correlationId: 'correlationId',
@@ -510,6 +646,22 @@ describe('internal-plugin-metrics', () => {
});
});
+ 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',
+ userId: 'userId',
+ });
+ });
+
it('should throw Error if correlationId is missing', () => {
assert.throws(() =>
cd.getIdentifiers({
@@ -604,11 +756,13 @@ describe('internal-plugin-metrics', () => {
options,
});
+ assert.called(getIdentifiersSpy);
assert.calledWith(getIdentifiersSpy, {
meeting: fakeMeeting,
mediaConnections: [{mediaAgentAlias: 'alias', mediaAgentGroupId: '1'}],
webexConferenceIdStr: undefined,
globalMeetingId: undefined,
+ sessionCorrelationId: undefined,
});
assert.notCalled(generateClientEventErrorPayloadSpy);
assert.calledWith(
@@ -717,6 +871,142 @@ describe('internal-plugin-metrics', () => {
]);
});
+ 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',
+ 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 log browser data, but only for the first call diagnostic event', () => {
const prepareDiagnosticEventSpy = sinon.spy(cd, 'prepareDiagnosticEvent');
const submitToCallDiagnosticsSpy = sinon.spy(cd, 'submitToCallDiagnostics');
@@ -724,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'}],
@@ -752,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, [
@@ -762,7 +1055,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 +1066,7 @@ describe('internal-plugin-metrics', () => {
correlationId: 'correlationId',
webexConferenceIdStr: 'webexConferenceIdStr1',
globalMeetingId: 'globalMeetingId1',
+ sessionCorrelationId: 'sessionCorrelationId1'
};
cd.submitClientEvent({
@@ -784,6 +1078,7 @@ describe('internal-plugin-metrics', () => {
correlationId: 'correlationId',
webexConferenceIdStr: 'webexConferenceIdStr1',
globalMeetingId: 'globalMeetingId1',
+ sessionCorrelationId: 'sessionCorrelationId1',
preLoginId: undefined,
});
@@ -798,6 +1093,7 @@ describe('internal-plugin-metrics', () => {
identifiers: {
correlationId: 'correlationId',
webexConferenceIdStr: 'webexConferenceIdStr1',
+ sessionCorrelationId: 'sessionCorrelationId1',
globalMeetingId: 'globalMeetingId1',
deviceId: 'deviceUrl',
locusUrl: 'locus-url',
@@ -818,6 +1114,7 @@ describe('internal-plugin-metrics', () => {
identifiers: {
correlationId: 'correlationId',
webexConferenceIdStr: 'webexConferenceIdStr1',
+ sessionCorrelationId: 'sessionCorrelationId1',
globalMeetingId: 'globalMeetingId1',
deviceId: 'deviceUrl',
locusUrl: 'locus-url',
@@ -863,6 +1160,7 @@ describe('internal-plugin-metrics', () => {
webexConferenceIdStr: 'webexConferenceIdStr1',
globalMeetingId: 'globalMeetingId1',
preLoginId: 'myPreLoginId',
+ sessionCorrelationId: 'sessionCorrelationId1'
};
cd.submitClientEvent({
@@ -875,6 +1173,7 @@ describe('internal-plugin-metrics', () => {
webexConferenceIdStr: 'webexConferenceIdStr1',
globalMeetingId: 'globalMeetingId1',
preLoginId: 'myPreLoginId',
+ sessionCorrelationId: 'sessionCorrelationId1'
});
assert.notCalled(generateClientEventErrorPayloadSpy);
@@ -887,6 +1186,7 @@ describe('internal-plugin-metrics', () => {
},
identifiers: {
correlationId: 'correlationId',
+ sessionCorrelationId: 'sessionCorrelationId1',
webexConferenceIdStr: 'webexConferenceIdStr1',
globalMeetingId: 'globalMeetingId1',
deviceId: 'deviceUrl',
@@ -913,6 +1213,7 @@ describe('internal-plugin-metrics', () => {
canProceed: true,
identifiers: {
correlationId: 'correlationId',
+ sessionCorrelationId: 'sessionCorrelationId1',
userId: 'myPreLoginId',
deviceId: 'deviceUrl',
orgId: 'orgId',
@@ -977,6 +1278,57 @@ describe('internal-plugin-metrics', () => {
});
});
+ 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',
+ 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('it should include errors if provided with meetingId', () => {
sinon.stub(cd, 'getOrigin').returns({origin: 'fake-origin'});
const submitToCallDiagnosticsSpy = sinon.spy(cd, 'submitToCallDiagnostics');
@@ -1417,6 +1769,7 @@ describe('internal-plugin-metrics', () => {
meetingId: fakeMeeting.id,
webexConferenceIdStr: 'webexConferenceIdStr1',
globalMeetingId: 'globalMeetingId1',
+ sessionCorrelationId: 'sessionCorrelationId1'
};
cd.submitMQE({
@@ -1454,6 +1807,9 @@ describe('internal-plugin-metrics', () => {
eventData: {webClientDomain: 'whatever'},
intervals: [{}],
callingServiceType: 'LOCUS',
+ meetingJoinInfo: {
+ clientSignallingProtocol: 'WebRTC',
+ },
sourceMetadata: {
applicationSoftwareType: 'webex-js-sdk',
applicationSoftwareVersion: 'webex-version',
@@ -1490,6 +1846,9 @@ describe('internal-plugin-metrics', () => {
eventData: {webClientDomain: 'whatever'},
intervals: [{}],
callingServiceType: 'LOCUS',
+ meetingJoinInfo: {
+ clientSignallingProtocol: 'WebRTC',
+ },
sourceMetadata: {
applicationSoftwareType: 'webex-js-sdk',
applicationSoftwareVersion: 'webex-version',
@@ -1524,6 +1883,9 @@ describe('internal-plugin-metrics', () => {
eventData: {webClientDomain: 'whatever'},
intervals: [{}],
callingServiceType: 'LOCUS',
+ meetingJoinInfo: {
+ clientSignallingProtocol: 'WebRTC',
+ },
sourceMetadata: {
applicationSoftwareType: 'webex-js-sdk',
applicationSoftwareVersion: 'webex-version',
@@ -2015,6 +2377,40 @@ describe('internal-plugin-metrics', () => {
});
});
+ 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({
@@ -2293,6 +2689,7 @@ describe('internal-plugin-metrics', () => {
environment: 'meeting_evn',
name: 'endpoint',
networkType: 'unknown',
+ upgradeChannel: 'test',
userAgent,
},
originTime: {
@@ -2376,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/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..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
@@ -235,6 +235,8 @@ describe('internal-plugin-metrics', () => {
});
describe('getBuildType', () => {
+ const webex = {internal: {metrics: {config: {}}}};
+
beforeEach(() => {
process.env.NODE_ENV = 'production';
});
@@ -246,25 +248,33 @@ 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'
+ );
});
});
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,
@@ -276,6 +286,7 @@ describe('internal-plugin-metrics', () => {
origin: {
buildType: 'prod',
networkType: 'unknown',
+ upgradeChannel: expectedUpgradeChannel
},
event: {name: eventName, ...expectedEvent},
},
@@ -407,19 +418,19 @@ 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: {
name: 'client.exit.app',
eventData: {
markAsTestEvent: true,
- webClientDomain: 'https://web.webex.com'
- }
+ webClientDomain: 'https://web.webex.com',
+ },
},
},
type: ['diagnostic-event'],
@@ -428,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/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',
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'],
+ });
+ })
+ })
+ });
+});
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']);
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 75%
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 7a0ff4eb3c4..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');
@@ -120,4 +121,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);
+ });
});
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/media-helpers/package.json b/packages/@webex/media-helpers/package.json
index b95c6dd1c99..8a91c6266bf 100644
--- a/packages/@webex/media-helpers/package.json
+++ b/packages/@webex/media-helpers/package.json
@@ -22,9 +22,9 @@
"deploy:npm": "yarn npm publish"
},
"dependencies": {
- "@webex/internal-media-core": "2.9.1",
+ "@webex/internal-media-core": "2.11.3",
"@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/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);
+ })
+ });
});
});
diff --git a/packages/@webex/plugin-meetings/package.json b/packages/@webex/plugin-meetings/package.json
index 02c7f99e073..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.9.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/@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/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/media/index.ts b/packages/@webex/plugin-meetings/src/media/index.ts
index 15d2376106e..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();
@@ -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,
@@ -163,6 +161,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 || '',
@@ -179,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/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 e255e3f633b..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';
@@ -24,6 +25,8 @@ import {
RoapMessage,
StatsAnalyzer,
StatsAnalyzerEventNames,
+ NetworkQualityEventNames,
+ NetworkQualityMonitor,
} from '@webex/internal-media-core';
import {
@@ -54,7 +57,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';
@@ -225,6 +227,7 @@ export type AddMediaOptions = {
export type CallStateForMetrics = {
correlationId?: string;
+ sessionCorrelationId?: string;
joinTrigger?: string;
loginType?: string;
};
@@ -536,6 +539,7 @@ export default class Meeting extends StatelessWebexPlugin {
id: string;
isMultistream: boolean;
locusUrl: string;
+ #isoLocalClientMeetingJoinTime?: string;
mediaConnections: any[];
mediaId?: string;
meetingFiniteStateMachine: any;
@@ -695,6 +699,7 @@ export default class Meeting extends StatelessWebexPlugin {
private connectionStateHandler?: ConnectionStateHandler;
private iceCandidateErrors: Map;
private iceCandidatesCount: number;
+ private rtcMetrics?: RtcMetrics;
/**
* @param {Object} attrs
@@ -738,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;
}
/**
@@ -1518,6 +1540,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;
}
/**
@@ -1566,6 +1599,31 @@ export default class Meeting extends StatelessWebexPlugin {
this.callStateForMetrics.correlationId = correlationId;
}
+ /**
+ * Getter - Returns callStateForMetrics.sessionCorrelationId
+ * @returns {string}
+ */
+ get sessionCorrelationId() {
+ return this.callStateForMetrics.sessionCorrelationId;
+ }
+
+ /**
+ * 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
+ * @returns {string | undefined}
+ */
+ get isoLocalClientMeetingJoinTime(): string | undefined {
+ return this.#isoLocalClientMeetingJoinTime;
+ }
+
/**
* Set meeting info and trigger `MEETING_INFO_AVAILABLE` event
* @param {any} info
@@ -3155,6 +3213,7 @@ export default class Meeting extends StatelessWebexPlugin {
options: {meetingId: this.id},
});
}
+ this.rtcMetrics?.sendNextMetrics();
this.updateLLMConnection();
});
@@ -3767,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],
@@ -5228,6 +5291,11 @@ export default class Meeting extends StatelessWebexPlugin {
this.meetingFiniteStateMachine.join();
this.setupLocusMediaRequest();
+ // @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, {
@@ -6306,14 +6374,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
@@ -6500,7 +6571,7 @@ export default class Meeting extends StatelessWebexPlugin {
});
this.setupStatsAnalyzerEventHandlers();
this.networkQualityMonitor.on(
- EVENT_TRIGGERS.NETWORK_QUALITY,
+ NetworkQualityEventNames.NETWORK_QUALITY,
this.sendNetworkQualityEvent.bind(this)
);
}
@@ -6511,12 +6582,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
@@ -7009,7 +7089,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`);
}
@@ -7022,6 +7102,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 +7114,7 @@ export default class Meeting extends StatelessWebexPlugin {
retriedWithTurnServer: this.addMediaData.retriedWithTurnServer,
isJoinWithMediaRetry: this.joinWithMediaRetryInfo.isRetry,
...reachabilityStats,
+ ...iceCandidateErrors,
iceCandidatesCount: this.iceCandidatesCount,
});
// @ts-ignore
@@ -8191,7 +8273,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/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/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/src/meetings/index.ts b/packages/@webex/plugin-meetings/src/meetings/index.ts
index 52481a6aee8..a9b79c52364 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);
}
/**
@@ -1080,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
@@ -1093,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
@@ -1104,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/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/src/reachability/index.ts b/packages/@webex/plugin-meetings/src/reachability/index.ts
index 7f67e44ff74..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
@@ -114,18 +116,50 @@ 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
+ * @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 {
- const {clusters, joinCookie} = await this.reachabilityRequest.getClusters(
- MeetingUtil.getIpVersion(this.webex)
- );
+ 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
+ this.webex.internal.device.ipNetworkDetector.detect();
+
+ const {clusters, joinCookie} = await this.getClusters();
// @ts-ignore
await this.webex.boundedStorage.put(
@@ -513,6 +547,17 @@ 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,
+ },
+ 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/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/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/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 4c5e7acea08..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,54 +1,53 @@
+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';
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',
};
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', () => {
@@ -61,7 +60,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,
@@ -80,7 +79,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 +90,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',
},
@@ -134,7 +138,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,
@@ -145,8 +155,9 @@ describe('createMediaConnection', () => {
receiveShare: true,
},
},
+ rtcMetrics,
turnServerInfo: {
- url: 'turn server url',
+ url: 'turns:turn-server-url:443?transport=tcp',
username: 'turn username',
password: 'turn password',
},
@@ -158,7 +169,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',
},
@@ -167,18 +183,42 @@ 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);
+
});
[
{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
.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,
@@ -198,7 +238,7 @@ describe('createMediaConnection', () => {
iceServers: [],
},
'meeting id'
- );
+ );
});
});
@@ -207,7 +247,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,
@@ -232,7 +272,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
@@ -244,7 +287,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/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/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 24e352c815f..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,6 +5,7 @@ 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 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';
@@ -305,7 +306,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(
@@ -329,6 +330,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);
@@ -373,7 +375,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', () => {
@@ -400,6 +402,37 @@ 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.equal(newMeeting.sessionCorrelationId, uuid1);
+ assert.deepEqual(newMeeting.callStateForMetrics, {
+ correlationId: uuid4,
+ sessionCorrelationId: uuid1,
+ joinTrigger: 'fake-join-trigger',
+ loginType: 'fake-login-type',
});
});
@@ -625,7 +658,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 +691,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 +725,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 +761,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 +814,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 +859,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 +895,6 @@ describe('plugin-meetings', () => {
reason: 'joinWithMedia failure',
});
-
// Behavioral metric is sent on both calls of joinWithMedia
assert.calledTwice(Metrics.sendBehavioralMetric);
assert.calledWith(
@@ -1068,12 +1134,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 +1408,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 +1421,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 +1576,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),
@@ -1559,6 +1618,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;
@@ -1577,13 +1640,10 @@ 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
- );
+ assert.calledWith(webex.internal.llm.on, 'online', meeting.handleLLMOnline);
});
[true, false].forEach((enableMultistream) => {
@@ -1906,7 +1966,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 +1982,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),
};
});
@@ -2376,9 +2443,7 @@ describe('plugin-meetings', () => {
Media.createMediaConnection,
false,
meeting.getMediaConnectionDebugId(),
- webex,
meeting.id,
- meeting.correlationId,
sinon.match({turnServerInfo: undefined})
);
assert.calledOnce(meeting.setMercuryListener);
@@ -2420,6 +2485,42 @@ describe('plugin-meetings', () => {
checkWorking({allowMediaInLobby: true});
});
+ it('should create rtcMetrics and pass them to Media.createMediaConnection()', async () => {
+ const setIntervalOriginal = window.setInterval;
+ window.setInterval = sinon.stub().returns(1);
+
+ // 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: {},
+ });
+
+ // check that rtcMetrics was passed to Media.createMediaConnection
+ assert.calledOnce(Media.createMediaConnection);
+ assert.calledWith(
+ Media.createMediaConnection,
+ true,
+ meeting.getMediaConnectionDebugId(),
+ meeting.id,
+ sinon.match.hasNested('rtcMetrics.webex', webex)
+ );
+
+ window.setInterval = setIntervalOriginal;
+ });
+
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';
@@ -2449,9 +2550,7 @@ describe('plugin-meetings', () => {
Media.createMediaConnection,
false,
meeting.getMediaConnectionDebugId(),
- webex,
meeting.id,
- meeting.correlationId,
sinon.match({
turnServerInfo: {
url: FAKE_TURN_URL,
@@ -2485,7 +2584,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 +2788,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));
@@ -2988,6 +3093,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: {},
@@ -3009,6 +3116,8 @@ describe('plugin-meetings', () => {
someReachabilityMetric1: 'some value1',
someReachabilityMetric2: 'some value2',
iceCandidatesCount: 3,
+ '701_error': 3,
+ '701_turn_host_lookup_received_error': 1,
}
);
@@ -3362,9 +3471,7 @@ describe('plugin-meetings', () => {
Media.createMediaConnection,
false,
meeting.getMediaConnectionDebugId(),
- webex,
meeting.id,
- meeting.correlationId,
sinon.match({
turnServerInfo: {
url: FAKE_TURN_URL,
@@ -3445,9 +3552,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 +3669,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 +3693,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 +3789,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 +3823,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 +4271,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});
@@ -4189,6 +4312,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();
@@ -4255,6 +4392,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}},
@@ -6589,14 +6734,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 +6823,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 +6834,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);
});
@@ -6809,33 +6955,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',
});
@@ -7482,9 +7631,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 +7657,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 +7722,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 +8432,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"', () => {
@@ -8374,6 +8547,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);
@@ -8385,6 +8561,8 @@ describe('plugin-meetings', () => {
{payload: test1}
);
assert.calledOnce(meeting.updateLLMConnection);
+ assert.calledOnceWithExactly(meeting.rtcMetrics.sendNextMetrics);
+
done();
});
@@ -9690,6 +9868,11 @@ describe('plugin-meetings', () => {
requiredDisplayHints: [],
requiredPolicies: [SELF_POLICY.SUPPORT_ANNOTATION],
},
+ {
+ actionName: 'canPollingAndQA',
+ requiredDisplayHints: [],
+ requiredPolicies: [SELF_POLICY.SUPPORT_POLLING_AND_QA],
+ },
],
({
actionName,
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/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;
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 f02c372960a..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';
@@ -71,6 +72,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 +89,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 +105,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/plugin-meetings/test/unit/spec/meetings/index.js b/packages/@webex/plugin-meetings/test/unit/spec/meetings/index.js
index 9775a52bb97..21629c5f398 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');
@@ -726,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',
@@ -742,11 +771,15 @@ describe('plugin-meetings', () => {
{},
correlationId,
true,
- callStateForMetrics
+ callStateForMetrics,
+ undefined,
+ undefined,
+ sessionCorrelationId
);
assert.calledOnceWithExactly(fakeMeeting.setCallStateForMetrics, {
...callStateForMetrics,
correlationId,
+ sessionCorrelationId,
});
});
@@ -787,13 +820,14 @@ describe('plugin-meetings', () => {
undefined,
meetingInfo,
'meetingLookupURL',
+ sessionCorrelationId
],
[
test1,
test2,
FAKE_USE_RANDOM_DELAY,
{},
- {correlationId},
+ {correlationId, sessionCorrelationId},
true,
meetingInfo,
'meetingLookupURL',
@@ -1692,6 +1726,7 @@ describe('plugin-meetings', () => {
const expectedMeetingData = {
correlationId: 'my-correlationId',
callStateForMetrics: {
+ sessionCorrelationId: '',
correlationId: 'my-correlationId',
joinTrigger: 'my-join-trigger',
loginType: 'my-login-type',
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/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/@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/index.ts b/packages/@webex/plugin-meetings/test/unit/spec/reachability/index.ts
index 8be4ce92bb2..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,10 +4,9 @@ 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';
+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 +144,6 @@ describe('isAnyPublicClusterReachable', () => {
});
});
-
describe('isWebexMediaBackendUnreachable', () => {
let webex;
@@ -486,6 +484,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 = {};
@@ -498,6 +506,11 @@ describe('gatherReachability', () => {
mockClusterReachabilityInstances[id] = mockInstance;
return mockInstance;
});
+
+ webex.config.meetings.experimental = {
+ enableTcpReachability: false,
+ enableTlsReachability: false,
+ };
});
afterEach(() => {
@@ -1035,6 +1048,16 @@ describe('gatherReachability', () => {
enableTlsReachability: true,
};
+ // 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 = {
done: 0,
firstResultAvailable: {
@@ -1064,7 +1087,7 @@ describe('gatherReachability', () => {
reachability.reachabilityRequest.getClusters = sinon.stub().returns(mockGetClustersResult);
- const resultPromise = reachability.gatherReachability();
+ const resultPromise = reachability.gatherReachability('test');
await testUtils.flushPromises();
@@ -1119,11 +1142,122 @@ describe('gatherReachability', () => {
assert.calledWith(
Metrics.sendBehavioralMetric,
'js_sdk_reachability_completed',
- expectedMetrics
+ expectedMetricsFull
);
})
);
+ 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,
+ 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('test');
+
+ 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,
+ trigger: 'test',
+ });
+ });
+
it('keeps updating reachability results after the 3s public cloud timeout expires', async () => {
webex.config.meetings.experimental = {
enableTcpReachability: true,
@@ -1152,7 +1286,7 @@ describe('gatherReachability', () => {
reachability.reachabilityRequest.getClusters = sinon.stub().returns(mockGetClustersResult);
- const resultPromise = reachability.gatherReachability();
+ const resultPromise = reachability.gatherReachability('test');
await testUtils.flushPromises();
@@ -1245,7 +1379,7 @@ describe('gatherReachability', () => {
reachability.reachabilityRequest.getClusters = sinon.stub().returns(mockGetClustersResult);
- const resultPromise = reachability.gatherReachability();
+ const resultPromise = reachability.gatherReachability('test');
await testUtils.flushPromises();
@@ -1286,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);
@@ -1304,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);
@@ -1339,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;
@@ -1385,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;
@@ -1419,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;
@@ -1454,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;
@@ -1466,6 +1600,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('test');
+
+ 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('test');
+
+ await simulateTimeout();
+
+ await promise;
+
+ assert.equal(getClustersCallCount, 2);
+
+ assert.neverCalledWith(clusterReachabilityCtorStub);
+ });
});
describe('getReachabilityResults', () => {
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';
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);
+ });
});
});
});
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: {
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;
diff --git a/packages/calling/package.json b/packages/calling/package.json
index d149f8f9b62..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.9.1",
+ "@webex/internal-media-core": "2.11.3",
"@webex/media-helpers": "workspace:*",
"async-mutex": "0.4.0",
"buffer": "6.0.3",
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/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/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();
}
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/calling/call.test.ts b/packages/calling/src/CallingClient/calling/call.test.ts
index ce43512f806..c95fe05c9f4 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', () => {
@@ -1980,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 9fc68949cba..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(),
},
}
);
@@ -1904,6 +1944,7 @@ export class Call extends Eventing implements ICall {
sdpMunging: {
convertPort9to0: true,
addContentSlides: false,
+ copyClineToSessionLevel: true,
},
},
{
@@ -2375,24 +2416,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 +2436,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 = {
@@ -2815,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,
+ });
+ }
}
/**
diff --git a/packages/calling/src/CallingClient/constants.ts b/packages/calling/src/CallingClient/constants.ts
index b395a335a5d..c3b8cc87fc4 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;
@@ -111,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();
diff --git a/packages/calling/src/Contacts/ContactsClient.test.ts b/packages/calling/src/Contacts/ContactsClient.test.ts
index ee1068f02eb..b882d31979e 100644
--- a/packages/calling/src/Contacts/ContactsClient.test.ts
+++ b/packages/calling/src/Contacts/ContactsClient.test.ts
@@ -1,9 +1,16 @@
-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,
+ WEBEX_API_BTS,
+} from '../common/constants';
import log from '../Logger';
import {
CONTACTS_FILE,
@@ -24,12 +31,12 @@ import {
mockContactResponseBodyOne,
mockCountry,
mockDisplayNameOne,
- mockDSSResponse,
mockEmail,
mockFirstName,
mockLastName,
mockNumber1,
mockNumber2,
+ mockSCIMListResponse,
mockSipAddress,
mockState,
mockStreet,
@@ -42,6 +49,8 @@ import {
mockContactGroupListOne,
mockContactGroupListTwo,
mockAvatarURL,
+ mockSCIMMinListResponse,
+ mockContactMinimum,
} from './contactFixtures';
describe('ContactClient Tests', () => {
@@ -51,6 +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_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');
@@ -90,6 +100,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 +130,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 +216,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 +228,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 +604,8 @@ describe('ContactClient Tests', () => {
webex.request
.mockResolvedValueOnce(successResponsePayloadGroup)
- .mockResolvedValueOnce(successResponsePayload);
+ .mockResolvedValueOnce(successResponsePayload)
+ .mockResolvedValueOnce(mockSCIMListResponse);
webex.internal.encryption.encryptText.mockResolvedValueOnce('Encrypted group name');
@@ -581,14 +623,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 +651,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 () => {
@@ -677,4 +727,81 @@ 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,
+ 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,
+ },
+ ]);
+ });
+
+ 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 d701f7d8f0e..0245c7461e3 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,10 +35,7 @@ import {
GroupType,
} from './types';
-import {serviceErrorCodeHandler} from '../common/Utils';
-import Logger from '../Logger';
-import ExtendedError from '../Errors/catalog/ExtendedError';
-import {ERROR_TYPE} from '../Errors/types';
+import {scimQuery, serviceErrorCodeHandler} from '../common/Utils';
/**
* `ContactsClient` module is designed to offer a set of APIs for retrieving and updating contacts and groups from the contacts-service.
@@ -66,7 +71,6 @@ export class ContactsClient implements IContacts {
}
this.webex = this.sdkConnector.getWebex();
- this.webex.internal.dss.register();
this.encryptionKeyUrl = '';
this.groups = undefined;
@@ -253,75 +257,73 @@ 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;
-
- if (addedPhoneNumbers) {
- const decryptedPhoneNumbers = await this.decryptContactDetail(
- encryptionKeyUrl,
- addedPhoneNumbers
- );
+ inputList: SCIMListResponse
+ ): Contact[] | null {
+ const loggerContext = {
+ file: CONTACTS_FILE,
+ method: 'resolveCloudContacts',
+ };
+ const finalContactList: Contact[] = [];
+ const resolvedList: string[] = [];
- decryptedPhoneNumbers.forEach((number) => phoneNumbers.push(number));
- }
+ try {
+ inputList.Resources.forEach((item) => {
+ resolvedList.push(item.id);
+ });
- const addedSipAddresses = contactsDataMap[contactId].sipAddresses;
+ Object.values(contactsDataMap).forEach((item) => {
+ const isResolved = resolvedList.some((listItem) => listItem === item.contactId);
+ if (!isResolved) {
+ finalContactList.push({...item, resolved: false});
+ }
+ });
- if (addedSipAddresses) {
- const decryptedSipAddresses = await this.decryptContactDetail(
+ 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]) {
+ 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[inputList.Resources[n].id];
+
+ const cloudContact = {
+ avatarUrlDomain,
+ avatarURL,
+ contactId: inputList.Resources[n].id,
+ contactType,
+ department,
+ displayName,
+ emails,
encryptionKeyUrl,
- addedSipAddresses
- );
-
- decryptedSipAddresses.forEach((address) => sipAddresses.push(address));
+ firstName,
+ groups,
+ lastName,
+ manager,
+ ownerId,
+ phoneNumbers,
+ sipAddresses,
+ resolved: true,
+ };
+
+ 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), {});
+ log.warn('Error occurred while parsing resolved contacts', loggerContext);
return null;
}
+
+ return finalContactList;
}
/**
@@ -334,7 +336,7 @@ export class ContactsClient implements IContacts {
};
const contactList: Contact[] = [];
- const contactsDataMap: ContactIdContactInfo = {};
+ const cloudContactsMap: ContactIdContactInfo = {};
try {
const response = await this.webex.request({
@@ -351,19 +353,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 +709,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..4295a607411 100644
--- a/packages/calling/src/Contacts/contactFixtures.ts
+++ b/packages/calling/src/Contacts/contactFixtures.ts
@@ -272,39 +272,213 @@ 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',
+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',
+ '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,
+ },
+ ],
+ 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',
},
- jobTitle: 'Software Engineer',
- lastName: 'Nagakawa',
- modified: '2023-03-03T13:37:03.196Z',
- nickName: 'Emily',
- userName: 'emikawa2@cisco.com',
},
- displayName: 'Emily Nagakawa',
- emails: [{value: 'emikawa2@cisco.com'}],
- entityProviderType: 'CI_USER',
- identity: '801bb994-343b-4f6b-97ae-d13c91d4b877',
- orgId: '1eb65fdf-9643-417f-9974-ad72cae0e10f',
+ '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',
+ },
+};
+
+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: 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 = {
diff --git a/packages/calling/src/Contacts/types.ts b/packages/calling/src/Contacts/types.ts
index 2c9d2e16fbf..8a0b01b3568 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.
*/
@@ -37,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`.
*/
@@ -49,11 +41,11 @@ 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.
*/
- 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,11 +89,11 @@ 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.
+ * This field indicates whether the contact was resolved successfully.
*/
- title?: string;
+ resolved: boolean;
};
export enum GroupType {
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/Metrics/index.ts b/packages/calling/src/Metrics/index.ts
index 2f60c64bb57..6e2ef686c08 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:
+ typeof process !== 'undefined' && process.env.CALLING_SDK_VERSION
+ ? process.env.CALLING_SDK_VERSION
+ : VERSION,
},
type,
};
@@ -303,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,
};
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..06b70c2c098 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,
@@ -20,15 +21,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 +47,13 @@ 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,
+ WEBEX_API_BTS,
+} from './constants';
import {CALL_EVENT_KEYS} from '../Events/types';
const mockSubmitRegistrationMetric = jest.fn();
@@ -915,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;
@@ -1051,6 +1050,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 d2a3d5b4763..4c2d8441dfe 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,
@@ -88,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,
@@ -113,9 +114,13 @@ import {
NATIVE_WEBEX_TEAMS_CALLING,
NATIVE_SIP_CALL_TO_UCM,
BW_XSI_ENDPOINT_VERSION,
+ IDENTITY_ENDPOINT_RESOURCE,
+ SCIM_ENDPOINT_RESOURCE,
+ SCIM_USER_FILTER,
+ WEBEX_API_PROD,
+ WEBEX_API_BTS,
} 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';
@@ -690,6 +695,7 @@ export async function serviceErrorCodeHandler(
| CallSettingResponse
| ContactResponse
| UpdateMissedCallsResponse
+ | UCMLinesResponse
> {
const errorCode = Number(err.statusCode);
const failureMessage = 'FAILURE';
@@ -1177,7 +1183,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',
@@ -1185,7 +1191,10 @@ 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({
@@ -1195,7 +1204,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 +1219,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,
@@ -1236,12 +1244,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/constants.ts b/packages/calling/src/common/constants.ts
index 92d37654651..a71f3de888a 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,9 @@ 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';
+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`;
diff --git a/packages/calling/src/common/testUtil.ts b/packages/calling/src/common/testUtil.ts
index 8dd11ab0909..dff60f0e25f 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(),
},
@@ -265,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 5a592b6b959..02605e6a1f0 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;
diff --git a/yarn.lock b/yarn.lock
index 2527d5aca6e..579647c5faa 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
@@ -5931,11 +5931,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
@@ -7493,7 +7493,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.11.3
"@webex/media-helpers": "workspace:*"
async-mutex: 0.4.0
buffer: 6.0.3
@@ -7692,17 +7692,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"
+"@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: "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
+ 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
@@ -7784,21 +7784,22 @@ __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.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
+ "@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: 358a8b43ae55d6272fa07ca6e075cceb07a29d464c7e494ec130cd918848595eecd8e52ff12407790051af92b5e7d9612fdb4d7c6cf7a11231ce3bb390bd10f3
+ checksum: b95c917890c98ded1346d093656a8c54cb4ae7c0a8a93ccccaf39e72913f3c2c8a53829d257eb5ac74a087ee2c4583c37ed3752acfbab179e6533761eb9a5ed0
languageName: node
linkType: hard
@@ -8199,7 +8200,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:*"
@@ -8465,10 +8466,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
@@ -8557,13 +8558,13 @@ __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.11.3
"@webex/jest-config-legacy": "workspace:*"
"@webex/legacy-tools": "workspace:*"
"@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
@@ -8793,7 +8794,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.11.3
"@webex/internal-plugin-conversation": "workspace:*"
"@webex/internal-plugin-device": "workspace:*"
"@webex/internal-plugin-llm": "workspace:*"
@@ -9085,13 +9086,13 @@ __metadata:
languageName: unknown
linkType: soft
-"@webex/rtcstats@npm:^1.3.2":
- version: 1.3.3
- resolution: "@webex/rtcstats@npm:1.3.3"
+"@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: 7ac73b2f6bf8bf44bfaff7a5e904b175509632b80bf4cbdb09c5570c940d7e02445ba0c95f713067b03770eee49162d964b98a8d1e05f42e3f80b5bfd503352c
+ checksum: d7af2b4be63a146de7eca11bbfa6478e3b4504c2d9df971315ca9ac07026c83813742ef9dcd019cbb38a9a74aebcd569a03436e1c1c35e087efcf9e682ddf8ff
languageName: node
linkType: hard
@@ -9496,41 +9497,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.18.0":
- version: 2.18.0
- resolution: "@webex/web-media-effects@npm:2.18.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": "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
+ 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
@@ -9538,7 +9539,7 @@ __metadata:
typed-emitter: ^1.4.0
uuid: ^9.0.1
worker-timers: ^8.0.2
- checksum: 97d51ff86bcb7880f18b3615dcd745df826758bb93cfd1c076fd60357ff57c2dd00b7ba4a609b0c71fa5b7caf39f5443c0fc371f1ed4486fa2aafe42a4b50d0d
+ checksum: 04c100b8eb01fbe05cc5ee1b1898c54f2ef537a9e367c7693767f6fb1df1d085c00ba91ee42d7bb9e9df55baeccd43d4ea979dcd486223a4dc3a82c61769494f
languageName: node
linkType: hard
@@ -9632,18 +9633,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
@@ -17050,13 +17051,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
@@ -31225,7 +31226,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
@@ -33388,37 +33389,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