From c4753197eec7d96eaf2fb8460ec9a5723920b239 Mon Sep 17 00:00:00 2001 From: Evan Morikawa Date: Tue, 7 Jun 2016 12:53:05 -0700 Subject: [PATCH] fix(analytics): improved analytics --- .../composer-templates/lib/template-store.es6 | 1 + .../composer-translate/lib/main.jsx | 4 ++++ internal_packages/composer/lib/main.es6 | 6 +++-- .../lib/view-on-github-button.cjsx | 1 + .../lib/headers/account-error-header.jsx | 8 ++++++- .../lib/decorators/create-page-for-form.jsx | 2 +- .../onboarding/lib/onboarding-store.es6 | 20 +++++++++++++---- .../onboarding/lib/page-authenticate.jsx | 2 +- .../lib/tabs/preferences-identity.jsx | 15 +++++++++---- .../lib/search-query-subscription.es6 | 6 ++--- .../thread-search/lib/search-store.coffee | 1 - .../thread-snooze/lib/snooze-store.es6 | 11 +++++----- .../lib/nylas-sync-worker-pool.coffee | 10 ++++----- src/components/evented-iframe.cjsx | 2 +- src/flux/stores/identity-store.es6 | 22 ++++++++++++++++--- src/pro | 2 +- 16 files changed, 81 insertions(+), 32 deletions(-) diff --git a/internal_packages/composer-templates/lib/template-store.es6 b/internal_packages/composer-templates/lib/template-store.es6 index 2a18e7f5f..228089942 100644 --- a/internal_packages/composer-templates/lib/template-store.es6 +++ b/internal_packages/composer-templates/lib/template-store.es6 @@ -267,6 +267,7 @@ class TemplateStore extends NylasStore { const signature = sigIndex > -1 ? draftContents.slice(sigIndex) : ''; const draftHtml = QuotedHTMLTransformer.appendQuotedHTML(templateBody + signature, session.draft().body); + Actions.recordUserEvent("Email Template Inserted") session.changes.add({body: draftHtml}); } }); diff --git a/internal_packages/composer-translate/lib/main.jsx b/internal_packages/composer-translate/lib/main.jsx index d3cf4ab9c..0cb0fa71f 100644 --- a/internal_packages/composer-translate/lib/main.jsx +++ b/internal_packages/composer-translate/lib/main.jsx @@ -70,6 +70,10 @@ class TranslateButton extends React.Component { const draftHtml = this.props.draft.body; const text = QuotedHTMLTransformer.removeQuotedHTML(draftHtml); + Actions.recordUserEvent("Email Translated", { + language: YandexLanguages[lang], + }) + const query = { key: YandexTranslationKey, lang: YandexLanguages[lang], diff --git a/internal_packages/composer/lib/main.es6 b/internal_packages/composer/lib/main.es6 index d61cc2be3..eb88b70c0 100644 --- a/internal_packages/composer/lib/main.es6 +++ b/internal_packages/composer/lib/main.es6 @@ -46,9 +46,11 @@ class ComposerWithWindowProps extends React.Component { _onDraftReady = () => { this.refs.composer.focus().then(() => { - const totalTime = NylasEnv.perf.stop("Popout Draft"); + const timeInMs = NylasEnv.perf.stop("Popout Draft"); if (!NylasEnv.inDevMode() && !NylasEnv.inSpecMode()) { - Actions.recordUserEvent("Popout Composer Time", {totalTime}) + if (timeInMs && timeInMs <= 4000) { + Actions.recordUserEvent("Composer Popout Timed", {timeInMs}) + } } NylasEnv.displayWindow(); diff --git a/internal_packages/message-view-on-github/lib/view-on-github-button.cjsx b/internal_packages/message-view-on-github/lib/view-on-github-button.cjsx index 8835a6ccb..afe2724c7 100644 --- a/internal_packages/message-view-on-github/lib/view-on-github-button.cjsx +++ b/internal_packages/message-view-on-github/lib/view-on-github-button.cjsx @@ -132,6 +132,7 @@ class ViewOnGithubButton extends React.Component # also queue a {Task} to eventually perform a mutating API POST or PUT # request. _openLink: => + Actions.recordUserEvent("Github Thread Opened", {pageUrl: @state.link}) shell.openExternal(@state.link) if @state.link module.exports = ViewOnGithubButton diff --git a/internal_packages/notifications/lib/headers/account-error-header.jsx b/internal_packages/notifications/lib/headers/account-error-header.jsx index b12ca48df..1a88c5296 100644 --- a/internal_packages/notifications/lib/headers/account-error-header.jsx +++ b/internal_packages/notifications/lib/headers/account-error-header.jsx @@ -9,6 +9,7 @@ export default class AccountErrorHeader extends React.Component { constructor() { super(); this.state = this.getStateFromStores(); + this.upgradeLabel = "" } componentDidMount() { @@ -62,7 +63,12 @@ export default class AccountErrorHeader extends React.Component { _onUpgrade = () => { this.setState({buildingUpgradeURL: true}); - IdentityStore.fetchSingleSignOnURL('/payment').then((url) => { + const isSubscription = this.state.subscriptionState === IdentityStore.State.Lapsed + const utm = { + source: "UpgradeBanner", + campaign: isSubscription ? "SubscriptionExpired" : "TrialExpired", + } + IdentityStore.fetchSingleSignOnURL('/payment', utm).then((url) => { this.setState({buildingUpgradeURL: false}); shell.openExternal(url); }); diff --git a/internal_packages/onboarding/lib/decorators/create-page-for-form.jsx b/internal_packages/onboarding/lib/decorators/create-page-for-form.jsx index 98f177409..e0b845fd0 100644 --- a/internal_packages/onboarding/lib/decorators/create-page-for-form.jsx +++ b/internal_packages/onboarding/lib/decorators/create-page-for-form.jsx @@ -90,7 +90,7 @@ const CreatePageForForm = (FormComponent) => { OnboardingActions.accountJSONReceived(json) }) .catch((err) => { - Actions.recordUserEvent('Auth Failed', { + Actions.recordUserEvent('Email Account Auth Failed', { errorMessage: err.message, provider: accountInfo.type, }) diff --git a/internal_packages/onboarding/lib/onboarding-store.es6 b/internal_packages/onboarding/lib/onboarding-store.es6 index 3a85aced9..16909e215 100644 --- a/internal_packages/onboarding/lib/onboarding-store.es6 +++ b/internal_packages/onboarding/lib/onboarding-store.es6 @@ -14,6 +14,16 @@ function accountTypeForProvider(provider) { return provider; } +function providerForAccountType(type) { + if (type === 'exchange') { + return 'eas'; + } + if (type === 'imap') { + return 'custom'; + } + return type; +} + class OnboardingStore extends NylasStore { constructor() { super(); @@ -99,7 +109,8 @@ class OnboardingStore extends NylasStore { } else if (type === 'exchange') { nextPage = "account-settings-exchange"; } - Actions.recordUserEvent('Auth Flow Started', {type}); + const provider = providerForAccountType(type) + Actions.recordUserEvent('Email Account Auth Started', {provider}); this._onSetAccountInfo(Object.assign({}, this._accountInfo, {type})); this._onMoveToPage(nextPage); } @@ -126,7 +137,6 @@ class OnboardingStore extends NylasStore { if (!json.seen_welcome_page) { this._openWelcomePage(); } - Actions.recordUserEvent('Nylas Identity Set'); setTimeout(() => { if (isFirstAccount) { @@ -148,7 +158,7 @@ class OnboardingStore extends NylasStore { AccountStore.addAccountFromJSON(json); this._accountFromAuth = AccountStore.accountForEmail(json.email_address); - Actions.recordUserEvent('Auth Successful', { + Actions.recordUserEvent('Email Account Auth Succeeded', { provider: this._accountFromAuth.provider, }); ipcRenderer.send('new-account-added'); @@ -156,7 +166,9 @@ class OnboardingStore extends NylasStore { if (isFirstAccount) { this._onMoveToPage('initial-preferences'); - Actions.recordUserEvent('First Account Linked'); + Actions.recordUserEvent('First Account Linked', { + provider: this._accountFromAuth.provider, + }); } else { this._onOnboardingComplete(); } diff --git a/internal_packages/onboarding/lib/page-authenticate.jsx b/internal_packages/onboarding/lib/page-authenticate.jsx index 2efc5ecc9..7ed1a5991 100644 --- a/internal_packages/onboarding/lib/page-authenticate.jsx +++ b/internal_packages/onboarding/lib/page-authenticate.jsx @@ -80,7 +80,7 @@ export default class AuthenticatePage extends React.Component { componentDidMount() { const webview = ReactDOM.findDOMNode(this.refs.webview); - webview.src = `${IdentityStore.URLRoot}/onboarding`; + webview.src = `${IdentityStore.URLRoot}/onboarding?utm_medium=N1&utm_source=OnboardingPage`; webview.addEventListener('did-start-loading', this.webviewDidStartLoading); webview.addEventListener('did-get-response-details', this.webviewDidGetResponseDetails); webview.addEventListener('did-fail-load', this.webviewDidFailLoad); diff --git a/internal_packages/preferences/lib/tabs/preferences-identity.jsx b/internal_packages/preferences/lib/tabs/preferences-identity.jsx index 49c3dc5e8..576e8cfc3 100644 --- a/internal_packages/preferences/lib/tabs/preferences-identity.jsx +++ b/internal_packages/preferences/lib/tabs/preferences-identity.jsx @@ -7,6 +7,8 @@ class OpenIdentityPageButton extends React.Component { static propTypes = { path: React.PropTypes.string, label: React.PropTypes.string, + source: React.PropTypes.string, + campaign: React.PropTypes.string, img: React.PropTypes.string, } @@ -18,7 +20,12 @@ class OpenIdentityPageButton extends React.Component { } _onClick = () => { - IdentityStore.fetchSingleSignOnURL(this.props.path).then((url) => { + this.setState({loading: true}); + IdentityStore.fetchSingleSignOnURL(this.props.path, { + source: this.props.source, + campaign: this.props.campaign, + content: this.props.label, + }).then((url) => { this.setState({loading: false}); shell.openExternal(url); }); @@ -83,7 +90,7 @@ class PreferencesIdentity extends React.Component { There {(trialDaysRemaining > 1) ? `are ${trialDaysRemaining} days ` : `is one day `} remaining in your 30-day trial of Nylas Pro. - + ) } @@ -95,7 +102,7 @@ class PreferencesIdentity extends React.Component { Your subscription has been cancelled or your billing information has expired. We've paused your mailboxes! Re-new your subscription to continue using N1. - + ) } @@ -128,7 +135,7 @@ class PreferencesIdentity extends React.Component {
{firstname} {lastname}
{email}
- +
Actions.logoutNylasIdentity()}>Sign Out
diff --git a/internal_packages/thread-search/lib/search-query-subscription.es6 b/internal_packages/thread-search/lib/search-query-subscription.es6 index 71c9b20c9..b81255c80 100644 --- a/internal_packages/thread-search/lib/search-query-subscription.es6 +++ b/internal_packages/thread-search/lib/search-query-subscription.es6 @@ -130,8 +130,8 @@ class SearchQuerySubscription extends MutableQuerySubscription { let timeToFirstThreadSelected = null; const searchQuery = this._searchQuery const timeInsideSearch = Math.round((Date.now() - this._searchStartedAt) / 1000) - const selectedThreads = this._focusedThreadCount - const didSelectAnyThreads = selectedThreads > 0 + const numItems = this._focusedThreadCount + const didSelectAnyThreads = numItems > 0 if (this._firstThreadSelectedAt) { timeToFirstThreadSelected = Math.round((this._firstThreadSelectedAt - this._searchStartedAt) / 1000) @@ -142,7 +142,7 @@ class SearchQuerySubscription extends MutableQuerySubscription { const data = { searchQuery, - selectedThreads, + numItems, timeInsideSearch, didSelectAnyThreads, timeToFirstServerResults, diff --git a/internal_packages/thread-search/lib/search-store.coffee b/internal_packages/thread-search/lib/search-store.coffee index ff931e8f9..d22b6eabe 100644 --- a/internal_packages/thread-search/lib/search-store.coffee +++ b/internal_packages/thread-search/lib/search-store.coffee @@ -60,7 +60,6 @@ class SearchStore extends NylasStore current = FocusedPerspectiveStore.current() if @queryPopulated() - Actions.recordUserEvent("Commit Search Query", {}) @_isSearching = true @_perspectiveBeforeSearch ?= current next = new SearchMailboxPerspective(current.accountIds, @_searchQuery.trim()) diff --git a/internal_packages/thread-snooze/lib/snooze-store.es6 b/internal_packages/thread-snooze/lib/snooze-store.es6 index d80fdc36f..31c37a113 100644 --- a/internal_packages/thread-snooze/lib/snooze-store.es6 +++ b/internal_packages/thread-snooze/lib/snooze-store.es6 @@ -22,11 +22,12 @@ class SnoozeStore { recordSnoozeEvent(threads, snoozeDate, label) { try { - const min = Math.round(((new Date(snoozeDate)).valueOf() - Date.now()) / 1000 / 60); - Actions.recordUserEvent("Snooze Threads", { - numThreads: threads.length, - snoozeTime: min, - buttonType: label, + 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 diff --git a/internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee b/internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee index 65c1582bb..bbe041c05 100644 --- a/internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee +++ b/internal_packages/worker-sync/lib/nylas-sync-worker-pool.coffee @@ -139,11 +139,11 @@ class NylasSyncWorkerPool _handleAccountDeltas: (deltas) => for delta in deltas Actions.updateAccount(delta.account_id, {syncState: delta.sync_state}) - Actions.recordUserEvent('Account State Delta', { - accountId: delta.account_id - accountEmail: delta.email_address - syncState: delta.sync_state - }) + if delta.sync_state isnt "running" + Actions.recordUserEvent('Account Sync Errored', { + accountId: delta.account_id + syncState: delta.sync_state + }) _handleDeltaDeletion: (delta) => klass = NylasAPI._apiObjectToClassMap[delta.object] diff --git a/src/components/evented-iframe.cjsx b/src/components/evented-iframe.cjsx index fd78e17d5..edfc8c6f3 100644 --- a/src/components/evented-iframe.cjsx +++ b/src/components/evented-iframe.cjsx @@ -163,7 +163,7 @@ class EventedIFrame extends React.Component # just following the link directly if rawHref.startsWith(IdentityStore.URLRoot) path = rawHref.split(IdentityStore.URLRoot).pop() - IdentityStore.fetchSingleSignOnURL(IdentityStore.identity(), path).then (href) => + IdentityStore.fetchSingleSignOnURL(path, {source: "SingleSignOnEmail"}).then (href) => NylasEnv.windowEventHandler.openLink(href: href, metaKey: e.metaKey) return diff --git a/src/flux/stores/identity-store.es6 b/src/flux/stores/identity-store.es6 index 2e6b3d192..d0a51ec2a 100644 --- a/src/flux/stores/identity-store.es6 +++ b/src/flux/stores/identity-store.es6 @@ -2,6 +2,7 @@ import NylasStore from 'nylas-store'; import keytar from 'keytar'; import {ipcRenderer} from 'electron'; import request from 'request'; +import url from 'url' import Actions from '../actions'; import AccountStore from './account-store'; @@ -125,12 +126,26 @@ class IdentityStore extends NylasStore { }); } - fetchSingleSignOnURL(path) { + /** + * This passes utm_source, utm_campaign, and utm_content params to the + * N1 billing site. Please reference: + * https://paper.dropbox.com/doc/Analytics-ID-Unification-oVDTkakFsiBBbk9aeuiA3 + * for the full list of utm_ labels. + */ + fetchSingleSignOnURL(path, {source, campaign, content}) { if (!this._identity) { return Promise.reject(new Error("fetchSingleSignOnURL: no identity set.")); } - if (!path.startsWith('/')) { + const qs = {utm_medium: "N1"} + if (source) { qs.utm_source = source } + if (campaign) { qs.utm_campaign = campaign } + if (content) { qs.utm_content = content } + + const pathWithUtm = url.parse(path); + pathWithUtm.query = Object.assign({}, qs, (pathWithUtm.query || {})) + + if (!pathWithUtm.startsWith('/')) { return Promise.reject(new Error("fetchSingleSignOnURL: path must start with a leading slash.")); } @@ -138,9 +153,10 @@ class IdentityStore extends NylasStore { request({ method: 'POST', url: `${this.URLRoot}/n1/login-link`, + qs: qs, json: true, body: { - next_path: path, + next_path: pathWithUtm.format(), account_token: this._identity.token, }, }, (error, response = {}, body) => { diff --git a/src/pro b/src/pro index 82d9667fe..13d3ffdb2 160000 --- a/src/pro +++ b/src/pro @@ -1 +1 @@ -Subproject commit 82d9667fe661ec332b86dd6b47babddd40e65d86 +Subproject commit 13d3ffdb2103110e2fe4e712dea817590c92309a