[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:
committed by
Sébastien Theys
parent
2a1436ded7
commit
29cf3d4e84
@@ -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>
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user