[IMP] mail: add log keeping to rtc calls

This commit adds a new field `logs` to the model `mail.rtc_session`
that holds logs for each rtc sessions of the current call, if we are
in debug mode.

This commit also adds a button to download a .json file of the logs.

closes odoo/odoo#76339

Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
Thanh Dodeur
2021-09-14 13:31:53 +00:00
committed by Sébastien Theys
parent 2a1436ded7
commit 29cf3d4e84
3 changed files with 84 additions and 30 deletions
@@ -24,6 +24,12 @@
<i class="o_RtcOptionList_button_icon fa fa-lg fa-cog"/>
<span class="o_RtcOptionList_button_text">Settings</span>
</button>
<t t-if="env.isDebug()">
<button class="o_RtcOptionList_button" t-on-click="rtcOptionList.onClickDownloadLogs">
<i class="o_RtcOptionList_button_icon fa fa-lg fa-file-text-o"/>
<span class="o_RtcOptionList_button_text">Download logs</span>
</button>
</t>
</t>
</div>
</t>
+56 -30
View File
@@ -1,15 +1,5 @@
/** @odoo-module **/
function temporaryLog(message, { trace } = {}) {
if (trace) {
console.groupCollapsed(message);
console.trace(trace);
console.groupEnd();
return;
}
console.log(message);
}
import { browser } from "@web/core/browser/browser";
import { registerNewModel } from '@mail/model/model_core';
@@ -118,6 +108,7 @@ function factory(dependencies) {
}
for (const token of Object.keys(this._peerConnections)) {
if (!currentSessionsTokens.has(token)) {
this._addLogEntry(token, 'session removed from the server');
this._removePeer(token);
}
}
@@ -164,24 +155,23 @@ function factory(dependencies) {
}
switch (event) {
case "offer":
temporaryLog(`RECEIVED NOTIFICATION: ${event} from: ${rtcSession.name}`);
this._addLogEntry(sender, `received notification: ${event}`, { step: 'received offer' });
await this._handleRtcTransactionOffer(sender, payload);
break;
case "answer":
temporaryLog(`RECEIVED NOTIFICATION: ${event} from: ${rtcSession.name}`);
this._addLogEntry(sender, `received notification: ${event}`, { step: 'received answer' });
await this._handleRtcTransactionAnswer(sender, payload);
break;
case "ice-candidate":
await this._handleRtcTransactionICECandidate(sender, payload);
break;
case "disconnect":
this._addLogEntry(sender, `received notification: ${event}`, { step: ' peer cleanly disconnected ' });
this._removePeer(sender);
break;
case 'trackChange':
this._handleTrackChange(rtcSession, payload);
break;
case 'onOpenDataChannel':
temporaryLog(`DATA CHANNEL: onOpenDataChannel - peer connection with ${sender} established`);
}
}
@@ -235,6 +225,7 @@ function factory(dependencies) {
this.update({
currentRtcSession: clear(),
logs: clear(),
sendUserVideo: clear(),
sendDisplay: clear(),
videoTrack: clear(),
@@ -368,6 +359,33 @@ function factory(dependencies) {
// Private
//----------------------------------------------------------------------
/**
* @private
* @param {String} token
* @param {String} entry
* @param {Object} [param2]
* @param {String} [param2.step] current step of the flow
* @param {String} [param2.state] current state of the connection
*/
_addLogEntry(token, entry, { step, state } = {}) {
if (!this.env.isDebug()) {
return;
}
if (!(token in this.logs)) {
this.logs[token] = { step: '', state: '', logs: [] };
}
this.logs[token].logs.push({
event: `${window.moment().format('h:mm:ss')}: ${entry}`,
trace: window.Error().stack.split('\n').slice(1, 15),
});
if (step) {
this.logs[token].step = step;
}
if (state) {
this.logs[token].state = state;
}
}
/**
* @private
* @param {String} token
@@ -397,8 +415,9 @@ function factory(dependencies) {
continue;
}
session.update({
connectionState: 'Disconnected: sending initial RTC offer',
connectionState: 'Not connected: sending initial RTC offer',
});
this._addLogEntry(session.peerToken, 'init call', { step: 'init call' });
this._callPeer(session.peerToken);
}
}
@@ -419,6 +438,7 @@ function factory(dependencies) {
*/
_createPeerConnection(token) {
const peerConnection = new window.RTCPeerConnection({ iceServers: this.iceServers });
this._addLogEntry(token, `RTCPeerConnection created`, { step: 'peer connection created' });
peerConnection.onicecandidate = async (event) => {
if (!event.candidate) {
return;
@@ -429,26 +449,26 @@ function factory(dependencies) {
});
};
peerConnection.oniceconnectionstatechange = (event) => {
temporaryLog('ICE STATE UPDATE: ' + peerConnection.iceConnectionState);
this._onICEConnectionStateChange(peerConnection.iceConnectionState, token);
};
peerConnection.onconnectionstatechange = (event) => {
temporaryLog('CONNECTION STATE UPDATE:' + peerConnection.connectionState);
this._onConnectionStateChange(peerConnection.connectionState, token);
};
peerConnection.onicecandidateerror = async (error) => {
temporaryLog('=== ERROR: onIceCandidate ===', { trace: error });
this._addLogEntry(token, `ice candidate error`);
this._recoverConnection(token, { delay: this.recoveryTimeout, reason: 'ice candidate error' });
};
peerConnection.onnegotiationneeded = async (event) => {
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
this._addLogEntry(token, `sending notification: offer`, { step: 'sending offer' });
await this._notifyPeers([token], {
event: 'offer',
payload: { sdp: peerConnection.localDescription },
});
};
peerConnection.ontrack = ({ transceiver, track }) => {
this._addLogEntry(token, `received ${track.kind} track`);
this._updateExternalSessionTrack(track, token);
};
const dataChannel = peerConnection.createDataChannel("notifications", { negotiated: true, id: 1 });
@@ -456,10 +476,6 @@ function factory(dependencies) {
this.handleNotification(token, event.data);
};
dataChannel.onopen = async () => {
await this._notifyPeers([token], {
event: 'onOpenDataChannel',
type: 'peerToPeer',
});
/**
* FIXME? it appears that the track yielded by the peerConnection's 'ontrack' event is always enabled,
* even when it is disabled on the sender-side.
@@ -508,6 +524,7 @@ function factory(dependencies) {
try {
await peerConnection.setRemoteDescription(rtcSessionDescription);
} catch (e) {
this._addLogEntry(fromToken, 'answer handling: Failed at setting remoteDescription');
// ignored the transaction may have been resolved by another concurrent offer.
}
}
@@ -527,7 +544,7 @@ function factory(dependencies) {
try {
await peerConnection.addIceCandidate(rtcIceCandidate);
} catch (error) {
temporaryLog('=== ERROR: ADD ICE CANDIDATE ===', { trace: error });
this._addLogEntry(fromToken, 'ICE candidate handling: failed at adding the candidate to the connection');
this._recoverConnection(fromToken, { delay: this.recoveryTimeout, reason: 'failed at adding ice candidate' });
}
}
@@ -552,16 +569,16 @@ function factory(dependencies) {
try {
await peerConnection.setRemoteDescription(rtcSessionDescription);
} catch (e) {
// ignored the transaction may have been resolved by another concurrent offer.
this._addLogEntry(fromToken, 'offer handling: failed at setting remoteDescription');
return;
}
await peerConnection.setRemoteDescription(rtcSessionDescription);
await this._updateRemoteTrack(peerConnection, 'audio', { token: fromToken });
await this._updateRemoteTrack(peerConnection, 'video', { token: fromToken });
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
this._addLogEntry(fromToken, `sending notification: answer`, { step: 'sending answer' });
await this._notifyPeers([fromToken], {
event: 'answer',
payload: { sdp: peerConnection.localDescription },
@@ -613,7 +630,6 @@ function factory(dependencies) {
});
if (type === 'server') {
temporaryLog(`SEND NOTIFICATION: - ${event} to: [${targetTokens}]`);
await this.env.services.rpc({
route: '/mail/rtc/session/notify_call_members',
params: {
@@ -664,8 +680,7 @@ function factory(dependencies) {
if (['connected', 'closed'].includes(peerConnection.connectionState)) {
return;
}
temporaryLog(`RECOVERY: calling back ${token} to salvage the connection ${peerConnection.iceConnectionState}, reason: ${reason}`);
this._addLogEntry(token, `calling back to recover ${peerConnection.iceConnectionState} connection, reason: ${reason}`);
await this._notifyPeers([token], {
event: 'disconnect',
});
@@ -697,6 +712,7 @@ function factory(dependencies) {
delete this._fallBackTimeouts[token];
this._outGoingCallTokens.delete(token);
this._addLogEntry(token, 'peer removed', { step: 'peer removed' });
}
/**
@@ -925,6 +941,7 @@ function factory(dependencies) {
* @param {String} [param2.token]
*/
async _updateRemoteTrack(peerConnection, trackKind, { initTransceiver, token } = {}) {
this._addLogEntry(token, `updating ${trackKind} transceiver`);
const track = trackKind === 'audio' ? this.audioTrack : this.videoTrack;
const fullDirection = track ? 'sendrecv' : 'recvonly';
const limitedDirection = track ? 'sendonly' : 'inactive';
@@ -976,6 +993,7 @@ function factory(dependencies) {
* @param {String} token of the peer whose the connection changed
*/
async _onConnectionStateChange(state, token) {
this._addLogEntry(token, `connection state changed: ${state}`);
switch (state) {
case "closed":
this._removePeer(token);
@@ -993,6 +1011,7 @@ function factory(dependencies) {
* @param {String} token of the peer whose the connection changed
*/
async _onICEConnectionStateChange(connectionState, token) {
this._addLogEntry(token, `ICE connection state changed: ${connectionState}`, { state: connectionState });
const rtcSession = this.messaging.models['mail.rtc_session'].findFromIdentifyingData({ id: token });
if (!rtcSession) {
return;
@@ -1099,13 +1118,20 @@ function factory(dependencies) {
],
}),
/**
* How long to wait before considering a connection as needing recovery.
* Contains the logs of the current session by token.
* { token: { name<String>, logs<Array> } }
*/
logs: attr({
default: {},
}),
/**
* How long to wait before considering a connection as needing recovery in ms.
*/
recoveryTimeout: attr({
default: 15000,
}),
/**
* How long to wait before recovering a connection that has failed.
* How long to wait before recovering a connection that has failed in ms.
*/
recoveryDelay: attr({
default: 3000,
@@ -12,6 +12,7 @@ function factory(dependencies) {
*/
_created() {
super._created();
this.onClickDownloadLogs = this.onClickDownloadLogs.bind(this);
this.onClickActivateFullScreen = this.onClickActivateFullScreen.bind(this);
this.onClickDeactivateFullScreen = this.onClickDeactivateFullScreen.bind(this);
this.onClickLayout = this.onClickLayout.bind(this);
@@ -22,6 +23,27 @@ function factory(dependencies) {
// Public
//----------------------------------------------------------------------
/**
* Creates and download a file that contains the logs of the current RTC call.
*
* @param {MouseEvent} ev
*/
async onClickDownloadLogs(ev) {
const channel = this.rtcController.callViewer.threadView.thread;
if (!channel.mailRtc) {
return;
}
const data = window.JSON.stringify(channel.mailRtc.logs);
const blob = new window.Blob([data], { type: 'application/json' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `RtcLogs_Channel${channel.id}_Session${channel.mailRtc.currentRtcSession.id}_${window.moment().format('YYYY-MM-DD_HH-mm')}.json`;
a.click();
window.URL.revokeObjectURL(url);
this.component.trigger('o-popover-close');
}
/**
* @param {MouseEvent} ev
*/