Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
RiotTranslate 2017-05-30 15:04:28 +00:00
commit 8a1f9ca38f
10 changed files with 208 additions and 8 deletions

145
src/Analytics.js Normal file
View file

@ -0,0 +1,145 @@
/*
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.
*/
import { getCurrentLanguage } from './languageHandler';
import MatrixClientPeg from './MatrixClientPeg';
import PlatformPeg from './PlatformPeg';
import SdkConfig from './SdkConfig';
function redact(str) {
return str.replace(/#\/(room|user)\/(.+)/, "#/$1/<redacted>");
}
const customVariables = {
'App Platform': 1,
'App Version': 2,
'User Type': 3,
'Chosen Language': 4,
};
class Analytics {
constructor() {
this._paq = null;
this.disabled = true;
this.firstPage = true;
}
/**
* 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;
}
}
/**
* 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;
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]);
const platform = PlatformPeg.get();
this._setVisitVariable('App Platform', platform.getHumanReadableName());
platform.getAppVersion().then((version) => {
this._setVisitVariable('App Version', version);
}).catch(() => {
this._setVisitVariable('App Version', 'unknown');
});
this._setVisitVariable('Chosen Language', getCurrentLanguage());
(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() {
console.log('Initialised anonymous analytics');
self._paq = window._paq;
};
s.parentNode.insertBefore(g, s);
})();
return true;
}
trackPageChange() {
if (this.disabled) return;
if (this.firstPage) {
// De-duplicate first page
// router seems to hit the fn twice
this.firstPage = false;
return;
}
this._paq.push(['setCustomUrl', redact(window.location.href)]);
this._paq.push(['trackPageView']);
}
trackEvent(category, action, name) {
if (this.disabled) return;
this._paq.push(['trackEvent', category, action, name]);
}
logout() {
if (this.disabled) return;
this._paq.push(['deleteCookies']);
}
login() { // not used currently
const cli = MatrixClientPeg.get();
if (this.disabled || !cli) return;
this._paq.push(['setUserId', `@${cli.getUserIdLocalpart()}:${cli.getDomain()}`]);
}
_setVisitVariable(key, value) {
this._paq.push(['setCustomVariable', customVariables[key], key, value, 'visit']);
}
setGuest(guest) {
if (this.disabled) return;
this._setVisitVariable('User Type', guest ? 'Guest' : 'Logged In');
}
}
if (!global.mxAnalytics) {
global.mxAnalytics = new Analytics();
}
module.exports = global.mxAnalytics;

View file

@ -29,6 +29,11 @@ export default class BasePlatform {
this.errorDidOccur = false; this.errorDidOccur = false;
} }
// Used primarily for Analytics
getHumanReadableName(): string {
return 'Base Platform';
}
setNotificationCount(count: number) { setNotificationCount(count: number) {
this.notificationCount = count; this.notificationCount = count;
} }

View file

@ -19,6 +19,7 @@ import q from 'q';
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import Analytics from './Analytics';
import Notifier from './Notifier'; import Notifier from './Notifier';
import UserActivity from './UserActivity'; import UserActivity from './UserActivity';
import Presence from './Presence'; import Presence from './Presence';
@ -276,6 +277,8 @@ export function initRtsClient(url) {
export function setLoggedIn(credentials) { export function setLoggedIn(credentials) {
credentials.guest = Boolean(credentials.guest); credentials.guest = Boolean(credentials.guest);
Analytics.setGuest(credentials.guest);
console.log( console.log(
"setLoggedIn: mxid:", credentials.userId, "setLoggedIn: mxid:", credentials.userId,
"deviceId:", credentials.deviceId, "deviceId:", credentials.deviceId,
@ -403,6 +406,7 @@ export function onLoggedOut() {
} }
function _clearLocalStorage() { function _clearLocalStorage() {
Analytics.logout();
if (!window.localStorage) { if (!window.localStorage) {
return; return;
} }

View file

@ -19,6 +19,7 @@ limitations under the License.
var React = require('react'); var React = require('react');
var ReactDOM = require('react-dom'); var ReactDOM = require('react-dom');
import Analytics from './Analytics';
import sdk from './index'; import sdk from './index';
const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
@ -104,6 +105,9 @@ class ModalManager {
} }
createDialog(Element, props, className) { createDialog(Element, props, className) {
if (props && props.title) {
Analytics.trackEvent('Modal', props.title, 'createDialog');
}
return this.createDialogAsync((cb) => {cb(Element);}, props, className); return this.createDialogAsync((cb) => {cb(Element);}, props, className);
} }

View file

@ -18,6 +18,7 @@ limitations under the License.
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import PlatformPeg from './PlatformPeg'; import PlatformPeg from './PlatformPeg';
import TextForEvent from './TextForEvent'; import TextForEvent from './TextForEvent';
import Analytics from './Analytics';
import Avatar from './Avatar'; import Avatar from './Avatar';
import dis from './dispatcher'; import dis from './dispatcher';
import sdk from './index'; import sdk from './index';
@ -121,6 +122,9 @@ const Notifier = {
setEnabled: function(enable, callback) { setEnabled: function(enable, callback) {
const plaf = PlatformPeg.get(); const plaf = PlatformPeg.get();
if (!plaf) return; if (!plaf) return;
Analytics.trackEvent('Notifier', 'Set Enabled', enable);
// make sure that we persist the current setting audio_enabled setting // make sure that we persist the current setting audio_enabled setting
// before changing anything // before changing anything
if (global.localStorage) { if (global.localStorage) {
@ -199,6 +203,8 @@ const Notifier = {
setToolbarHidden: function(hidden, persistent = true) { setToolbarHidden: function(hidden, persistent = true) {
this.toolbarHidden = hidden; this.toolbarHidden = hidden;
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden);
// XXX: why are we dispatching this here? // XXX: why are we dispatching this here?
// this is nothing to do with notifier_enabled // this is nothing to do with notifier_enabled
dis.dispatch({ dis.dispatch({

View file

@ -20,6 +20,8 @@ import q from 'q';
import React from 'react'; import React from 'react';
import Matrix from "matrix-js-sdk"; import Matrix from "matrix-js-sdk";
import Analytics from "../../Analytics";
import UserSettingsStore from '../../UserSettingsStore';
import MatrixClientPeg from "../../MatrixClientPeg"; import MatrixClientPeg from "../../MatrixClientPeg";
import PlatformPeg from "../../PlatformPeg"; import PlatformPeg from "../../PlatformPeg";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
@ -189,6 +191,8 @@ module.exports = React.createClass({
componentWillMount: function() { componentWillMount: function() {
SdkConfig.put(this.props.config); SdkConfig.put(this.props.config);
if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable();
// Used by _viewRoom before getting state from sync // Used by _viewRoom before getting state from sync
this.firstSyncComplete = false; this.firstSyncComplete = false;
this.firstSyncPromise = q.defer(); this.firstSyncPromise = q.defer();
@ -1002,6 +1006,7 @@ module.exports = React.createClass({
if (this.props.onNewScreen) { if (this.props.onNewScreen) {
this.props.onNewScreen(screen); this.props.onNewScreen(screen);
} }
Analytics.trackPageChange();
}, },
onAliasClick: function(event, alias) { onAliasClick: function(event, alias) {

View file

@ -28,6 +28,7 @@ const GeminiScrollbar = require('react-gemini-scrollbar');
const Email = require('../../email'); const Email = require('../../email');
const AddThreepid = require('../../AddThreepid'); const AddThreepid = require('../../AddThreepid');
const SdkConfig = require('../../SdkConfig'); const SdkConfig = require('../../SdkConfig');
import Analytics from '../../Analytics';
import AccessibleButton from '../views/elements/AccessibleButton'; import AccessibleButton from '../views/elements/AccessibleButton';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import * as languageHandler from '../../languageHandler'; import * as languageHandler from '../../languageHandler';
@ -90,12 +91,25 @@ const SETTINGS_LABELS = [
*/ */
]; ];
const ANALYTICS_SETTINGS_LABELS = [
{
id: 'analyticsOptOut',
label: 'Opt out of analytics',
fn: function(checked) {
Analytics[checked ? 'disable' : 'enable']();
},
},
];
// 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. // since they will be translated when rendered.
const CRYPTO_SETTINGS_LABELS = [ const CRYPTO_SETTINGS_LABELS = [
{ {
id: 'blacklistUnverifiedDevices', id: 'blacklistUnverifiedDevices',
label: 'Never send encrypted messages to unverified devices from this device', 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 // XXX: this is here for documentation; the actual setting is managed via RoomSettings
// { // {
@ -599,7 +613,12 @@ module.exports = React.createClass({
<input id={ setting.id } <input id={ setting.id }
type="checkbox" type="checkbox"
defaultChecked={ this._syncedSettings[setting.id] } defaultChecked={ this._syncedSettings[setting.id] }
onChange={ (e) => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) } onChange={
(e) => {
UserSettingsStore.setSyncedSetting(setting.id, e.target.checked);
if (setting.fn) setting.fn(e.target.checked);
}
}
/> />
<label htmlFor={ setting.id }> <label htmlFor={ setting.id }>
{ _t(setting.label) } { _t(setting.label) }
@ -675,7 +694,6 @@ module.exports = React.createClass({
}, },
_renderLocalSetting: function(setting) { _renderLocalSetting: function(setting) {
const client = MatrixClientPeg.get();
return <div className="mx_UserSettings_toggle" key={ setting.id }> return <div className="mx_UserSettings_toggle" key={ setting.id }>
<input id={ setting.id } <input id={ setting.id }
type="checkbox" type="checkbox"
@ -683,9 +701,7 @@ module.exports = React.createClass({
onChange={ onChange={
(e) => { (e) => {
UserSettingsStore.setLocalSetting(setting.id, e.target.checked); UserSettingsStore.setLocalSetting(setting.id, e.target.checked);
if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly if (setting.fn) setting.fn(e.target.checked);
client.setGlobalBlacklistUnverifiedDevices(e.target.checked);
}
} }
} }
/> />
@ -722,6 +738,16 @@ module.exports = React.createClass({
); );
}, },
_renderAnalyticsControl: function() {
return <div>
<h3>{ _t('Analytics') }</h3>
<div className="mx_UserSettings_section">
{_t('Riot collects anonymous analytics to allow us to improve the application.')}
{ANALYTICS_SETTINGS_LABELS.map( this._renderLocalSetting )}
</div>
</div>;
},
_renderLabs: function() { _renderLabs: function() {
// default to enabled if undefined // default to enabled if undefined
if (this.props.enableLabs === false) return null; if (this.props.enableLabs === false) return null;
@ -1019,6 +1045,8 @@ module.exports = React.createClass({
{this._renderBulkOptions()} {this._renderBulkOptions()}
{this._renderBugReport()} {this._renderBugReport()}
{this._renderAnalyticsControl()}
<h3>{ _t("Advanced") }</h3> <h3>{ _t("Advanced") }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">

View file

@ -33,7 +33,7 @@ const TRUNCATE_QUERY_LIST = 40;
module.exports = React.createClass({ module.exports = React.createClass({
displayName: "ChatInviteDialog", displayName: "ChatInviteDialog",
propTypes: { propTypes: {
title: React.PropTypes.string, title: React.PropTypes.string.isRequired,
description: React.PropTypes.oneOfType([ description: React.PropTypes.oneOfType([
React.PropTypes.element, React.PropTypes.element,
React.PropTypes.string, React.PropTypes.string,

View file

@ -674,6 +674,9 @@
"%(oneUser)schanged their avatar %(repeats)s times": "%(oneUser)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", "%(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.",
"Please select the destination room for this message": "Please select the destination room for this message", "Please select the destination room for this message": "Please select the destination room for this message",
"Passphrases must match": "Passphrases must match", "Passphrases must match": "Passphrases must match",
"Passphrase must not be empty": "Passphrase must not be empty", "Passphrase must not be empty": "Passphrase must not be empty",