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

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
pull/21833/head
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,18 +1,21 @@
/* /*
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> 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"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react';
import { getCurrentLanguage, _t, _td } from './languageHandler'; import { getCurrentLanguage, _t, _td } from './languageHandler';
import PlatformPeg from './PlatformPeg'; import PlatformPeg from './PlatformPeg';
@ -106,61 +109,80 @@ function whitelistRedact(whitelist, str) {
return '<redacted>'; 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 { class Analytics {
constructor() { constructor() {
this._paq = null; this.baseUrl = null;
this.disabled = true; this.siteId = null;
this.visitVariables = {};
this.firstPage = true; 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 * Enable Analytics if initialized but disabled
* otherwise try and initalize, no-op if piwik config missing * otherwise try and initalize, no-op if piwik config missing
*/ */
enable() { async enable() {
if (this._paq || this._init()) { if (!this.disabled) return;
this.disabled = false;
}
}
/**
* 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(); const config = SdkConfig.get();
if (!config || !config.piwik || !config.piwik.url || !config.piwik.siteId) return; if (!config || !config.piwik || !config.piwik.url || !config.piwik.siteId) return;
const url = config.piwik.url; this.baseUrl = new URL("piwik.php", config.piwik.url);
const siteId = config.piwik.siteId; // set constants
const self = this; this.baseUrl.searchParams.set("rec", 1); // rec is required for tracking
this.baseUrl.searchParams.set("idsite", config.piwik.siteId); // rec is required for tracking
window._paq = this._paq = window._paq || []; 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
this._paq.push(['setTrackerUrl', url+'piwik.php']); // set user parameters
this._paq.push(['setSiteId', siteId]); this.baseUrl.searchParams.set("_id", getUid()); // uuid
this.baseUrl.searchParams.set("_idts", this.creationTs); // first ts
this._paq.push(['trackAllContentImpressions']); this.baseUrl.searchParams.set("_idvc", parseInt(this.visitCount, 10)+ 1); // visit count
this._paq.push(['discardHashTag', false]); if (this.lastVisitTs) {
this._paq.push(['enableHeartBeatTimer']); this.baseUrl.searchParams.set("_viewts", this.lastVisitTs); // last visit ts
// this._paq.push(['enableLinkTracking', true]); }
const platform = PlatformPeg.get(); const platform = PlatformPeg.get();
this._setVisitVariable('App Platform', platform.getHumanReadableName()); this._setVisitVariable('App Platform', platform.getHumanReadableName());
platform.getAppVersion().then((version) => { try {
this._setVisitVariable('App Version', version); this._setVisitVariable('App Version', await platform.getAppVersion());
}).catch(() => { } catch (e) {
this._setVisitVariable('App Version', 'unknown'); this._setVisitVariable('App Version', 'unknown');
}); }
this._setVisitVariable('Chosen Language', getCurrentLanguage()); this._setVisitVariable('Chosen Language', getCurrentLanguage());
@ -168,20 +190,61 @@ class Analytics {
this._setVisitVariable('Instance', window.location.pathname); this._setVisitVariable('Instance', window.location.pathname);
} }
(function() { // start heartbeat
const g = document.createElement('script'); this._heartbeatIntervalID = window.setInterval(this.ping.bind(this), HEARTBEAT_INTERVAL);
const s = document.getElementsByTagName('script')[0]; }
g.type='text/javascript'; g.async=true; g.defer=true; g.src=url+'piwik.js';
g.onload = function() { /**
console.log('Initialised anonymous analytics'); * Disable Analytics calls, will not fully unload Piwik until a refresh,
self._paq = window._paq; * 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 = {};
}
s.parentNode.insertBefore(g, s); async _track(data) {
})(); if (this.disabled) return;
return true; 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(),
};
const url = new URL(this.baseUrl);
for (const key in params) {
url.searchParams.set(key, params[key]);
}
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) { trackPageChange(generationTimeMs) {
@ -193,31 +256,29 @@ class Analytics {
return; return;
} }
if (typeof generationTimeMs === 'number') { if (typeof generationTimeMs !== 'number') {
this._paq.push(['setGenerationTimeMs', generationTimeMs]);
} else {
console.warn('Analytics.trackPageChange: expected generationTimeMs to be a number'); console.warn('Analytics.trackPageChange: expected generationTimeMs to be a number');
// But continue anyway because we still want to track the change // But continue anyway because we still want to track the change
} }
this._paq.push(['setCustomUrl', getRedactedUrl()]); this._track({
this._paq.push(['trackPageView']); gt_ms: generationTimeMs,
});
} }
trackEvent(category, action, name, value) { trackEvent(category, action, name, value) {
if (this.disabled) return; if (this.disabled) return;
this._paq.push(['setCustomUrl', getRedactedUrl()]); this._track({
this._paq.push(['trackEvent', category, action, name, value]); e_c: category,
} e_a: action,
e_n: name,
logout() { e_v: value,
if (this.disabled) return; });
this._paq.push(['deleteCookies']);
} }
_setVisitVariable(key, value) { _setVisitVariable(key, value) {
if (this.disabled) return; 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) { setLoggedIn(isGuest, homeserverUrl, identityServerUrl) {
@ -234,23 +295,16 @@ class Analytics {
this._setVisitVariable('Identity Server URL', whitelistRedact(whitelistedISUrls, identityServerUrl)); 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) { setBreadcrumbs(state) {
if (this.disabled) return; if (this.disabled) return;
this._setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled'); this._setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled');
} }
showDetailsModal() { showDetailsModal = () => {
let rows = []; let rows = [];
if (window.Piwik) { if (!this.disabled) {
const Tracker = window.Piwik.getAsyncTracker(); rows = Object.values(this.visitVariables);
rows = Object.values(customVariables).map((v) => Tracker.getCustomVariable(v.id)).filter(Boolean);
} else { } else {
// Piwik may not have been enabled, so show example values
rows = Object.keys(customVariables).map( rows = Object.keys(customVariables).map(
(k) => [ (k) => [
k, k,
@ -300,7 +354,7 @@ class Analytics {
</div> </div>
</div>, </div>,
}); });
} };
} }
if (!global.mxAnalytics) { if (!global.mxAnalytics) {

View File

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