From 98c2f9201bd4becef31c9fcb4a07123223114da8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 27 May 2017 20:47:09 +0100 Subject: [PATCH 01/19] initial piwik stuff Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Analytics.js | 77 ++++++++++++++++++++++ src/Lifecycle.js | 2 + src/components/structures/MatrixChat.js | 2 + src/components/views/dialogs/BaseDialog.js | 4 +- 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 src/Analytics.js diff --git a/src/Analytics.js b/src/Analytics.js new file mode 100644 index 0000000000..eccbecc304 --- /dev/null +++ b/src/Analytics.js @@ -0,0 +1,77 @@ +/* + Copyright 2017 Michael Telatynski <7t3chguy@gmail.com> + + 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 + + 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. + */ + +/* + * Holds the current Platform object used by the code to do anything + * specific to the platform we're running on (eg. web, electron) + * Platforms are provided by the app layer. + * This allows the app layer to set a Platform without necessarily + * having to have a MatrixChat object + */ + +import MatrixClientPeg from './MatrixClientPeg'; +// import dis from './dispatcher'; + +function redact(str) { + return str.replace(/#\/(room|user)\/(.+)/, "#/$1/"); +} + +class Analytics { + constructor() { + this.tracker = null; + } + + set(tracker) { + this.tracker = tracker; + + this.tracker.enableHeartBeatTimer(); + this.tracker.enableLinkTracking(true); + + // dis.register(this._onAction.bind(this)); + } + + // _onAction(payload) { + // this.trackEvent('Dispatcher', payload.action); + // } + + async trackPageChange() { + if (!this.tracker) return; + this.tracker.trackPageView(redact(window.location.hash)); + } + + async trackEvent(category, action, name) { + if (!this.tracker) return; + this.tracker.trackEvent(category, action, name); + } + + async logout() { + if (!this.tracker) return; + this.tracker.deleteCookies(); + } + + async login() { // not used currently + const cli = MatrixClientPeg.get(); + if (!this.tracker || !cli) return; + + this.tracker.setUserId(`@${cli.getUserIdLocalpart()}:${cli.getDomain()}`); + } + +} + +if (!global.mxAnalytics) { + global.mxAnalytics = new Analytics(); +} +module.exports = global.mxAnalytics; diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 0e3e52fe40..951f00d270 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -19,6 +19,7 @@ import q from 'q'; import Matrix from 'matrix-js-sdk'; import MatrixClientPeg from './MatrixClientPeg'; +import Analytics from './Analytics'; import Notifier from './Notifier'; import UserActivity from './UserActivity'; import Presence from './Presence'; @@ -405,6 +406,7 @@ export function onLoggedOut() { } function _clearLocalStorage() { + Analytics.logout(); if (!window.localStorage) { return; } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 2ba1506551..625ff26604 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -20,6 +20,7 @@ import q from 'q'; import React from 'react'; import Matrix from "matrix-js-sdk"; +import Analytics from "../../Analytics"; import MatrixClientPeg from "../../MatrixClientPeg"; import PlatformPeg from "../../PlatformPeg"; import SdkConfig from "../../SdkConfig"; @@ -1004,6 +1005,7 @@ module.exports = React.createClass({ if (this.props.onNewScreen) { this.props.onNewScreen(screen); } + Analytics.trackPageChange(); }, onAliasClick: function(event, alias) { diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 0b2ca5225d..8c3e9d687c 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -16,6 +16,7 @@ limitations under the License. import React from 'react'; +import Analytics from '../../../Analytics'; import * as KeyCode from '../../../KeyCode'; import AccessibleButton from '../elements/AccessibleButton'; import sdk from '../../../index'; @@ -66,8 +67,9 @@ export default React.createClass({ }, render: function() { + Analytics.trackEvent('Dialog', 'mount', this.props.title); const TintableSvg = sdk.getComponent("elements.TintableSvg"); - + return (
Date: Sat, 27 May 2017 21:08:00 +0100 Subject: [PATCH 02/19] remove unrelated comment, my copy pasting is way too obvious xD Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Analytics.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Analytics.js b/src/Analytics.js index eccbecc304..9832013053 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -14,14 +14,6 @@ limitations under the License. */ -/* - * Holds the current Platform object used by the code to do anything - * specific to the platform we're running on (eg. web, electron) - * Platforms are provided by the app layer. - * This allows the app layer to set a Platform without necessarily - * having to have a MatrixChat object - */ - import MatrixClientPeg from './MatrixClientPeg'; // import dis from './dispatcher'; From eb6136b957ddc3b5bd0c2843876076bbb9e5083d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 28 May 2017 11:26:41 +0100 Subject: [PATCH 03/19] remove dispatcher Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Analytics.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Analytics.js b/src/Analytics.js index 9832013053..a39fc7250b 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -15,7 +15,6 @@ */ import MatrixClientPeg from './MatrixClientPeg'; -// import dis from './dispatcher'; function redact(str) { return str.replace(/#\/(room|user)\/(.+)/, "#/$1/"); @@ -31,14 +30,8 @@ class Analytics { this.tracker.enableHeartBeatTimer(); this.tracker.enableLinkTracking(true); - - // dis.register(this._onAction.bind(this)); } - // _onAction(payload) { - // this.trackEvent('Dispatcher', payload.action); - // } - async trackPageChange() { if (!this.tracker) return; this.tracker.trackPageView(redact(window.location.hash)); From 7cebfc1ff12c12ed5f8954ae2d3a66822e49d75d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 28 May 2017 13:19:03 +0100 Subject: [PATCH 04/19] change analytics to set custom url rather than custom title this way exit/entry/page transition will work better Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Analytics.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Analytics.js b/src/Analytics.js index a39fc7250b..f33138bc95 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -34,7 +34,8 @@ class Analytics { async trackPageChange() { if (!this.tracker) return; - this.tracker.trackPageView(redact(window.location.hash)); + this.tracker.setCustomUrl(redact(window.location.href)); + this.tracker.trackPageView(); } async trackEvent(category, action, name) { From fb3187b58ee71cf905af2a741ac7092f19c9d003 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 28 May 2017 13:26:33 +0100 Subject: [PATCH 05/19] change event wording Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/dialogs/BaseDialog.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 8c3e9d687c..f2ea4eda95 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -67,7 +67,7 @@ export default React.createClass({ }, render: function() { - Analytics.trackEvent('Dialog', 'mount', this.props.title); + Analytics.trackEvent('Dialog', this.props.title, 'mount'); const TintableSvg = sdk.getComponent("elements.TintableSvg"); return ( From 7e8123e5fe7289c541574145ad465e9f812124c0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 29 May 2017 14:26:29 +0100 Subject: [PATCH 06/19] move all init/enable/disable logic to Analytics/MatrixChat Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Analytics.js | 57 ++++++++++++++++++++++--- src/components/structures/MatrixChat.js | 3 ++ 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/Analytics.js b/src/Analytics.js index f33138bc95..a660a214c7 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -15,6 +15,7 @@ */ import MatrixClientPeg from './MatrixClientPeg'; +import SdkConfig from './SdkConfig'; function redact(str) { return str.replace(/#\/(room|user)\/(.+)/, "#/$1/"); @@ -23,34 +24,78 @@ function redact(str) { class Analytics { constructor() { this.tracker = null; + this.disabled = true; } - set(tracker) { - this.tracker = tracker; + /** + * Enable Analytics if initialized but disabled + * otherwise try and initalize, no-op if piwik config missing + */ + enable() { + if (this.tracker || this._init()) { + 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.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; + + (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'; + + g.onload = function() { + const tracker = window.Piwik.getTracker(url+'piwik.php', siteId); + console.log('Initialised anonymous analytics'); + self._set(tracker); + }; + + s.parentNode.insertBefore(g, s); + })(); + + return true; + } + + _set(tracker) { + this.tracker = tracker; + this.tracker.discardHashTag(false); this.tracker.enableHeartBeatTimer(); this.tracker.enableLinkTracking(true); } async trackPageChange() { - if (!this.tracker) return; + if (this.disabled) return; this.tracker.setCustomUrl(redact(window.location.href)); this.tracker.trackPageView(); } async trackEvent(category, action, name) { - if (!this.tracker) return; + if (this.disabled) return; this.tracker.trackEvent(category, action, name); } async logout() { - if (!this.tracker) return; + if (this.disabled) return; this.tracker.deleteCookies(); } async login() { // not used currently const cli = MatrixClientPeg.get(); - if (!this.tracker || !cli) return; + if (this.disabled || !cli) return; this.tracker.setUserId(`@${cli.getUserIdLocalpart()}:${cli.getDomain()}`); } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 7cbc4d3ea7..eb52443a55 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -21,6 +21,7 @@ import React from 'react'; import Matrix from "matrix-js-sdk"; import Analytics from "../../Analytics"; +import UserSettingsStore from '../../UserSettingsStore'; import MatrixClientPeg from "../../MatrixClientPeg"; import PlatformPeg from "../../PlatformPeg"; import SdkConfig from "../../SdkConfig"; @@ -190,6 +191,8 @@ module.exports = React.createClass({ componentWillMount: function() { SdkConfig.put(this.props.config); + if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable(); + // Used by _viewRoom before getting state from sync this.firstSyncComplete = false; this.firstSyncPromise = q.defer(); From f5d336103ebbdc1483f5ed55ca620dfa38eae727 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 29 May 2017 14:36:50 +0100 Subject: [PATCH 07/19] add opt out / un opt out toggle Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/UserSettings.js | 31 +++++++++++++++++++++-- src/i18n/strings/en_EN.json | 5 +++- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index f4bf8b18cb..2f8f1ac002 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -28,6 +28,7 @@ const GeminiScrollbar = require('react-gemini-scrollbar'); const Email = require('../../email'); const AddThreepid = require('../../AddThreepid'); const SdkConfig = require('../../SdkConfig'); +import Analytics from '../../Analytics'; import AccessibleButton from '../views/elements/AccessibleButton'; import { _t } from '../../languageHandler'; import * as languageHandler from '../../languageHandler'; @@ -55,7 +56,7 @@ const gHVersionLabel = function(repo, token='') { // Enumerate some simple 'flip a bit' UI settings (if any). // 'id' gives the key name in the im.vector.web.settings account data event // 'label' is how we describe it in the UI. -// Warning: Each "label" string below must be added to i18n/strings/en_EN.json, +// Warning: Each "label" string below must be added to i18n/strings/en_EN.json, // since they will be translated when rendered. const SETTINGS_LABELS = [ { @@ -90,7 +91,7 @@ const SETTINGS_LABELS = [ */ ]; -// Warning: Each "label" string below must be added to i18n/strings/en_EN.json, +// Warning: Each "label" string below must be added to i18n/strings/en_EN.json, // since they will be translated when rendered. const CRYPTO_SETTINGS_LABELS = [ { @@ -722,6 +723,30 @@ module.exports = React.createClass({ ); }, + _onAnalyticsOptOut: function(ev) { + UserSettingsStore.setSyncedSetting('analyticsOptOut', ev.target.checked); + Analytics[ev.target.checked ? 'disable' : 'enable'](); + }, + + _renderAnalyticsControl: function() { + return
+

