Remove “reportUserEvent” calls, remnants of analytics integration

This commit is contained in:
Ben Gotow 2017-12-31 11:39:52 -05:00
parent 89bd1909ae
commit 40caf341b4
26 changed files with 9 additions and 649 deletions

View file

@ -297,7 +297,6 @@ class TemplateStore extends MailspringStore {
templateBody + signature,
session.draft().body
);
Actions.recordUserEvent('Email Template Inserted');
session.changes.add({ body: draftHtml });
}
});

View file

@ -66,10 +66,6 @@ class TranslateButton extends React.Component {
const draftHtml = this.props.draft.body;
const text = QuotedHTMLTransformer.removeQuotedHTML(draftHtml);
Actions.recordUserEvent('Email Translated', {
language: YandexLanguages[lang],
});
const queryParams = new URLSearchParams();
queryParams.set('key', YandexTranslationKey);
queryParams.set('lang', YandexLanguages[lang]);

View file

@ -1,5 +1,5 @@
import { shell } from 'electron';
import { Actions, React, PropTypes } from 'mailspring-exports';
import { React, PropTypes } from 'mailspring-exports';
import { RetinaImg, KeyCommandsRegion } from 'mailspring-component-kit';
import GithubStore from './github-store';
@ -140,7 +140,6 @@ export default class ViewOnGithubButton extends React.Component {
* request.
*/
_openLink = () => {
Actions.recordUserEvent('Github Thread Opened', { pageUrl: this.state.link });
if (this.state.link) {
shell.openExternal(this.state.link);
}

View file

@ -1,6 +1,6 @@
import { shell } from 'electron';
import { RetinaImg } from 'mailspring-component-kit';
import { Actions, React, ReactDOM, PropTypes } from 'mailspring-exports';
import { React, ReactDOM, PropTypes } from 'mailspring-exports';
import OnboardingActions from '../onboarding-actions';
import { finalizeAndValidateAccount } from '../onboarding-helpers';
@ -120,11 +120,6 @@ const CreatePageForForm = FormComponent => {
OnboardingActions.finishAndAddAccount(validated);
})
.catch(err => {
Actions.recordUserEvent('Email Account Auth Failed', {
errorMessage: err.message,
provider: account.provider,
});
const errorFieldNames = [];
if (err.message.includes('Authentication Error')) {

View file

@ -1,5 +1,5 @@
import { ipcRenderer, shell, clipboard } from 'electron';
import { React, PropTypes, Actions } from 'mailspring-exports';
import { React, PropTypes } from 'mailspring-exports';
import { RetinaImg } from 'mailspring-component-kit';
import FormErrorMessage from './form-error-message';
@ -75,10 +75,6 @@ export default class OAuthSignInPage extends React.Component {
_handleError(err) {
this.setState({ authStage: 'error', errorMessage: err.message });
AppEnv.reportError(err);
Actions.recordUserEvent('Email Account Auth Failed', {
errorMessage: err.message,
provider: 'gmail',
});
}
startPollingForResponse() {

View file

@ -1,4 +1,4 @@
import { AccountStore, Account, Actions, IdentityStore } from 'mailspring-exports';
import { AccountStore, Account, IdentityStore } from 'mailspring-exports';
import { ipcRenderer } from 'electron';
import MailspringStore from 'mailspring-store';
@ -136,15 +136,8 @@ class OnboardingStore extends MailspringStore {
AppEnv.displayWindow();
Actions.recordUserEvent('Email Account Auth Succeeded', {
provider: account.provider,
});
if (isFirstAccount) {
this._onMoveToPage('initial-preferences');
Actions.recordUserEvent('First Account Linked', {
provider: account.provider,
});
} else {
// let them see the "success" screen for a moment
// before the window is closed.

View file

@ -6,15 +6,6 @@ export default class WelcomePage extends React.Component {
static displayName = 'WelcomePage';
_onContinue = () => {
// We don't have a NylasId yet and therefore can't track the "Welcome
// Page Finished" event.
//
// If a user already has a Nylas ID and gets to this page (which
// happens if they sign out of all of their accounts), then it would
// properly fire. This is a rare case though and we don't want
// analytics users thinking it's part of the full funnel.
//
// Actions.recordUserEvent('Welcome Page Finished');
OnboardingActions.moveToPage('tutorial');
};

View file

@ -75,12 +75,6 @@ class SendLaterButton extends Component {
}
this.setState({ saving: true });
const sendInSec = Math.round((new Date(sendLaterDate).valueOf() - Date.now()) / 1000);
Actions.recordUserEvent('Draft Send Later', {
timeInSec: sendInSec,
timeInLog10Sec: Math.log10(sendInSec),
label: dateLabel,
});
}
this.props.session.changes.addPluginMetadata(PLUGIN_ID, {

View file

@ -30,13 +30,6 @@ async function incrementMetadataUse(model, expiration) {
return false;
}
}
if (expiration) {
const seconds = Math.round((new Date(expiration).getTime() - Date.now()) / 1000);
Actions.recordUserEvent('Set Reminder', {
seconds: seconds,
secondsLog10: Math.log10(seconds),
});
}
return true;
}

View file

@ -1,6 +1,5 @@
import _ from 'underscore';
import {
Actions,
Thread,
DatabaseStore,
SearchQueryParser,
@ -119,31 +118,8 @@ class SearchQuerySubscription extends MutableQuerySubscription {
return;
}
let timeToFirstServerResults = null;
let timeToFirstThreadSelected = null;
const timeInsideSearch = Math.round((Date.now() - this._searchStartedAt) / 1000);
const numItems = this._focusedThreadCount;
const didSelectAnyThreads = numItems > 0;
// Not Implemented
if (this._firstThreadSelectedAt) {
timeToFirstThreadSelected = Math.round(
(this._firstThreadSelectedAt - this._searchStartedAt) / 1000
);
}
if (this._resultsReceivedAt) {
timeToFirstServerResults = Math.round(
(this._resultsReceivedAt - this._searchStartedAt) / 1000
);
}
const data = {
numItems,
timeInsideSearch,
didSelectAnyThreads,
timeToFirstServerResults,
timeToFirstThreadSelected,
};
Actions.recordUserEvent('Search Performed', data);
this.resetData();
}

View file

@ -29,20 +29,6 @@ class SnoozeStore extends MailspringStore {
this.unsubscribers.forEach(unsub => unsub());
}
_recordSnoozeEvent(threads, snoozeDate, label) {
try {
const timeInSec = Math.round((new Date(snoozeDate).valueOf() - Date.now()) / 1000);
Actions.recordUserEvent('Threads Snoozed', {
timeInSec: timeInSec,
timeInLog10Sec: Math.log10(timeInSec),
label: label,
numItems: threads.length,
});
} catch (e) {
// Do nothing
}
}
_onSnoozeThreads = async (threads, snoozeDate, label) => {
try {
// ensure the user is authorized to use this feature
@ -52,9 +38,6 @@ class SnoozeStore extends MailspringStore {
iconUrl: 'mailspring://thread-snooze/assets/ic-snooze-modal@2x.png',
});
// log to analytics
this._recordSnoozeEvent(threads, snoozeDate, label);
// move the threads to the snoozed folder
await moveThreads(threads, {
snooze: true,

View file

@ -1,291 +0,0 @@
/* eslint no-param-reassign: 0 */
const assert = require('assert');
const crypto = require('crypto');
const validate = require('@segment/loosely-validate-event');
const debug = require('debug')('analytics-node');
const version = `3.0.0`;
// BG: Dependencies of analytics-node I lifted in
// https://github.com/segmentio/crypto-token/blob/master/lib/index.js
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
function uid(length, fn) {
const str = bytes => {
const res = [];
for (let i = 0; i < bytes.length; i++) {
res.push(chars[bytes[i] % chars.length]);
}
return res.join('');
};
if (typeof length === 'function') {
fn = length;
length = null;
}
if (!length) {
length = 10;
}
if (!fn) {
return str(crypto.randomBytes(length));
}
crypto.randomBytes(length, (err, bytes) => {
if (err) {
fn(err);
return;
}
fn(null, str(bytes));
});
return null;
}
// https://github.com/stephenmathieson/remove-trailing-slash/blob/master/index.js
function removeSlash(str) {
return String(str).replace(/\/+$/, '');
}
const setImmediate = global.setImmediate || process.nextTick.bind(process);
const noop = () => {};
export default class Analytics {
/**
* Initialize a new `Analytics` with your Segment project's `writeKey` and an
* optional dictionary of `options`.
*
* @param {String} writeKey
* @param {Object} [options] (optional)
* @property {Number} flushAt (default: 20)
* @property {Number} flushInterval (default: 10000)
* @property {String} host (default: 'https://api.segment.io')
*/
constructor(writeKey, options) {
options = options || {};
assert(writeKey, "You must pass your Segment project's write key.");
this.queue = [];
this.writeKey = writeKey;
this.host = removeSlash(options.host || 'https://api.segment.io');
this.flushAt = Math.max(options.flushAt, 1) || 20;
this.flushInterval = options.flushInterval || 10000;
this.flushed = false;
}
/**
* Send an identify `message`.
*
* @param {Object} message
* @param {Function} [callback] (optional)
* @return {Analytics}
*/
identify(message, callback) {
validate(message, 'identify');
this.enqueue('identify', message, callback);
return this;
}
/**
* Send a group `message`.
*
* @param {Object} message
* @param {Function} [callback] (optional)
* @return {Analytics}
*/
group(message, callback) {
validate(message, 'group');
this.enqueue('group', message, callback);
return this;
}
/**
* Send a track `message`.
*
* @param {Object} message
* @param {Function} [callback] (optional)
* @return {Analytics}
*/
track(message, callback) {
validate(message, 'track');
this.enqueue('track', message, callback);
return this;
}
/**
* Send a page `message`.
*
* @param {Object} message
* @param {Function} [callback] (optional)
* @return {Analytics}
*/
page(message, callback) {
validate(message, 'page');
this.enqueue('page', message, callback);
return this;
}
/**
* Send a screen `message`.
*
* @param {Object} message
* @param {Function} fn (optional)
* @return {Analytics}
*/
screen(message, callback) {
validate(message, 'screen');
this.enqueue('screen', message, callback);
return this;
}
/**
* Send an alias `message`.
*
* @param {Object} message
* @param {Function} [callback] (optional)
* @return {Analytics}
*/
alias(message, callback) {
validate(message, 'alias');
this.enqueue('alias', message, callback);
return this;
}
/**
* Add a `message` of type `type` to the queue and
* check whether it should be flushed.
*
* @param {String} type
* @param {Object} message
* @param {Functino} [callback] (optional)
* @api private
*/
enqueue(type, message, callback) {
callback = callback || noop;
message = Object.assign({}, message);
message.type = type;
message.context = Object.assign(
{
library: {
name: 'analytics-node',
version,
},
},
message.context
);
message._metadata = Object.assign(
{
nodeVersion: process.versions.node,
},
message._metadata
);
if (!message.timestamp) {
message.timestamp = new Date();
}
if (!message.messageId) {
message.messageId = `node-${uid(32)}`;
}
debug('%s: %o', type, message);
this.queue.push({ message, callback });
if (!this.flushed) {
this.flushed = true;
this.flush();
return;
}
if (this.queue.length >= this.flushAt) {
this.flush();
}
if (this.flushInterval && !this.timer) {
this.timer = setTimeout(this.flush.bind(this), this.flushInterval);
}
}
/**
* Flush the current queue
*
* @param {Function} [callback] (optional)
* @return {Analytics}
*/
async flush(callback) {
callback = callback || noop;
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
if (!this.queue.length) {
setImmediate(callback);
return;
}
const items = this.queue.splice(0, this.flushAt);
const callbacks = items.map(item => item.callback);
const messages = items.map(item => item.message);
const data = {
batch: messages,
timestamp: new Date(),
sentAt: new Date(),
};
debug('flush: %o', data);
const options = {
body: JSON.stringify(data),
credentials: 'include',
headers: new Headers(),
method: 'POST',
};
options.headers.set('Accept', 'application/json');
options.headers.set('Authorization', `Basic ${btoa(`${this.writeKey}:`)}`);
options.headers.set('Content-Type', 'application/json');
const runCallbacks = err => {
callbacks.forEach(cb => cb(err));
callback(err, data);
debug('flushed: %o', data);
};
let resp = null;
try {
resp = await fetch(`${this.host}/v1/batch`, options);
} catch (err) {
runCallbacks(err, null);
return;
}
if (!resp.ok) {
runCallbacks(new Error(`${resp.statusCode}: ${resp.statusText}`), null);
return;
}
const body = await resp.json();
if (body.error) {
runCallbacks(new Error(body.error.message), null);
return;
}
runCallbacks(null, {
status: resp.statusCode,
body: body,
ok: true,
});
}
}

View file

@ -1 +0,0 @@
This folder contains a modified version of analytics-node. The original version uses `superagent`, which is both unnecessary (since we have both Browser and Node APIs and can use `fetch` and also added ~2.1MB of JavaScript to Mailspring.)

View file

@ -1,154 +0,0 @@
import _ from 'underscore';
import MailspringStore from 'mailspring-store';
import {
IdentityStore,
Actions,
AccountStore,
FocusedPerspectiveStore,
MailspringAPIRequest,
} from 'mailspring-exports';
import AnalyticsSink from '../analytics-electron';
/**
* We wait 15 seconds to give the alias time to register before we send
* any events.
*/
const DEBOUNCE_TIME = 5 * 1000;
class AnalyticsStore extends MailspringStore {
activate() {
// Allow requests to be grouped together if they're fired back-to-back,
// but generally report each event as it happens. This segment library
// is intended for a server where the user doesn't quit...
this.analytics = new AnalyticsSink('mailspring', {
host: `${MailspringAPIRequest.rootURLForServer('identity')}/api/s`,
flushInterval: 500,
flushAt: 5,
});
this.launchTime = Date.now();
const identifySoon = _.debounce(this.identify, DEBOUNCE_TIME);
identifySoon();
// also ping the server every hour to make sure someone running
// the app for days has up-to-date traits.
setInterval(identifySoon, 60 * 60 * 1000); // 60 min
this.listenTo(IdentityStore, identifySoon);
this.listenTo(Actions.recordUserEvent, (eventName, eventArgs) => {
this.track(eventName, eventArgs);
});
}
// Properties applied to all events (only).
eventState() {
// Get a bit of context about the current perspective
// so we can assess usage of unified inbox, etc.
const perspective = FocusedPerspectiveStore.current();
let currentProvider = null;
if (perspective && perspective.accountIds.length > 1) {
currentProvider = 'Unified';
} else {
// Warning: when you auth a new account there's a single moment where the account cannot be found
const account = perspective
? AccountStore.accountForId(perspective.accountIds[0])
: AccountStore.accounts()[0];
currentProvider = account && account.displayProvider();
}
return {
currentProvider,
};
}
// Properties applied to all events and all people during an identify.
superTraits() {
const theme = AppEnv.themes ? AppEnv.themes.getActiveTheme() : null;
return {
version: AppEnv.getVersion().split('-')[0],
platform: process.platform,
activeTheme: theme ? theme.name : null,
workspaceMode: AppEnv.config.get('core.workspace.mode'),
};
}
baseTraits() {
return Object.assign({}, this.superTraits(), {
firstDaySeen: this.firstDaySeen(),
timeSinceLaunch: (Date.now() - this.launchTime) / 1000,
accountCount: AccountStore.accounts().length,
providers: AccountStore.accounts().map(a => a.displayProvider()),
});
}
personalTraits() {
const identity = IdentityStore.identity();
if (!(identity && identity.id)) {
return {};
}
return {
email: identity.emailAddress,
firstName: identity.firstName,
lastName: identity.lastName,
};
}
track(eventName, eventArgs = {}) {
// if (AppEnv.inDevMode()) { return }
const identity = IdentityStore.identity();
if (!(identity && identity.id)) {
return;
}
this.analytics.track({
event: eventName,
userId: identity.id,
properties: Object.assign({}, eventArgs, this.eventState(), this.superTraits()),
});
}
firstDaySeen() {
let firstDaySeen = AppEnv.config.get('firstDaySeen');
if (!firstDaySeen) {
const [y, m, d] = new Date().toISOString().split(/[-|T]/);
firstDaySeen = `${m}/${d}/${y}`;
AppEnv.config.set('firstDaySeen', firstDaySeen);
}
return firstDaySeen;
}
identify = () => {
if (!AppEnv.isMainWindow()) {
return;
}
const identity = IdentityStore.identity();
if (!(identity && identity.id)) {
return;
}
this.analytics.identify({
userId: identity.id,
traits: this.baseTraits(),
integrations: { All: true },
});
// Ensure we never send PI anywhere but Mixpanel
this.analytics.identify({
userId: identity.id,
traits: Object.assign({}, this.baseTraits(), this.personalTraits()),
integrations: {
All: false,
Mixpanel: true,
},
});
};
}
export default new AnalyticsStore();

View file

@ -1,5 +0,0 @@
import AnalyticsStore from './analytics-store';
export function activate() {
AnalyticsStore.activate();
}

View file

@ -1,14 +0,0 @@
{
"name": "analytics",
"version": "0.1.0",
"main": "./lib/main",
"description": "Analytics",
"license": "Proprietary",
"private": true,
"windowTypes": {
"default": true
},
"engines": {
"mailspring": "*"
}
}

View file

@ -54,7 +54,7 @@ export default class ThreadSharingPopover extends React.Component {
}
_onToggleShared = async () => {
const { thread, accountId } = this.props;
const { thread } = this.props;
const { shared } = this.state;
this.setState({ saving: true });
@ -64,9 +64,6 @@ export default class ThreadSharingPopover extends React.Component {
return;
}
if (!shared === true) {
Actions.recordUserEvent('Thread Sharing Enabled', { accountId, threadId: thread.id });
}
Actions.queueTask(
SyncbackMetadataTask.forSaving({
model: thread,

View file

@ -27,7 +27,7 @@ describe "ActionBridge", ->
it "should not rebroadcast mainWindow actions since it is the main window", ->
spyOn(@bridge, 'onRebroadcast')
testAction = Actions.recordUserEvent
testAction = Actions.queueTask
testAction('bla')
expect(@bridge.onRebroadcast).not.toHaveBeenCalled()
@ -57,7 +57,7 @@ describe "ActionBridge", ->
it "should rebroadcast mainWindow actions", ->
spyOn(@bridge, 'onRebroadcast')
testAction = Actions.recordUserEvent
testAction = Actions.queueTask
testAction('bla')
expect(@bridge.onRebroadcast).toHaveBeenCalled()

View file

@ -1,7 +1,6 @@
import {
React,
PropTypes,
Actions,
MailspringAPIRequest,
APIError,
FeatureUsageStore,
@ -87,10 +86,8 @@ export default class MetadataComposerToggleButton extends React.Component {
}
_onClick = async () => {
const { pluginName, pluginId } = this.props;
const { pluginId } = this.props;
let nextEnabled = !this._isEnabled();
const dir = nextEnabled ? 'Enabled' : 'Disabled';
if (this.state.pending) {
return;
}
@ -114,7 +111,6 @@ export default class MetadataComposerToggleButton extends React.Component {
}
}
Actions.recordUserEvent(`${pluginName} ${dir}`);
AppEnv.config.set(this._configKey(), nextEnabled);
this._setEnabled(nextEnabled);
};

View file

@ -468,11 +468,6 @@ class Actions {
*/
static pushSheet = ActionScopeWindow;
/*
Public: Publish a user event to any analytics services linked to N1.
*/
static recordUserEvent = ActionScopeMainWindow;
static addMailRule = ActionScopeWindow;
static reorderMailRule = ActionScopeWindow;
static updateMailRule = ActionScopeWindow;

View file

@ -182,7 +182,6 @@ class DraftStore extends MailspringStore {
};
_onComposeReply = ({ thread, threadId, message, messageId, popout, type, behavior }) => {
Actions.recordUserEvent('Draft Created', { type });
return Promise.props(this._modelifyContext({ thread, threadId, message, messageId }))
.then(({ message: m, thread: t }) => {
return DraftFactory.createOrUpdateDraftForReply({ message: m, thread: t, type, behavior });
@ -193,7 +192,6 @@ class DraftStore extends MailspringStore {
};
_onComposeForward = async ({ thread, threadId, message, messageId, popout }) => {
Actions.recordUserEvent('Draft Created', { type: 'forward' });
return Promise.props(this._modelifyContext({ thread, threadId, message, messageId }))
.then(({ thread: t, message: m }) => {
return DraftFactory.createDraftForForward({ thread: t, message: m });
@ -275,7 +273,6 @@ class DraftStore extends MailspringStore {
};
_onPopoutBlankDraft = async () => {
Actions.recordUserEvent('Draft Created', { type: 'new' });
const draft = await DraftFactory.createDraft();
const { headerMessageId } = await this._finalizeAndPersistNewMessage(draft);
await this._onPopoutDraft(headerMessageId, { newDraft: true });

View file

@ -103,12 +103,8 @@ class WorkspaceStore extends MailspringStore
throw new Error("Actions.toggleWorkspaceLocationHidden - pass a WorkspaceStore.Location")
if @_hiddenLocations[location.id]
if location is @Location.MessageListSidebar
Actions.recordUserEvent("Sidebar Opened")
delete @_hiddenLocations[location.id]
else
if location is @Location.MessageListSidebar
Actions.recordUserEvent("Sidebar Closed")
@_hiddenLocations[location.id] = location
AppEnv.config.set('core.workspace.hiddenLocations', @_hiddenLocations)

View file

@ -44,17 +44,4 @@ export default class ChangeStarredTask extends ChangeMailTask {
task.starred = !this.starred;
return task;
}
recordUserEvent() {
if (this.source === 'Mail Rules') {
return;
}
const eventName = this.starred ? 'Starred' : 'Unstarred';
Actions.recordUserEvent(`Threads ${eventName}`, {
source: this.source,
numThreads: this.threadIds.length,
description: this.description(),
isUndo: this.isUndo,
});
}
}

View file

@ -86,7 +86,6 @@ export default class SendDraftTask extends Task {
}
onSuccess() {
Actions.recordUserEvent('Draft Sent');
Actions.draftDeliverySucceeded({
headerMessageId: this.draft.headerMessageId,
accountId: this.draft.accountId,
@ -143,9 +142,6 @@ export default class SendDraftTask extends Task {
errorMessage,
errorDetail,
});
Actions.recordUserEvent('Draft Sending Errored', {
key: key,
});
}
// note - this code must match what is used for send-later!

View file

@ -191,7 +191,6 @@ lazyLoad(`SanitizeTransformer`, 'services/sanitize-transformer');
lazyLoad(`QuotedHTMLTransformer`, 'services/quoted-html-transformer');
lazyLoad(`InlineStyleTransformer`, 'services/inline-style-transformer');
lazyLoad(`SearchableComponentMaker`, 'searchable-components/searchable-component-maker');
lazyLoad(`BatteryStatusManager`, 'services/battery-status-manager');
// Errors
lazyLoadWithGetter(`APIError`, () => require('../flux/errors').APIError);

View file

@ -1,53 +0,0 @@
import moment from 'moment-timezone';
import Actions from '../flux/actions';
class BatteryStatusManager {
constructor() {
this._callbacks = [];
this._battery = null;
this._lastChangeTime = Date.now();
this.activate();
}
async activate() {
if (this._battery) {
return;
}
this._battery = await navigator.getBattery();
this._battery.addEventListener('chargingchange', this._onChargingChange);
}
deactivate() {
if (!this._battery) {
return;
}
this._battery.removeEventListener('chargingchange', this._onChargingChange);
this._battery = null;
}
_onChargingChange = () => {
const changeTime = Date.now();
Actions.recordUserEvent('Battery State Changed', {
oldState: this.isBatteryCharging() ? 'battery' : 'ac',
oldStateDuration: Math.min(
changeTime - this._lastChangeTime,
moment.duration(12, 'hours').asMilliseconds()
),
});
this._lastChangeTime = changeTime;
this._callbacks.forEach(cb => cb());
};
onChange(callback) {
this._callbacks.push(callback);
}
isBatteryCharging() {
if (!this._battery) {
return false;
}
return this._battery.charging;
}
}
export default new BatteryStatusManager();