diff --git a/packages/@webex/internal-plugin-support/src/config.js b/packages/@webex/internal-plugin-support/src/config.js index c85a0aa5cca..8fcd1226b2a 100644 --- a/packages/@webex/internal-plugin-support/src/config.js +++ b/packages/@webex/internal-plugin-support/src/config.js @@ -17,5 +17,6 @@ export default { appType: '', appVersion: '', languageCode: '', + incrementalLogs: false, }, }; diff --git a/packages/@webex/internal-plugin-support/src/support.js b/packages/@webex/internal-plugin-support/src/support.js index f41d380637e..b5cb0824f19 100644 --- a/packages/@webex/internal-plugin-support/src/support.js +++ b/packages/@webex/internal-plugin-support/src/support.js @@ -107,6 +107,10 @@ const Support = WebexPlugin.extend({ return this.webex.upload(options); }) .then((body) => { + if (this.config.incrementalLogs) { + this.webex.logger.clearBuffers(); + } + if (userId && !body.userId) { body.userId = userId; } diff --git a/packages/@webex/plugin-logger/src/logger.js b/packages/@webex/plugin-logger/src/logger.js index 7ef5a925fa9..183602dbe50 100644 --- a/packages/@webex/plugin-logger/src/logger.js +++ b/packages/@webex/plugin-logger/src/logger.js @@ -252,6 +252,20 @@ const Logger = WebexPlugin.extend({ return this.getCurrentLevel(); }, + /** + * Clears the log buffers + * + * @instance + * @memberof Logger + * @public + * @returns {undefined} + */ + clearBuffers() { + this.clientBuffer = []; + this.sdkBuffer = []; + this.buffer = []; + }, + /** * Format logs (for upload) * diff --git a/packages/@webex/plugin-meetings/src/config.ts b/packages/@webex/plugin-meetings/src/config.ts index c95a2b43405..28c451ab152 100644 --- a/packages/@webex/plugin-meetings/src/config.ts +++ b/packages/@webex/plugin-meetings/src/config.ts @@ -96,5 +96,6 @@ export default { iceCandidatesGatheringTimeout: undefined, backendIpv6NativeSupport: false, reachabilityGetClusterTimeout: 5000, + logUploadIntervalMultiplicationFactor: 0, // if set to 0 or undefined, logs won't be uploaded periodically }, }; diff --git a/packages/@webex/plugin-meetings/src/meeting/index.ts b/packages/@webex/plugin-meetings/src/meeting/index.ts index edff3f1de51..debefba9fd5 100644 --- a/packages/@webex/plugin-meetings/src/meeting/index.ts +++ b/packages/@webex/plugin-meetings/src/meeting/index.ts @@ -5,6 +5,7 @@ import jwtDecode from 'jwt-decode'; import {StatelessWebexPlugin} from '@webex/webex-core'; // @ts-ignore - Types not available for @webex/common import {Defer} from '@webex/common'; +import {safeSetTimeout, safeSetInterval} from '@webex/common-timers'; import { ClientEvent, ClientEventLeaveReason, @@ -702,6 +703,8 @@ export default class Meeting extends StatelessWebexPlugin { private iceCandidateErrors: Map; private iceCandidatesCount: number; private rtcMetrics?: RtcMetrics; + private uploadLogsTimer?: ReturnType; + private logUploadIntervalIndex: number; /** * @param {Object} attrs @@ -770,6 +773,8 @@ export default class Meeting extends StatelessWebexPlugin { ); this.callStateForMetrics.correlationId = this.id; } + this.logUploadIntervalIndex = 0; + /** * @instance * @type {String} @@ -4060,6 +4065,65 @@ export default class Meeting extends StatelessWebexPlugin { Trigger.trigger(this, options, EVENTS.REQUEST_UPLOAD_LOGS, this); } + /** + * sets the timer for periodic log upload + * @returns {void} + */ + private setLogUploadTimer() { + // start with short timeouts and increase them later on so in case users have very long multi-hour meetings we don't get too fragmented logs + const LOG_UPLOAD_INTERVALS = [0.1, 1, 15, 15, 30, 30, 30, 60]; + + const delay = + 1000 * + // @ts-ignore - config coming from registerPlugin + this.config.logUploadIntervalMultiplicationFactor * + LOG_UPLOAD_INTERVALS[this.logUploadIntervalIndex]; + + if (this.logUploadIntervalIndex < LOG_UPLOAD_INTERVALS.length - 1) { + this.logUploadIntervalIndex += 1; + } + + this.uploadLogsTimer = safeSetTimeout(() => { + this.uploadLogsTimer = undefined; + + this.uploadLogs(); + + // just as an extra precaution, to avoid uploading logs forever in case something goes wrong + // and the page remains opened, we stop it if there is no media connection + if (!this.mediaProperties.webrtcMediaConnection) { + return; + } + + this.setLogUploadTimer(); + }, delay); + } + + /** + * Starts a periodic upload of logs + * + * @returns {undefined} + */ + public startPeriodicLogUpload() { + // @ts-ignore - config coming from registerPlugin + if (this.config.logUploadIntervalMultiplicationFactor && !this.uploadLogsTimer) { + this.logUploadIntervalIndex = 0; + + this.setLogUploadTimer(); + } + } + + /** + * Stops the periodic upload of logs + * + * @returns {undefined} + */ + public stopPeriodicLogUpload() { + if (this.uploadLogsTimer) { + clearTimeout(this.uploadLogsTimer); + this.uploadLogsTimer = undefined; + } + } + /** * Removes remote audio, video and share streams from class instance's mediaProperties * @returns {undefined} @@ -7247,6 +7311,7 @@ export default class Meeting extends StatelessWebexPlugin { // We can log ReceiveSlot SSRCs only after the SDP exchange, so doing it here: this.remoteMediaManager?.logAllReceiveSlots(); + this.startPeriodicLogUpload(); } catch (error) { LoggerProxy.logger.error(`${LOG_HEADER} failed to establish media connection: `, error); diff --git a/packages/@webex/plugin-meetings/src/meeting/util.ts b/packages/@webex/plugin-meetings/src/meeting/util.ts index 8fa62d26d8c..6b903125a1e 100644 --- a/packages/@webex/plugin-meetings/src/meeting/util.ts +++ b/packages/@webex/plugin-meetings/src/meeting/util.ts @@ -177,6 +177,7 @@ const MeetingUtil = { cleanUp: (meeting) => { meeting.getWebexObject().internal.device.meetingEnded(); + meeting.stopPeriodicLogUpload(); meeting.breakouts.cleanUp(); meeting.simultaneousInterpretation.cleanUp(); 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 a94ea8b5737..d54d1cb75f1 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/index.js @@ -2471,6 +2471,63 @@ describe('plugin-meetings', () => { checkWorking(); }); + it('should upload logs periodically', async () => { + const clock = sinon.useFakeTimers(); + + meeting.roap.doTurnDiscovery = sinon + .stub() + .resolves({turnServerInfo: undefined, turnDiscoverySkippedReason: undefined}); + + let logUploadCounter = 0; + + TriggerProxy.trigger.callsFake((meetingObject, options, event) => { + if ( + meetingObject === meeting && + options.file === 'meeting/index' && + options.function === 'uploadLogs' && + event === 'REQUEST_UPLOAD_LOGS' + ) { + logUploadCounter += 1; + } + }); + + meeting.config.logUploadIntervalMultiplicationFactor = 1; + meeting.meetingState = 'ACTIVE'; + + await meeting.addMedia({ + mediaSettings: {}, + }); + + const checkLogCounter = (delay, expectedCounter) => { + // first check that the counter is not increased just before the delay + clock.tick(delay - 50); + assert.equal(logUploadCounter, expectedCounter - 1); + + // and now check that it has reached expected value after the delay + clock.tick(50); + assert.equal(logUploadCounter, expectedCounter); + }; + + checkLogCounter(100, 1); + checkLogCounter(1000, 2); + checkLogCounter(15000, 3); + checkLogCounter(15000, 4); + checkLogCounter(30000, 5); + checkLogCounter(30000, 6); + checkLogCounter(30000, 7); + checkLogCounter(60000, 8); + checkLogCounter(60000, 9); + checkLogCounter(60000, 10); + + // simulate media connection being removed -> no more log uploads should happen + meeting.mediaProperties.webrtcMediaConnection = undefined; + + clock.tick(60000); + assert.equal(logUploadCounter, 11); + + clock.restore(); + }); + it('should attach the media and return promise when in the lobby if allowMediaInLobby is set', async () => { meeting.roap.doTurnDiscovery = sinon .stub() 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 0c9be4c03f5..05e195d8fce 100644 --- a/packages/@webex/plugin-meetings/test/unit/spec/meeting/utils.js +++ b/packages/@webex/plugin-meetings/test/unit/spec/meeting/utils.js @@ -39,6 +39,7 @@ describe('plugin-meetings', () => { meeting.cleanupLocalStreams = sinon.stub().returns(Promise.resolve()); meeting.closeRemoteStreams = sinon.stub().returns(Promise.resolve()); meeting.closePeerConnections = sinon.stub().returns(Promise.resolve()); + meeting.stopPeriodicLogUpload = sinon.stub(); meeting.unsetRemoteStreams = sinon.stub(); meeting.unsetPeerConnections = sinon.stub(); @@ -64,6 +65,7 @@ describe('plugin-meetings', () => { assert.calledOnce(meeting.cleanupLocalStreams); assert.calledOnce(meeting.closeRemoteStreams); assert.calledOnce(meeting.closePeerConnections); + assert.calledOnce(meeting.stopPeriodicLogUpload); assert.calledOnce(meeting.unsetRemoteStreams); assert.calledOnce(meeting.unsetPeerConnections);