Skip to content

Commit

Permalink
feat(calling): add rtcMetrics in the call (#3906)
Browse files Browse the repository at this point in the history
  • Loading branch information
rarajes2 authored Oct 18, 2024
1 parent fc5e98d commit 94ab1cc
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 21 deletions.
16 changes: 16 additions & 0 deletions packages/@webex/internal-plugin-metrics/src/metrics.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,19 @@ export type PreComputedLatencies =
| 'internal.call.init.join.req'
| 'internal.other.app.api.time'
| 'internal.api.fetch.intelligence.models';

export interface IdType {
meetingId?: string;
callId?: string;
}

export interface IMetricsAttributes {
type: string;
version: string;
userId: string;
correlationId: string;
connectionId: string;
data: any[];
meetingId?: string;
callId?: string;
}
47 changes: 33 additions & 14 deletions packages/@webex/internal-plugin-metrics/src/rtcMetrics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import uuid from 'uuid';
import * as CallDiagnosticUtils from '../call-diagnostic/call-diagnostic-metrics.util';
import RTC_METRICS from './constants';
import {IdType, IMetricsAttributes} from '../metrics.types';

const parseJsonPayload = (payload: any[]): any | null => {
try {
Expand All @@ -28,7 +29,9 @@ export default class RtcMetrics {

webex: any;

meetingId: string;
meetingId?: string;

callId?: string;

correlationId: string;

Expand All @@ -40,18 +43,29 @@ export default class RtcMetrics {
* Initialize the interval.
*
* @param {object} webex - The main `webex` object.
* @param {string} meetingId - The meeting id.
* @param {IdType} Ids - Meeting or Calling id.
* @param {string} correlationId - The correlation id.
*/
constructor(webex, meetingId, correlationId) {
constructor(webex, {meetingId, callId}: IdType, correlationId) {
// `window` is used to prevent typescript from returning a NodeJS.Timer.
this.intervalId = window.setInterval(this.sendMetricsInQueue.bind(this), 30 * 1000);
this.meetingId = meetingId;
this.callId = callId;
this.webex = webex;
this.correlationId = correlationId;
this.resetConnection();
}

/**
* Updates the call identifier with the provided value.
*
* @param {string} callId - The new call identifier to set.
* @returns {void}
*/
public updateCallId(callId: string) {
this.callId = callId;
}

/**
* Check to see if the metrics queue has any items.
*
Expand Down Expand Up @@ -160,6 +174,21 @@ export default class RtcMetrics {
* @returns {void}
*/
private sendMetrics() {
const metricsAttributes: IMetricsAttributes = {
type: 'webrtc',
version: '1.1.0',
userId: this.webex.internal.device.userId,
correlationId: this.correlationId,
connectionId: this.connectionId,
data: this.metricsQueue,
};

if (this.meetingId) {
metricsAttributes.meetingId = this.meetingId;
} else if (this.callId) {
metricsAttributes.callId = this.callId;
}

this.webex.request({
method: 'POST',
service: 'unifiedTelemetry',
Expand All @@ -169,17 +198,7 @@ export default class RtcMetrics {
appId: RTC_METRICS.APP_ID,
},
body: {
metrics: [
{
type: 'webrtc',
version: '1.1.0',
userId: this.webex.internal.device.userId,
meetingId: this.meetingId,
correlationId: this.correlationId,
connectionId: this.connectionId,
data: this.metricsQueue,
},
],
metrics: [metricsAttributes],
},
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('RtcMetrics', () => {
clock = sinon.useFakeTimers();
window.setInterval = setInterval;
webex = new MockWebex();
metrics = new RtcMetrics(webex, 'mock-meeting-id', 'mock-correlation-id');
metrics = new RtcMetrics(webex, {meetingId: 'mock-meeting-id'}, 'mock-correlation-id');
anonymizeIpSpy = sandbox.spy(metrics, 'anonymizeIp');
});

Expand Down Expand Up @@ -152,4 +152,45 @@ describe('RtcMetrics', () => {
metrics.addMetrics({ name: 'stats-report', payload: [STATS_WITH_IP] });
assert.callCount(webex.request, 3);
});

describe('RtcMetrics - callId', () => {
let metrics: RtcMetrics;
let webex: MockWebex;
let clock;
let sandbox;

beforeEach(() => {
clock = sinon.useFakeTimers();
window.setInterval = setInterval;
webex = new MockWebex();
metrics = new RtcMetrics(webex, {callId: 'mock-call-id'}, 'mock-correlation-id');
sandbox = sinon.createSandbox();
});

afterEach(() => {
sandbox.restore();
});

it('sendMetrics should send a webex request with callId', () => {
assert.notCalled(webex.request);

metrics.addMetrics(FAKE_METRICS_ITEM);
(metrics as any).sendMetrics();

assert.callCount(webex.request, 1);
assert.calledWithMatch(webex.request, sinon.match.has('headers', {
type: 'webrtcMedia',
appId: RTC_METRICS.APP_ID,
}));
assert.calledWithMatch(webex.request, sinon.match.hasNested('body.metrics[0].data[0].payload', FAKE_METRICS_ITEM.payload));
assert.calledWithMatch(webex.request, sinon.match.hasNested('body.metrics[0].callId', 'mock-call-id'));
assert.calledWithMatch(webex.request, sinon.match.hasNested('body.metrics[0].correlationId', 'mock-correlation-id'));
});

it('should update the callId correctly', () => {
const newCallId = 'new-call-id';
metrics.updateCallId(newCallId);
assert.strictEqual(metrics.callId, newCallId);
});
});
});
2 changes: 1 addition & 1 deletion packages/@webex/plugin-meetings/src/meeting/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6376,7 +6376,7 @@ export default class Meeting extends StatelessWebexPlugin {
private async createMediaConnection(turnServerInfo, bundlePolicy?: BundlePolicy) {
this.rtcMetrics = this.isMultistream
? // @ts-ignore
new RtcMetrics(this.webex, this.id, this.correlationId)
new RtcMetrics(this.webex, {meetingId: this.id}, this.correlationId)
: undefined;

const mc = Media.createMediaConnection(
Expand Down
15 changes: 12 additions & 3 deletions packages/calling/src/CallingClient/calling/call.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,10 @@ describe('Call Tests', () => {
expect(mockInternalMediaCoreModule.RoapMediaConnection).toBeCalledOnceWith(
roapMediaConnectionConfig,
roapMediaConnectionOptions,
expect.any(String)
expect.any(String),
expect.any(Function),
expect.any(Function),
expect.any(Function)
);
expect(call['mediaStateMachine'].state.value).toBe('S_SEND_ROAP_OFFER');

Expand Down Expand Up @@ -400,7 +403,10 @@ describe('Call Tests', () => {
expect(mockInternalMediaCoreModule.RoapMediaConnection).toBeCalledOnceWith(
roapMediaConnectionConfig,
roapMediaConnectionOptions,
expect.any(String)
expect.any(String),
expect.any(Function),
expect.any(Function),
expect.any(Function)
);
expect(call['callStateMachine'].state.value).toBe('S_IDLE');
expect(warnSpy).toBeCalledOnceWith(`Call cannot be answered because the state is : S_IDLE`, {
Expand Down Expand Up @@ -454,7 +460,10 @@ describe('Call Tests', () => {
expect(mockInternalMediaCoreModule.RoapMediaConnection).toBeCalledOnceWith(
roapMediaConnectionConfig,
roapMediaConnectionOptions,
expect.any(String)
expect.any(String),
expect.any(Function),
expect.any(Function),
expect.any(Function)
);
expect(call['mediaStateMachine'].state.value).toBe('S_SEND_ROAP_OFFER');

Expand Down
46 changes: 44 additions & 2 deletions packages/calling/src/CallingClient/calling/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ import {
import {createMachine, interpret} from 'xstate';
import {v4 as uuid} from 'uuid';
import {EffectEvent, TrackEffect} from '@webex/web-media-effects';
import {RtcMetrics} from '@webex/internal-plugin-metrics';
import ExtendedError from '../../Errors/catalog/ExtendedError';
import {ERROR_LAYER, ERROR_TYPE, ErrorContext} from '../../Errors/types';
import {handleCallErrors, parseMediaQualityStatistics} from '../../common/Utils';
import {
handleCallErrors,
parseMediaQualityStatistics,
serviceErrorCodeHandler,
} from '../../common/Utils';
import {
ALLOWED_SERVICES,
CallDetails,
Expand Down Expand Up @@ -158,6 +164,8 @@ export class Call extends Eventing<CallEventTypes> implements ICall {

private localAudioStream?: LocalMicrophoneStream;

private rtcMetrics: RtcMetrics;

/**
* Getter to check if the call is muted or not.
*
Expand Down Expand Up @@ -244,6 +252,8 @@ export class Call extends Eventing<CallEventTypes> implements ICall {
this.remoteRoapMessage = null;
this.disconnectReason = {code: DisconnectCode.NORMAL, cause: DisconnectCause.NORMAL};

this.rtcMetrics = new RtcMetrics(this.webex, {callId: this.callId}, this.correlationId);

const callMachine = createMachine(
{
schema: {
Expand Down Expand Up @@ -1927,6 +1937,33 @@ export class Call extends Eventing<CallEventTypes> implements ICall {
}
}

/**
* Media failed, so collect a stats report from webrtc
* send a webrtc telemetry dump to the configured server using the internal media core check metrics configured callback
* @param {String} callFrom - the function calling this function, optional.
* @returns {Promise<void>}
*/
private forceSendStatsReport = async ({callFrom}: {callFrom?: string}) => {
const loggerContext = {
file: CALL_FILE,
method: this.forceSendStatsReport.name,
};

try {
await this.mediaConnection.forceRtcMetricsSend();
log.info(`Successfully uploaded available webrtc telemetry statistics`, loggerContext);
log.info(`callFrom: ${callFrom}`, loggerContext);
} catch (error) {
const errorInfo = error as WebexRequestPayload;
const errorStatus = serviceErrorCodeHandler(errorInfo, loggerContext);
const errorLog = new Error(
`Failed to upload webrtc telemetry statistics. ${errorStatus}`
) as ExtendedError;

log.error(errorLog, loggerContext);
}
};

/* istanbul ignore next */
/**
* Initialize Media Connection.
Expand Down Expand Up @@ -1955,7 +1992,10 @@ export class Call extends Eventing<CallEventTypes> implements ICall {
screenShareVideo: 'inactive',
},
},
debugId || `WebexCallSDK-${this.correlationId}`
debugId || `WebexCallSDK-${this.correlationId}`,
(data) => this.rtcMetrics.addMetrics(data),
() => this.rtcMetrics.closeMetrics(),
() => this.rtcMetrics.sendMetricsInQueue()
);

this.mediaConnection = mediaConnection;
Expand Down Expand Up @@ -1999,6 +2039,8 @@ export class Call extends Eventing<CallEventTypes> implements ICall {
*/
public setCallId = (callId: CallId) => {
this.callId = callId;
this.rtcMetrics.updateCallId(callId);

log.info(`Setting callId : ${this.callId} for correlationId: ${this.correlationId}`, {
file: CALL_FILE,
method: this.setCallId.name,
Expand Down

0 comments on commit 94ab1cc

Please sign in to comment.