Skip to content

Commit

Permalink
Use Mixpanel to track load performance (#2049)
Browse files Browse the repository at this point in the history
* Fire action when editor is ready

* Add Mixpanel client

* Instrument Environment Ready in Mixpanel

* Add Experimental Mode property to all Mixpanel events
  • Loading branch information
outoftime authored Dec 31, 2019
1 parent 3eeb9fa commit 92a6555
Show file tree
Hide file tree
Showing 18 changed files with 163 additions and 1 deletion.
5 changes: 5 additions & 0 deletions __mocks__/mixpanel-browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export default {
init: jest.fn(),
register: jest.fn(),
track: jest.fn(),
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@
"lodash-es": "4.17.15",
"loop-breaker": "0.1.1",
"lru-cache": "5.1.1",
"mixpanel-browser": "2.32.0",
"moment": "2.24.0",
"mousetrap": "1.6.3",
"object-inspect": "1.7.0",
Expand Down
9 changes: 9 additions & 0 deletions src/actions/instrumentation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {createAction} from 'redux-actions';

export const editorReady = createAction(
'EDITOR_READY',
(language, timestamp) => ({
language,
timestamp,
}),
);
2 changes: 2 additions & 0 deletions src/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {install as installOfflinePlugin} from 'offline-plugin/runtime';
import {bugsnagClient} from './util/bugsnag';
import Application from './components/Application';
import initI18n from './util/initI18n';
import {initMixpanel} from './clients/mixpanel';

installDevTools(Immutable);
installOfflinePlugin({
Expand All @@ -22,6 +23,7 @@ installOfflinePlugin({
});

initI18n();
initMixpanel();

ReactDOM.render(
React.createElement(Application),
Expand Down
13 changes: 13 additions & 0 deletions src/clients/mixpanel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {mixpanelToken} from '../config';

export async function loadMixpanel() {
const {default: mixpanel} = await import(
/* webpackChunkName: "mainAsync" */
'mixpanel-browser'
);
return mixpanel;
}
export async function initMixpanel() {
const mixpanel = await loadMixpanel();
mixpanel.init(mixpanelToken);
}
4 changes: 4 additions & 0 deletions src/components/AceEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ class AceEditor extends React.Component {
_setupEditor(containerElement) {
if (containerElement) {
this._editor = createAceEditor(containerElement);
this._editor.renderer.once('afterRender', () => {
this.props.onReady(performance.now());
});
this._startNewSession(this.props.source);
this._resizeEditor();
this._editor.on('focus', this._resizeEditor);
Expand Down Expand Up @@ -166,6 +169,7 @@ AceEditor.propTypes = {
textSizeIsLarge: PropTypes.bool.isRequired,
onAutoFormat: PropTypes.func.isRequired,
onInput: PropTypes.func.isRequired,
onReady: PropTypes.func.isRequired,
onRequestedLineFocused: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
};
Expand Down
11 changes: 11 additions & 0 deletions src/components/CodeMirrorEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default function CodeMirrorEditor({
textSizeIsLarge,
onAutoFormat,
onInput,
onReady,
onRequestedLineFocused,
onSave,
}) {
Expand All @@ -53,6 +54,15 @@ export default function CodeMirrorEditor({
editor.setSize('100%', '100%');
}, []);

useLayoutEffect(() => {
const editor = editorRef.current;
function handleUpdate() {
onReady(performance.now());
editor.off('update', handleUpdate);
}
editor.on('update', handleUpdate);
}, [onReady]);

useLayoutEffect(() => {
const mode = CODEMIRROR_MODES_MAP[language];
const editor = editorRef.current;
Expand Down Expand Up @@ -149,6 +159,7 @@ CodeMirrorEditor.propTypes = {
textSizeIsLarge: PropTypes.bool,
onAutoFormat: PropTypes.func.isRequired,
onInput: PropTypes.func.isRequired,
onReady: PropTypes.func.isRequired,
onRequestedLineFocused: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
};
Expand Down
3 changes: 3 additions & 0 deletions src/components/EditorsColumn.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default function EditorsColumn({
onAutoFormat,
onComponentHide,
onEditorInput,
onEditorReady,
onRequestedLineFocused,
onResizableFlexDividerDrag,
onSave,
Expand Down Expand Up @@ -63,6 +64,7 @@ export default function EditorsColumn({
useCodeMirror={isExperimental}
onAutoFormat={onAutoFormat}
onInput={handleInputForLanguage(language)}
onReady={partial(onEditorReady, language)}
onRequestedLineFocused={onRequestedLineFocused}
onSave={onSave}
/>
Expand Down Expand Up @@ -104,6 +106,7 @@ EditorsColumn.propTypes = {
onAutoFormat: PropTypes.func.isRequired,
onComponentHide: PropTypes.func.isRequired,
onEditorInput: PropTypes.func.isRequired,
onEditorReady: PropTypes.func.isRequired,
onRequestedLineFocused: PropTypes.func.isRequired,
onResizableFlexDividerDrag: PropTypes.func.isRequired,
onSave: PropTypes.func.isRequired,
Expand Down
9 changes: 9 additions & 0 deletions src/components/__tests__/CodeMirrorEditor.test.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import CodeMirror from 'codemirror';
import isNil from 'lodash-es/isNil';
import find from 'lodash-es/find';
import findLast from 'lodash-es/findLast';
import last from 'lodash-es/last';
import React from 'react';
Expand All @@ -20,6 +21,7 @@ const DEFAULT_PROPS = {
source: '<!doctype html>\n<html>\n</html>',
onAutoFormat: jest.fn(),
onInput: jest.fn(),
onReady: jest.fn(),
onRequestedLineFocused: jest.fn(),
onSave: jest.fn(),
};
Expand Down Expand Up @@ -107,6 +109,13 @@ describe('codemirror editor', () => {
expect(editor.setValue).toHaveBeenLastCalledWith(DEFAULT_PROPS.source);
});

test('onReady', () => {
const [, handleUpdate] = find(editor.on.mock.calls, {0: 'update'});
jest.spyOn(performance, 'now').mockReturnValue(12345);
handleUpdate();
expect(DEFAULT_PROPS.onReady).toHaveBeenCalledWith(12345);
});

test('swapping docs', () => {
updateComponent({projectKey: '1'});
const [doc0, doc1] = CodeMirror.Doc.mock.instances;
Expand Down
2 changes: 2 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ module.exports = {
bugsnagApiKey: '3cc590a735bc2e50d2a21e467cf62fee',

gitRevision: process.env.GIT_REVISION,

mixpanelToken: process.env.MIXPANEL_TOKEN,
};
5 changes: 5 additions & 0 deletions src/containers/EditorsColumn.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
updateProjectSource,
saveProject,
} from '../actions';
import {editorReady} from '../actions/instrumentation';

function mapStateToProps(state) {
const {visibleLanguages} = getHiddenAndVisibleLanguages(state);
Expand All @@ -40,6 +41,10 @@ function mapDispatchToProps(dispatch) {
dispatch(updateProjectSource(projectKey, language, source));
},

onEditorReady(language, timestamp) {
dispatch(editorReady(language, timestamp));
},

onRequestedLineFocused() {
dispatch(editorFocusedRequestedLine());
},
Expand Down
18 changes: 18 additions & 0 deletions src/logic/__tests__/instrumentApplicationLoaded.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import mixpanel from 'mixpanel-browser';

import instrumentApplicationLoaded from '../instrumentApplicationLoaded';
import {applicationLoaded} from '../../actions';

import {makeTestLogic} from './helpers';

const testLogic = makeTestLogic(instrumentApplicationLoaded);

it('should register no experimental mode', async () => {
await testLogic(applicationLoaded());
expect(mixpanel.register).toHaveBeenCalledWith({'Experimental Mode': false});
});

it('should register experimental mode', async () => {
await testLogic(applicationLoaded({isExperimental: true}));
expect(mixpanel.register).toHaveBeenCalledWith({'Experimental Mode': true});
});
23 changes: 23 additions & 0 deletions src/logic/__tests__/instrumentEnvironmentReady.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import mixpanel from 'mixpanel-browser';

import {makeInstrumentEnvironmentReady} from '../instrumentEnvironmentReady';
import {editorReady} from '../../actions/instrumentation';

import {makeTestLogic} from './helpers';

test('dispatches to mixpanel on the first editor ready action', async () => {
const testLogic = makeTestLogic(makeInstrumentEnvironmentReady());
const timestamp = 12345;
await testLogic(editorReady('html', timestamp));
expect(mixpanel.track).toHaveBeenCalledWith('Environment Ready', {
Timestamp: timestamp,
});
});

test('does not send additional events to mixpanel', async () => {
const testLogic = makeTestLogic(makeInstrumentEnvironmentReady());
await testLogic(editorReady('html', 0));
mixpanel.track.mockClear();
await testLogic(editorReady('css', 0));
expect(mixpanel.track).not.toHaveBeenCalled();
});
4 changes: 4 additions & 0 deletions src/logic/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import startAccountMigration from './startAccountMigration';
import projectSuccessfullySaved from './projectSuccessfullySaved';
import unlinkGithubIdentity from './unlinkGithubIdentity';
import saveProject from './saveProject';
import instrumentEnvironmentReady from './instrumentEnvironmentReady';
import instrumentApplicationLoaded from './instrumentApplicationLoaded';

export default [
instrumentApplicationLoaded,
instrumentEnvironmentReady,
linkGithubIdentity,
logout,
startAccountMigration,
Expand Down
15 changes: 15 additions & 0 deletions src/logic/instrumentApplicationLoaded.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {createLogic} from 'redux-logic';

import {applicationLoaded} from '../actions';
import {loadMixpanel} from '../clients/mixpanel';

export default createLogic({
type: applicationLoaded,

async process({action: {payload: {isExperimental} = {}}}) {
const mixpanel = await loadMixpanel();
mixpanel.register({
'Experimental Mode': Boolean(isExperimental),
});
},
});
32 changes: 32 additions & 0 deletions src/logic/instrumentEnvironmentReady.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {createLogic} from 'redux-logic';

import {editorReady} from '../actions/instrumentation';
import {loadMixpanel} from '../clients/mixpanel';

export function makeInstrumentEnvironmentReady() {
let hasTracked = false;

return createLogic({
type: editorReady,

validate(_, allow, reject) {
if (hasTracked) {
reject();
} else {
hasTracked = true;
allow();
}
},

async process({
action: {
payload: {timestamp},
},
}) {
const mixpanel = await loadMixpanel();
mixpanel.track('Environment Ready', {Timestamp: timestamp});
},
});
}

export default makeInstrumentEnvironmentReady();
1 change: 1 addition & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ module.exports = (env = process.env.NODE_ENV || 'development') => {
FIREBASE_MEASUREMENT_ID: 'G-GSST3YLQE7',
FIREBASE_PROJECT_ID: 'popcode-development',
GIT_REVISION: null,
MIXPANEL_TOKEN: '68e1844d4a34c789e9368740a3bb4ceb',
NODE_ENV: env,
WARN_ON_DROPPED_ERRORS: 'false',
}),
Expand Down
7 changes: 6 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7231,7 +7231,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"

inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4:
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3, inherits@~2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
Expand Down Expand Up @@ -9593,6 +9593,11 @@ mixin-deep@^1.2.0:
for-in "^1.0.2"
is-extendable "^1.0.1"

[email protected]:
version "2.32.0"
resolved "https://registry.yarnpkg.com/mixpanel-browser/-/mixpanel-browser-2.32.0.tgz#cbd7b3dd9ccf62082496096ac7090fd396b642fb"
integrity sha512-BLfwtGLjq6KSW3ZlRuq4/KCOcGm2PlQlAhPmt9ZjM//ksNfuYfvfQNJ8Vz+7Tw5/cVxLd6/bIrRLA6IBZaUjfA==

[email protected], mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
Expand Down

0 comments on commit 92a6555

Please sign in to comment.