{ _t('Analytics') }

+
+ {_t('Riot collects anonymous analytics to allow us to improve the application.')} +
+ + +
+
+
; + }, + _renderLabs: function() { // default to enabled if undefined if (this.props.enableLabs === false) return null; @@ -1019,6 +1044,8 @@ module.exports = React.createClass({ {this._renderBulkOptions()} {this._renderBugReport()} + {this._renderAnalyticsControl()} +

{ _t("Advanced") }

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 4105594058..1bf681f136 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -678,5 +678,8 @@ "%(severalUsers)schanged their avatar %(repeats)s times": "%(severalUsers)schanged their avatar %(repeats)s times", "%(oneUser)schanged their avatar %(repeats)s times": "%(oneUser)schanged their avatar %(repeats)s times", "%(severalUsers)schanged their avatar": "%(severalUsers)schanged their avatar", - "%(oneUser)schanged their avatar": "%(oneUser)schanged their avatar" + "%(oneUser)schanged their avatar": "%(oneUser)schanged their avatar", + "Analytics": "Analytics", + "Opt out of analytics": "Opt out of analytics", + "Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application." } From 75e386e0ee0109a77caf924aa231813721dbb4e6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 29 May 2017 14:56:41 +0100 Subject: [PATCH 08/19] fix async behaviour, tracking should maybe use Async Piwik tracker events before piwik loads are lost currently blocking for piwik would be stupid. Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/Analytics.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Analytics.js b/src/Analytics.js index a660a214c7..112d7ab58c 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -32,9 +32,7 @@ class Analytics { * otherwise try and initalize, no-op if piwik config missing */ enable() { - if (this.tracker || this._init()) { - this.disabled = false; - } + if (!this.tracker) this._init(); } /** @@ -71,6 +69,7 @@ class Analytics { } _set(tracker) { + this.disabled = false; this.tracker = tracker; this.tracker.discardHashTag(false); this.tracker.enableHeartBeatTimer(); From 97d0c41d30b078de587c5c5a79e0da31fc91d1ce Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 29 May 2017 15:08:11 +0100 Subject: [PATCH 09/19] fix ugly special casing in generic settings renderer Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/UserSettings.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 2f8f1ac002..c01a7d2cc7 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -97,6 +97,9 @@ const CRYPTO_SETTINGS_LABELS = [ { id: 'blacklistUnverifiedDevices', label: 'Never send encrypted messages to unverified devices from this device', + fn: function(checked) { + MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked); + }, }, // XXX: this is here for documentation; the actual setting is managed via RoomSettings // { @@ -600,7 +603,12 @@ module.exports = React.createClass({ UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) } + onChange={ + (e) => { + UserSettingsStore.setSyncedSetting(setting.id, e.target.checked); + if (setting.fn) setting.fn(e.target.checked); + } + } />