fix(analytics): improved analytics

This commit is contained in:
Evan Morikawa 2016-06-07 12:53:05 -07:00
parent b69f150e6a
commit c4753197ee
16 changed files with 81 additions and 32 deletions

View file

@ -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});
}
});

View file

@ -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],

View file

@ -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();

View file

@ -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

View file

@ -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);
});

View file

@ -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,
})

View file

@ -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();
}

View file

@ -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);

View file

@ -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.
</div>
<OpenIdentityPageButton img="ic-upgrade.png" label="Upgrade to Nylas Pro" path="/payment" />
<OpenIdentityPageButton img="ic-upgrade.png" label="Upgrade to Nylas Pro" path="/payment" campaign="Upgrade" source="Preferences" />
</div>
)
}
@ -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.
</div>
<OpenIdentityPageButton img="ic-upgrade.png" label="Update Subscription" path="/dashboard#subscription" />
<OpenIdentityPageButton img="ic-upgrade.png" label="Update Subscription" path="/dashboard#subscription" campaign="Renew" source="Preferences" />
</div>
)
}
@ -128,7 +135,7 @@ class PreferencesIdentity extends React.Component {
<div className="name">{firstname} {lastname}</div>
<div className="email">{email}</div>
<div className="identity-actions">
<OpenIdentityPageButton label="Account Details" path="/dashboard" />
<OpenIdentityPageButton label="Account Details" path="/dashboard" source="Preferences" campaign="Dashboard" />
<div className="btn" onClick={() => Actions.logoutNylasIdentity()}>Sign Out</div>
</div>
</div>

View file

@ -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,

View file

@ -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())

View file

@ -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

View file

@ -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]

View file

@ -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

View file

@ -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) => {

@ -1 +1 @@
Subproject commit 82d9667fe661ec332b86dd6b47babddd40e65d86
Subproject commit 13d3ffdb2103110e2fe4e712dea817590c92309a