Use embedded piwik caller rather than piwik.js bcuz CSP :D

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2020-02-13 00:39:28 +00:00
parent 7a026eb15d
commit 8d445d54ec
2 changed files with 139 additions and 85 deletions

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -14,6 +15,8 @@
limitations under the License.
*/
import React from 'react';
import { getCurrentLanguage, _t, _td } from './languageHandler';
import PlatformPeg from './PlatformPeg';
import SdkConfig from './SdkConfig';
@ -106,61 +109,80 @@ function whitelistRedact(whitelist, str) {
return '<redacted>';
}
const UID_KEY = "mx_Riot_Analytics_uid";
const CREATION_TS_KEY = "mx_Riot_Analytics_cts";
const VISIT_COUNT_KEY = "mx_Riot_Analytics_vc";
const LAST_VISIT_TS_KEY = "mx_Riot_Analytics_lvts";
function getUid() {
try {
let data = localStorage.getItem(UID_KEY);
if (!data) {
localStorage.setItem(UID_KEY, data = [...Array(16)].map(() => Math.random().toString(16)[2]).join(''));
}
return data;
} catch (e) {
console.error("Analytics error: ", e);
return "";
}
}
const HEARTBEAT_INTERVAL = 30 * 1000; // seconds
class Analytics {
constructor() {
this._paq = null;
this.disabled = true;
this.baseUrl = null;
this.siteId = null;
this.visitVariables = {};
this.firstPage = true;
this._heartbeatIntervalID = null;
this.creationTs = localStorage.getItem(CREATION_TS_KEY);
if (!this.creationTs) {
localStorage.setItem(CREATION_TS_KEY, this.creationTs = new Date().getTime());
}
this.lastVisitTs = localStorage.getItem(LAST_VISIT_TS_KEY);
this.visitCount = localStorage.getItem(VISIT_COUNT_KEY) || 0;
localStorage.setItem(VISIT_COUNT_KEY, parseInt(this.visitCount, 10) + 1);
}
get disabled() {
return !this.baseUrl;
}
/**
* Enable Analytics if initialized but disabled
* otherwise try and initalize, no-op if piwik config missing
*/
enable() {
if (this._paq || this._init()) {
this.disabled = false;
}
}
async enable() {
if (!this.disabled) return;
/**
* Disable Analytics calls, will not fully unload Piwik until a refresh,
* but this is second best, Piwik should not pull anything implicitly.
*/
disable() {
this.trackEvent('Analytics', 'opt-out');
// disableHeartBeatTimer is undocumented but exists in the piwik code
// the _paq.push method will result in an error being printed in the console
// if an unknown method signature is passed
this._paq.push(['disableHeartBeatTimer']);
this.disabled = true;
}
_init() {
const config = SdkConfig.get();
if (!config || !config.piwik || !config.piwik.url || !config.piwik.siteId) return;
const url = config.piwik.url;
const siteId = config.piwik.siteId;
const self = this;
window._paq = this._paq = window._paq || [];
this._paq.push(['setTrackerUrl', url+'piwik.php']);
this._paq.push(['setSiteId', siteId]);
this._paq.push(['trackAllContentImpressions']);
this._paq.push(['discardHashTag', false]);
this._paq.push(['enableHeartBeatTimer']);
// this._paq.push(['enableLinkTracking', true]);
this.baseUrl = new URL("piwik.php", config.piwik.url);
// set constants
this.baseUrl.searchParams.set("rec", 1); // rec is required for tracking
this.baseUrl.searchParams.set("idsite", config.piwik.siteId); // rec is required for tracking
this.baseUrl.searchParams.set("apiv", 1); // API version to use
this.baseUrl.searchParams.set("send_image", 0); // we want a 204, not a tiny GIF
// set user parameters
this.baseUrl.searchParams.set("_id", getUid()); // uuid
this.baseUrl.searchParams.set("_idts", this.creationTs); // first ts
this.baseUrl.searchParams.set("_idvc", parseInt(this.visitCount, 10)+ 1); // visit count
if (this.lastVisitTs) {
this.baseUrl.searchParams.set("_viewts", this.lastVisitTs); // last visit ts
}
const platform = PlatformPeg.get();
this._setVisitVariable('App Platform', platform.getHumanReadableName());
platform.getAppVersion().then((version) => {
this._setVisitVariable('App Version', version);
}).catch(() => {
try {
this._setVisitVariable('App Version', await platform.getAppVersion());
} catch (e) {
this._setVisitVariable('App Version', 'unknown');
});
}
this._setVisitVariable('Chosen Language', getCurrentLanguage());
@ -168,20 +190,61 @@ class Analytics {
this._setVisitVariable('Instance', window.location.pathname);
}
(function() {
const g = document.createElement('script');
const s = document.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=url+'piwik.js';
// start heartbeat
this._heartbeatIntervalID = window.setInterval(this.ping.bind(this), HEARTBEAT_INTERVAL);
}
g.onload = function() {
console.log('Initialised anonymous analytics');
self._paq = window._paq;
/**
* Disable Analytics calls, will not fully unload Piwik until a refresh,
* but this is second best, Piwik should not pull anything implicitly.
*/
disable() {
if (this.disabled) return;
this.trackEvent('Analytics', 'opt-out');
window.clearInterval(this._heartbeatIntervalID);
this.baseUrl = null;
this.visitVariables = {};
}
async _track(data) {
if (this.disabled) return;
const now = new Date();
const params = {
...data,
url: getRedactedUrl(),
_cvar: this.visitVariables, // user custom vars
res: `${window.screen.width}x${window.screen.height}`, // resolution as WWWWxHHHH
rand: String(Math.random()).slice(2, 8), // random nonce to cache-bust
h: now.getHours(),
m: now.getMinutes(),
s: now.getSeconds(),
};
s.parentNode.insertBefore(g, s);
})();
const url = new URL(this.baseUrl);
for (const key in params) {
url.searchParams.set(key, params[key]);
}
return true;
try {
await window.fetch(url, {
method: "GET",
mode: "no-cors",
cache: "no-cache",
redirect: "follow",
});
} catch (e) {
console.error("Analytics error: ", e);
window.err = e;
}
}
ping() {
this._track({
ping: 1,
});
localStorage.setItem(LAST_VISIT_TS_KEY, new Date().getTime()); // update last visit ts
}
trackPageChange(generationTimeMs) {
@ -193,31 +256,29 @@ class Analytics {
return;
}
if (typeof generationTimeMs === 'number') {
this._paq.push(['setGenerationTimeMs', generationTimeMs]);
} else {
if (typeof generationTimeMs !== 'number') {
console.warn('Analytics.trackPageChange: expected generationTimeMs to be a number');
// But continue anyway because we still want to track the change
}
this._paq.push(['setCustomUrl', getRedactedUrl()]);
this._paq.push(['trackPageView']);
this._track({
gt_ms: generationTimeMs,
});
}
trackEvent(category, action, name, value) {
if (this.disabled) return;
this._paq.push(['setCustomUrl', getRedactedUrl()]);
this._paq.push(['trackEvent', category, action, name, value]);
}
logout() {
if (this.disabled) return;
this._paq.push(['deleteCookies']);
this._track({
e_c: category,
e_a: action,
e_n: name,
e_v: value,
});
}
_setVisitVariable(key, value) {
if (this.disabled) return;
this._paq.push(['setCustomVariable', customVariables[key].id, key, value, 'visit']);
this.visitVariables[customVariables[key].id] = [key, value];
}
setLoggedIn(isGuest, homeserverUrl, identityServerUrl) {
@ -234,23 +295,16 @@ class Analytics {
this._setVisitVariable('Identity Server URL', whitelistRedact(whitelistedISUrls, identityServerUrl));
}
setRichtextMode(state) {
if (this.disabled) return;
this._setVisitVariable('RTE: Uses Richtext Mode', state ? 'on' : 'off');
}
setBreadcrumbs(state) {
if (this.disabled) return;
this._setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled');
}
showDetailsModal() {
showDetailsModal = () => {
let rows = [];
if (window.Piwik) {
const Tracker = window.Piwik.getAsyncTracker();
rows = Object.values(customVariables).map((v) => Tracker.getCustomVariable(v.id)).filter(Boolean);
if (!this.disabled) {
rows = Object.values(this.visitVariables);
} else {
// Piwik may not have been enabled, so show example values
rows = Object.keys(customVariables).map(
(k) => [
k,
@ -300,7 +354,7 @@ class Analytics {
</div>
</div>,
});
}
};
}
if (!global.mxAnalytics) {

View file

@ -632,7 +632,7 @@ export async function onLoggedOut() {
* @returns {Promise} promise which resolves once the stores have been cleared
*/
async function _clearStorage() {
Analytics.logout();
Analytics.disable();
if (window.localStorage) {
window.localStorage.clear();