Skip to content

Commit 32ecb1e

Browse files
authored
feat(plugin-meetings): mark media errors as handled (webex#4253)
1 parent d717051 commit 32ecb1e

File tree

6 files changed

+130
-55
lines changed

6 files changed

+130
-55
lines changed

packages/@webex/plugin-meetings/src/meeting/index.ts

Lines changed: 55 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5856,16 +5856,7 @@ export default class Meeting extends StatelessWebexPlugin {
58565856
this
58575857
);
58585858

5859-
const proxyError = new Proxy(error, {
5860-
// eslint-disable-next-line require-jsdoc
5861-
get(target, prop) {
5862-
if (prop === 'handledBySdk') {
5863-
return true;
5864-
}
5865-
5866-
return Reflect.get(target, prop);
5867-
},
5868-
});
5859+
const proxyError = MeetingUtil.markErrorAsHandledBySdk(error);
58695860

58705861
joinFailed(proxyError);
58715862

@@ -6254,10 +6245,10 @@ export default class Meeting extends StatelessWebexPlugin {
62546245
/**
62556246
* Handles ROAP_FAILURE event from the webrtc media connection
62566247
*
6257-
* @param {Error} error
6248+
* @param {Error} roapError
62586249
* @returns {void}
62596250
*/
6260-
handleRoapFailure = (error) => {
6251+
handleRoapFailure = (roapError) => {
62616252
// eslint-disable-next-line @typescript-eslint/no-shadow
62626253
const sendBehavioralMetric = (metricName, error, correlationId) => {
62636254
const data = {
@@ -6273,6 +6264,8 @@ export default class Meeting extends StatelessWebexPlugin {
62736264
Metrics.sendBehavioralMetric(metricName, data, metadata);
62746265
};
62756266

6267+
const error = MeetingUtil.markErrorAsHandledBySdk(roapError);
6268+
62766269
if (error instanceof Errors.SdpOfferCreationError) {
62776270
sendBehavioralMetric(BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE, error, this.correlationId);
62786271

@@ -6311,7 +6304,7 @@ export default class Meeting extends StatelessWebexPlugin {
63116304
clearTimeout(this.sdpResponseTimer);
63126305
this.sdpResponseTimer = undefined;
63136306

6314-
this.deferSDPAnswer.reject();
6307+
this.deferSDPAnswer.reject(error);
63156308
}
63166309
} else if (error instanceof Errors.SdpError) {
63176310
// this covers also the case of Errors.IceGatheringError which extends Errors.SdpError
@@ -6466,7 +6459,9 @@ export default class Meeting extends StatelessWebexPlugin {
64666459
{
64676460
logText: `${LOG_HEADER} Roap Offer`,
64686461
}
6469-
).catch((error) => {
6462+
).catch((originalError) => {
6463+
const error = MeetingUtil.markErrorAsHandledBySdk(originalError);
6464+
64706465
const multistreamNotSupported = error instanceof MultistreamNotSupportedError;
64716466

64726467
// @ts-ignore
@@ -7114,45 +7109,54 @@ export default class Meeting extends StatelessWebexPlugin {
71147109
} catch (error) {
71157110
const {iceConnected} = error;
71167111

7112+
let handledBySdk = false;
7113+
71177114
if (!this.hasMediaConnectionConnectedAtLeastOnce) {
7115+
const caError =
7116+
// @ts-ignore
7117+
this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode({
7118+
clientErrorCode: CallDiagnosticUtils.generateClientErrorCodeForIceFailure({
7119+
signalingState:
7120+
this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
7121+
?.signalingState ||
7122+
this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc?.signalingState ||
7123+
'unknown',
7124+
iceConnected,
7125+
turnServerUsed: this.turnServerUsed,
7126+
unreachable:
7127+
// @ts-ignore
7128+
await this.webex.meetings.reachability
7129+
.isWebexMediaBackendUnreachable()
7130+
.catch(() => false),
7131+
}),
7132+
});
7133+
71187134
// Only send CA event for join flow if we haven't successfully connected media yet
71197135
// @ts-ignore
71207136
this.webex.internal.newMetrics.submitClientEvent({
71217137
name: 'client.ice.end',
71227138
payload: {
71237139
canProceed: !this.turnServerUsed, // If we haven't done turn tls retry yet we will proceed with join attempt
71247140
icePhase: this.addMediaData.icePhaseCallback(),
7125-
errors: [
7126-
// @ts-ignore
7127-
this.webex.internal.newMetrics.callDiagnosticMetrics.getErrorPayloadForClientErrorCode(
7128-
{
7129-
clientErrorCode: CallDiagnosticUtils.generateClientErrorCodeForIceFailure({
7130-
signalingState:
7131-
this.mediaProperties.webrtcMediaConnection?.multistreamConnection?.pc?.pc
7132-
?.signalingState ||
7133-
this.mediaProperties.webrtcMediaConnection?.mediaConnection?.pc
7134-
?.signalingState ||
7135-
'unknown',
7136-
iceConnected,
7137-
turnServerUsed: this.turnServerUsed,
7138-
unreachable:
7139-
// @ts-ignore
7140-
await this.webex.meetings.reachability
7141-
.isWebexMediaBackendUnreachable()
7142-
.catch(() => false),
7143-
}),
7144-
}
7145-
),
7146-
],
7141+
errors: [caError],
71477142
},
71487143
options: {
71497144
meetingId: this.id,
71507145
},
71517146
});
7147+
7148+
handledBySdk = true;
71527149
}
7153-
throw new Error(
7150+
7151+
let timedOutError = new Error(
71547152
`Timed out waiting for media connection to be connected, correlationId=${this.correlationId}`
71557153
);
7154+
7155+
if (handledBySdk) {
7156+
timedOutError = MeetingUtil.markErrorAsHandledBySdk(timedOutError);
7157+
}
7158+
7159+
throw timedOutError;
71567160
}
71577161
}
71587162

@@ -7213,6 +7217,11 @@ export default class Meeting extends StatelessWebexPlugin {
72137217
ROAP_OFFER_ANSWER_EXCHANGE_TIMEOUT / 1000
72147218
} seconds`
72157219
);
7220+
7221+
const timeoutError = new Error('Timeout waiting for SDP answer');
7222+
7223+
const timeoutErrorProxy = MeetingUtil.markErrorAsHandledBySdk(timeoutError);
7224+
72167225
// @ts-ignore
72177226
this.webex.internal.newMetrics.submitClientEvent({
72187227
name: 'client.media-engine.remote-sdp-received',
@@ -7225,7 +7234,7 @@ export default class Meeting extends StatelessWebexPlugin {
72257234
}),
72267235
],
72277236
},
7228-
options: {meetingId: this.id, rawError: new Error('Timeout waiting for SDP answer')},
7237+
options: {meetingId: this.id, rawError: timeoutErrorProxy},
72297238
});
72307239

72317240
deferSDPAnswer.reject(new Error('Timed out waiting for REMOTE SDP ANSWER'));
@@ -7333,7 +7342,14 @@ export default class Meeting extends StatelessWebexPlugin {
73337342
error
73347343
);
73357344

7336-
throw new AddMediaFailed();
7345+
let addMediaFailedError = new AddMediaFailed();
7346+
7347+
// @ts-ignore - handledBySdk is added by a proxy
7348+
if (error.handledBySdk) {
7349+
addMediaFailedError = MeetingUtil.markErrorAsHandledBySdk(addMediaFailedError);
7350+
}
7351+
7352+
throw addMediaFailedError;
73377353
}
73387354
}
73397355

packages/@webex/plugin-meetings/src/meeting/locusMediaRequest.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {WebexPlugin} from '@webex/webex-core';
66
import {MEDIA, HTTP_VERBS, ROAP} from '../constants';
77
import LoggerProxy from '../common/logs/logger-proxy';
88
import {ClientMediaPreferences} from '../reachability/reachability.types';
9+
import MeetingUtil from './util';
910

1011
export type MediaRequestType = 'RoapMessage' | 'LocalMute';
1112
export type RequestResult = any;
@@ -263,7 +264,9 @@ export class LocusMediaRequest extends WebexPlugin {
263264

264265
return result;
265266
})
266-
.catch((e) => {
267+
.catch((error) => {
268+
let e = error;
269+
267270
if (
268271
isRequestAffectingConfluenceState(request) &&
269272
this.confluenceState === 'creation in progress'
@@ -272,6 +275,8 @@ export class LocusMediaRequest extends WebexPlugin {
272275
}
273276

274277
if (request.type === 'RoapMessage') {
278+
e = MeetingUtil.markErrorAsHandledBySdk(e);
279+
275280
// @ts-ignore
276281
this.webex.internal.newMetrics.submitClientEvent({
277282
name: 'client.locus.media.response',

packages/@webex/plugin-meetings/src/meeting/util.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,24 @@ const MeetingUtil = {
812812
},
813813
];
814814
},
815+
816+
/**
817+
* Creates a proxy object to mark an error as handled by the SDK.
818+
* @param {Error} error original error
819+
* @returns {Proxy} proxy object with handledBySdk property
820+
*/
821+
markErrorAsHandledBySdk: (error) => {
822+
return new Proxy(error, {
823+
// eslint-disable-next-line require-jsdoc
824+
get(target, prop) {
825+
if (prop === 'handledBySdk') {
826+
return true;
827+
}
828+
829+
return Reflect.get(target, prop);
830+
},
831+
});
832+
},
815833
};
816834

817835
export default MeetingUtil;

packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,13 +1011,19 @@ describe('plugin-meetings', () => {
10111011
.stub()
10121012
.returns(fakeClientError);
10131013

1014-
// call joinWithMedia() - it should fail
1015-
await assert.isRejected(
1016-
meeting.joinWithMedia({
1014+
const promise = meeting.joinWithMedia({
10171015
joinOptions,
10181016
mediaOptions,
10191017
})
1020-
);
1018+
1019+
// call joinWithMedia() - it should fail
1020+
await assert.isRejected(promise);
1021+
1022+
const rejectedError = await promise.catch((error) => error);
1023+
1024+
// Since the SDK has sent the CA events, we need to mark this error as handled
1025+
// so the client doesn't try and send CA events again
1026+
assert.isTrue(rejectedError.handledBySdk);
10211027

10221028
// check the right CA events have been sent:
10231029
// calls at index 0 and 2 to submitClientEvent are for "client.media.capabilities" which we don't care about in this test
@@ -8810,14 +8816,21 @@ describe('plugin-meetings', () => {
88108816
clock.restore();
88118817
});
88128818

8813-
const checkMetricSent = (event, error) => {
8819+
const checkMetricSent = (event, error, expectedErrorCode) => {
88148820
assert.calledOnce(webex.internal.newMetrics.submitClientEvent);
8815-
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
8821+
assert.deepEqual(webex.internal.newMetrics.submitClientEvent.getCall(0).args[0], {
88168822
name: event,
88178823
payload: {
88188824
canProceed: false,
88198825
},
8820-
options: {rawError: error, meetingId: meeting.id},
8826+
options: {
8827+
rawError: {
8828+
...(error.cause ? {cause: {name: error.cause.name}} : {cause: undefined}),
8829+
code: expectedErrorCode,
8830+
name: error.name,
8831+
},
8832+
meetingId: meeting.id,
8833+
},
88218834
});
88228835
};
88238836

@@ -8851,7 +8864,7 @@ describe('plugin-meetings', () => {
88518864

88528865
eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
88538866

8854-
checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
8867+
checkMetricSent('client.media-engine.local-sdp-generated', fakeError, 30005);
88558868
checkBehavioralMetricSent(
88568869
BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE,
88578870
Errors.ErrorCode.SdpOfferCreationError,
@@ -8868,7 +8881,7 @@ describe('plugin-meetings', () => {
88688881

88698882
eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
88708883

8871-
checkMetricSent('client.media-engine.remote-sdp-received', fakeError);
8884+
checkMetricSent('client.media-engine.remote-sdp-received', fakeError, 30006);
88728885
checkBehavioralMetricSent(
88738886
BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE,
88748887
Errors.ErrorCode.SdpOfferHandlingError,
@@ -8892,14 +8905,15 @@ describe('plugin-meetings', () => {
88928905

88938906
eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
88948907

8895-
checkMetricSent('client.media-engine.remote-sdp-received', fakeError);
8908+
checkMetricSent('client.media-engine.remote-sdp-received', fakeError, 30004);
88968909
checkBehavioralMetricSent(
88978910
BEHAVIORAL_METRICS.PEERCONNECTION_FAILURE,
88988911
Errors.ErrorCode.SdpAnswerHandlingError,
88998912
fakeErrorMessage,
89008913
fakeRootCauseName
89018914
);
89028915
assert.calledOnce(meeting.deferSDPAnswer.reject);
8916+
assert.isTrue(meeting.deferSDPAnswer.reject.getCall(0).args[0].handledBySdk);
89038917
assert.calledOnce(clearTimeoutSpy);
89048918
});
89058919

@@ -8909,7 +8923,7 @@ describe('plugin-meetings', () => {
89098923

89108924
eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
89118925

8912-
checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
8926+
checkMetricSent('client.media-engine.local-sdp-generated', fakeError, 30002);
89138927
// expectedMetadataType is the error name in this case
89148928
checkBehavioralMetricSent(
89158929
BEHAVIORAL_METRICS.INVALID_ICE_CANDIDATE,
@@ -8927,7 +8941,7 @@ describe('plugin-meetings', () => {
89278941

89288942
eventListeners[MediaConnectionEventNames.ROAP_FAILURE](fakeError);
89298943

8930-
checkMetricSent('client.media-engine.local-sdp-generated', fakeError);
8944+
checkMetricSent('client.media-engine.local-sdp-generated', fakeError, 30003);
89318945
// expectedMetadataType is the error name in this case
89328946
checkBehavioralMetricSent(
89338947
BEHAVIORAL_METRICS.INVALID_ICE_CANDIDATE,
@@ -9121,17 +9135,26 @@ describe('plugin-meetings', () => {
91219135
assert.calledOnceWithExactly(getErrorPayloadForClientErrorCodeStub, {
91229136
clientErrorCode: expectedErrorCode,
91239137
});
9124-
assert.calledWithMatch(webex.internal.newMetrics.submitClientEvent, {
9138+
assert.deepEqual(webex.internal.newMetrics.submitClientEvent.getCall(0).args[0], {
91259139
name: 'client.media-engine.remote-sdp-received',
91269140
payload: {
91279141
canProceed,
91289142
errors: [{errorCode: expectedErrorCode, fatal: true}],
91299143
},
91309144
options: {
91319145
meetingId: meeting.id,
9132-
rawError: fakeError,
9146+
rawError: fakeError instanceof MultistreamNotSupportedError ? {
9147+
code: fakeError.code,
9148+
name: fakeError.name,
9149+
sdkMessage: fakeError.sdkMessage,
9150+
error: fakeError.error,
9151+
} : {},
91339152
},
91349153
});
9154+
const actualError = webex.internal.newMetrics.submitClientEvent.getCall(0).args[0].options.rawError;
9155+
9156+
assert.isTrue(actualError.handledBySdk);
9157+
assert.equal(actualError.message, fakeError.message);
91359158
};
91369159

91379160
it('handles OFFER message correctly when request fails', async () => {

packages/@webex/plugin-meetings/test/unit/spec/meeting/locusMediaRequest.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,12 @@ describe('LocusMediaRequest.send()', () => {
185185

186186
it('sends correct metric event when roap message fails', async () => {
187187
webexRequestStub.rejects({code: 300, message: 'fake error'});
188-
await assert.isRejected(sendRoapMessage('OFFER'));
188+
189+
const promise = sendRoapMessage('OFFER');
190+
await assert.isRejected(promise);
191+
192+
const error = await promise.catch((err) => err);
193+
assert.isTrue(error.handledBySdk);
189194

190195
assert.calledWith(mockWebex.internal.newMetrics.submitClientEvent, {
191196
name: 'client.locus.media.response',

packages/@webex/plugin-meetings/test/unit/spec/meeting/utils.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,6 +1185,14 @@ describe('plugin-meetings', () => {
11851185
});
11861186
});
11871187

1188+
describe('markErrorAsHandledBySdk', () => {
1189+
it('should set the error as handled', () => {
1190+
const error = MeetingUtil.markErrorAsHandledBySdk(new Error('Test error'));
1191+
1192+
assert.isTrue(error.handledBySdk);
1193+
})
1194+
});
1195+
11881196
describe('getChangeMeetingFloorErrorPayload', () => {
11891197
[
11901198
{

0 commit comments

Comments
 (0)