mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-17 14:05:04 +08:00
Merge pull request #4066 from matrix-org/t3chguy/piwik_csp
Use embedded piwik script rather than piwik.js to respect CSP
This commit is contained in:
commit
12c743b160
225
src/Analytics.js
225
src/Analytics.js
@ -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");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { getCurrentLanguage, _t, _td } from './languageHandler';
|
||||
import PlatformPeg from './PlatformPeg';
|
||||
@ -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,64 @@ 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, stop the heartbeat and clear identifiers from localStorage
|
||||
*/
|
||||
disable() {
|
||||
if (this.disabled) return;
|
||||
this.trackEvent('Analytics', 'opt-out');
|
||||
window.clearInterval(this._heartbeatIntervalID);
|
||||
this.baseUrl = null;
|
||||
this.visitVariables = {};
|
||||
localStorage.removeItem(UID_KEY);
|
||||
localStorage.removeItem(CREATION_TS_KEY);
|
||||
localStorage.removeItem(VISIT_COUNT_KEY);
|
||||
localStorage.removeItem(LAST_VISIT_TS_KEY);
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -193,31 +259,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 +298,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 +357,7 @@ class Analytics {
|
||||
</div>
|
||||
</div>,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!global.mxAnalytics) {
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user