Skip to content

Commit

Permalink
Merge pull request #26 from nordeck/nic/feat/BMI-209_feedback_injector
Browse files Browse the repository at this point in the history
BMI-209 added feedback hadler functionality as a Jitsi HTML plugin
  • Loading branch information
owanckel authored Apr 21, 2023
2 parents 51ca7db + 593a116 commit 9b850d8
Show file tree
Hide file tree
Showing 2 changed files with 293 additions and 3 deletions.
21 changes: 20 additions & 1 deletion jitsi-feedback-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ If required by your special use case, it is technically possible to host `feedba
These are the relevant settings that need to be set in jitsi-meet.
The intended way to configure this is by leveraging the `custom-config.js` method.

Do not copy plugin.head.html into `/usr/share/jitsi-meet/` if the analytics system is enabled and third party requests are enabled.

```javascript
// address of the feedback backend REST API, reachable from the end user device
config.feedbackBackend = 'https://example.org:8080'
Expand All @@ -26,7 +28,7 @@ config.callStatsID = 'id';
// disables CallStats even if callStatsID is set
config.callStatsSecret = null;

// this will enable analytics, both need to be false for our handler to work
// IMPORTANT: this will enable analytics, both need to be false for our handler to work
config.disableThirdPartyRequests = false;
config.analytics.disabled = false

Expand Down Expand Up @@ -73,6 +75,23 @@ config.deploymentInfo = {

```

### Deployment if third party requests are disabled

If `config.disableThirdPartyRequests = true` the analytics system will be disabled.
Copy plugin.head.html into `/usr/share/jitsi-meet/plugin.head.html`.

The plugin injects itself into Jitsi javascript functions and is more likely to break after upgrading the Jitsi frontend to a new version,
but it enables collecting the feedback even if it can't be sent using the analytics handlers.

The above configuration has to be changed this way:
```js
config.disableThirdPartyRequests = true;

// the handlers won't be loaded, so this may be removed
// config.analytics.scriptURLs = ['/feedback.js'];
```


## Sample Data

| Metadata | Value |
Expand Down
275 changes: 273 additions & 2 deletions jitsi-feedback-plugin/plugin.head.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,276 @@
-->

<script type="application/javascript">
console.log('plugin.head.html loaded.');
</script>
console.log(`FEEDBACK html plugin loaded.`);
const LOG = 'FEEDBACK_INJECTOR';

// DON'T DEPLOY THIS PLUGIN AND feedback.js ON THE SAME SERVER, THEY WILL CAUSE CONFLICT.
// REMOVE feedback.js FROM config.analytics.scriptURLs

// save the original function
const oldInitJitsiConference = window.JitsiMeetJS.JitsiConnection.prototype.initJitsiConference;


window.JitsiMeetJS.JitsiConnection.prototype.initJitsiConference = function () {
console.log('initJitsiConference', arguments);

// leaving conference global allows us to collect metrics from here later
conference = oldInitJitsiConference.apply(this, arguments);

// isCallstatsEnabled = true enables the feedback form when leaving the application
// note that in order to enable the feedback button in the toolbar, a callStatsID must be configured regardless
conference.statistics.isCallstatsEnabled = () => true;


const feedback = new Feedback();

// get the JWT on the conference initialization
feedback.sendEvent({
action: 'connection.stage.reached',
actionSubject: 'conference_muc.joined'
});


// overriding sendFeedback achieves 2 of our goals:
// 1. it lets us inject code in the right place (feedback submission)
// 2. it disables sending feedback to other backends, primarily callstats.io
conference.statistics.sendFeedback = async function (score, details) {
feedback.sendEvent({
action: 'feedback',
actionSubject: 'feedback',
attributes: {
rating: score,
comment: details
}
});
}

return conference;
}


// ------------------------------------------------------------------------------------------

class Feedback {
constructor(options) {
this.sendEvent = this.sendEvent.bind(this);
this.setUserProperties = this.setUserProperties.bind(this);
}

// called from lib-jitsi-meet
sendEvent(event) {
// console.log(`${LOG} handler`, event);

if (event.action === 'connection.stage.reached' && event.actionSubject === 'conference_muc.joined') {
this.handleJoin();
return;
}

if (event.action === 'feedback' && event.actionSubject === 'feedback') {
this.handleFeedback(event.attributes);
return;
}
}

// called from lib-jitsi-meet
setUserProperties(permanentProperties) {
// nothing
}

handleFeedback(data) {
const {rating, comment} = data;
console.log(`${LOG} Feedback`, rating, comment);

const jwt = window.APP.conference.feedbackToken;
console.log(`${LOG} gather metrics for JWT: ${jwt}`);

const postFeedback = async (jwt, payload) => {
const baseUrl = APP.store.getState()['features/base/config'].feedbackBackend;
const url = `${baseUrl}/feedback`;

const headers = {
'authorization': `Bearer ${jwt}`
};

const res = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(payload)
});

if (!res.ok) {
throw `${LOG} Status error: ${res.status}`;
}

return res.text();
};

const metrics = this._gatherMetrics();

const payload = {
rating: rating,
rating_comment: comment,
metadata: {
...metrics
}
}

postFeedback(jwt, payload)
.then(result => {
console.log(`${LOG} feedback result: `, payload);
})
.catch(e => console.error(`${LOG} failed to feedback`, e));
}

handleJoin() {
// Extract matrix openId token from the Jitsi JWT token
const oidToken = this._getMatrixContext().matrix.token;

console.log(`${LOG} Extracted matrix token: ${oidToken}`);

const getToken = async (oidToken) => {
const baseUrl = APP.store.getState()['features/base/config'].feedbackBackend;
const url = `${baseUrl}/token`;

const headers = {
'authorization': `Bearer ${oidToken}`
};

const res = await fetch(url, {
method: 'GET',
headers
});

if (!res.ok) {
throw `${LOG} Status error: ${res.status}`;
}

return res.text();
};

// get the feedback JWT token as soon as possible
getToken(oidToken)
.then(feedbackToken => {
console.log(`${LOG} feedback JWT: ${feedbackToken}`);
window.APP.conference.feedbackToken = feedbackToken;
})
.catch(e => console.error(`${LOG} failed to fetch JWT`, e));

return;
}

_gatherMetrics() {
const metrics = {};
const config = APP.store.getState()['features/base/config'];
const flags = config.metadata || [];

flags.forEach(flag => {
try {
this._addMetric(flag, metrics)
} catch (e) {
console.error(`${LOG} Metrics error ${flag}:`, e);
}
});

return metrics;
}

_getMatrixContext() {
const token = window.APP.store.getState()['features/base/jwt'].jwt;
const payload = token.split('.')[1];
const content = JSON.parse(atob(payload));
return content.context;
}

_addMetric(flag, metrics) {
const config = APP.store.getState()['features/base/config'];
const conference = window.APP.store.getState()['features/base/conference'].conference;
const localParticipant = window.APP.store.getState()['features/base/participants'].local;

switch (flag) {
// meetingId
case 'MEETING_URL':
metrics.meetingUrl = window.location.href;
break;
case 'MEETING_ID':
metrics.meetingId = JitsiMeetJS.analytics.permanentProperties.conference_name;
break;

// participantID
case 'PARTICIPANT_ID':
metrics.participantId = conference.myUserId();
break;

// matrix user Id
case 'MATRIX_USER_ID':
metrics.matrixUserId = localParticipant.email;
break;
case 'DISPLAY_NAME':
metrics.displayName = localParticipant.name;
break;

case 'USER_REGION':
metrics.userRegion = JitsiMeetJS.analytics.permanentProperties.userRegion;
break;

// app data
case 'APP_LIB_VERSION':
metrics.appLibVersion = JitsiMeetJS.version;
break;
case 'APP_FOCUS_VERSION':
metrics.appFocusVersion = conference.componentsVersions.versions.focus;
break;
case 'APP_NAME':
metrics.appName = JitsiMeetJS.analytics.permanentProperties.appName;
break;
case 'APP_MEETING_REGION':
metrics.appMeetingRegion = config.deploymentInfo.region;
break;
case 'APP_SHARD':
metrics.appShard = config.deploymentInfo.shard;
break;
case 'APP_REGION':
metrics.appRegion = config.deploymentInfo.region;
break;
case 'APP_ENVIRONMENT':
metrics.appEnvironment = config.deploymentInfo.environment;
break;
case 'APP_ENV_TYPE':
metrics.appEnvType = config.deploymentInfo.envType;
break;
case 'APP_BACKEND_RELEASE':
metrics.appBackendRelease = config.deploymentInfo.backendRelease;
break;

// browser, os
case 'USER_AGENT':
metrics.userAgent = JitsiMeetJS.analytics.permanentProperties.user_agent;
break;
case 'BROWSER_NAME':
metrics.browserName = JitsiMeetJS.util.browser.getName();
break;
case 'BROWSER_VERSION':
metrics.browserVersion = JitsiMeetJS.util.browser.getVersion();
break;
case 'OS_NAME':
metrics.osName = JitsiMeetJS.util.browser._bowser.parseOS().name;
break;
case 'OS_VERSION':
metrics.osVersion = JitsiMeetJS.util.browser._bowser.parseOS().version;
break;
case 'OS_VERSION_NAME':
metrics.osVersionName = JitsiMeetJS.util.browser._bowser.parseOS().versionName;
break;

// standalone or embedded
case 'EXTERNAL_API':
metrics.externalApi = JitsiMeetJS.analytics.permanentProperties.externalApi;
break;
case 'IN_IFRAME':
metrics.inIframe = JitsiMeetJS.analytics.permanentProperties.inIframe;
break;
}
}
}

</script>

0 comments on commit 9b850d8

Please sign in to comment.