Merge pull request #3503 from picklesrus/join-ga

Add analytics logging to join flow.
This commit is contained in:
picklesrus 2019-11-06 11:01:40 -05:00 committed by GitHub
commit b845010025
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 182 additions and 20 deletions

View file

@ -54,6 +54,12 @@ class BirthDateStep extends React.Component {
'validateSelect'
]);
}
componentDidMount () {
if (this.props.sendAnalytics) {
this.props.sendAnalytics('join-birthdate');
}
}
validateSelect (selection) {
if (selection === 'null') {
return this.props.intl.formatMessage({id: 'general.required'});
@ -162,7 +168,8 @@ class BirthDateStep extends React.Component {
BirthDateStep.propTypes = {
intl: intlShape,
onNextStep: PropTypes.func
onNextStep: PropTypes.func,
sendAnalytics: PropTypes.func.isRequired
};
const IntlBirthDateStep = injectIntl(BirthDateStep);

View file

@ -23,6 +23,9 @@ class CountryStep extends React.Component {
this.countryOptions = [];
}
componentDidMount () {
if (this.props.sendAnalytics) {
this.props.sendAnalytics('join-country');
}
this.setCountryOptions();
}
setCountryOptions () {
@ -120,7 +123,8 @@ class CountryStep extends React.Component {
CountryStep.propTypes = {
intl: intlShape,
onNextStep: PropTypes.func
onNextStep: PropTypes.func,
sendAnalytics: PropTypes.func.isRequired
};
const IntlCountryStep = injectIntl(CountryStep);

View file

@ -35,6 +35,9 @@ class EmailStep extends React.Component {
this.emailRemoteCache = {};
}
componentDidMount () {
if (this.props.sendAnalytics) {
this.props.sendAnalytics('join-email');
}
// automatically start with focus on username field
if (this.emailInput) this.emailInput.focus();
@ -231,6 +234,7 @@ EmailStep.propTypes = {
intl: intlShape,
onCaptchaError: PropTypes.func,
onNextStep: PropTypes.func,
sendAnalytics: PropTypes.func.isRequired,
waiting: PropTypes.bool
};

View file

@ -61,6 +61,11 @@ class GenderStep extends React.Component {
'handleValidSubmit'
]);
}
componentDidMount () {
if (this.props.sendAnalytics) {
this.props.sendAnalytics('join-gender');
}
}
handleSetCustomRef (customInputRef) {
this.customInput = customInputRef;
}
@ -180,7 +185,8 @@ class GenderStep extends React.Component {
GenderStep.propTypes = {
intl: intlShape,
onNextStep: PropTypes.func
onNextStep: PropTypes.func,
sendAnalytics: PropTypes.func.isRequired
};
module.exports = injectIntl(GenderStep);

View file

@ -215,6 +215,18 @@ class JoinFlow extends React.Component {
resetState () {
this.setState(this.initialState);
}
sendAnalytics (path) {
const gaID = window.GA_ID;
if (!window.ga) {
return;
}
window.ga('send', {
hitType: 'pageview',
page: path,
tid: gaID
});
}
render () {
return (
<React.Fragment>
@ -222,17 +234,33 @@ class JoinFlow extends React.Component {
<RegistrationErrorStep
canTryAgain={this.canTryAgain()}
errorMsg={this.state.registrationError.errorMsg}
sendAnalytics={this.sendAnalytics}
/* eslint-disable react/jsx-no-bind */
onSubmit={this.handleErrorNext}
/* eslint-enable react/jsx-no-bind */
/>
) : (
<Progression step={this.state.step}>
<UsernameStep onNextStep={this.handleAdvanceStep} />
<CountryStep onNextStep={this.handleAdvanceStep} />
<BirthDateStep onNextStep={this.handleAdvanceStep} />
<GenderStep onNextStep={this.handleAdvanceStep} />
<UsernameStep
sendAnalytics={this.sendAnalytics}
onNextStep={this.handleAdvanceStep}
/>
<CountryStep
sendAnalytics={this.sendAnalytics}
onNextStep={this.handleAdvanceStep}
/>
<BirthDateStep
sendAnalytics={this.sendAnalytics}
onNextStep={this.handleAdvanceStep}
/>
<GenderStep
sendAnalytics={this.sendAnalytics}
onNextStep={this.handleAdvanceStep}
/>
<EmailStep
sendAnalytics={this.sendAnalytics}
waiting={this.state.waiting}
onCaptchaError={this.handleCaptchaError}
onNextStep={this.handlePrepareToRegister}
@ -240,6 +268,7 @@ class JoinFlow extends React.Component {
<WelcomeStep
createProjectOnComplete={this.props.createProjectOnComplete}
email={this.state.formData.email}
sendAnalytics={this.sendAnalytics}
username={this.state.formData.username}
onNextStep={this.props.onCompleteRegistration}
/>

View file

@ -15,6 +15,11 @@ class RegistrationErrorStep extends React.Component {
'handleSubmit'
]);
}
componentDidMount () {
if (this.props.sendAnalytics) {
this.props.sendAnalytics('join-error');
}
}
handleSubmit (e) {
// JoinFlowStep includes a <form> that handles a submit action.
// But here, we're not really submitting, so we need to prevent
@ -60,7 +65,8 @@ RegistrationErrorStep.propTypes = {
canTryAgain: PropTypes.bool.isRequired,
errorMsg: PropTypes.string,
intl: intlShape,
onSubmit: PropTypes.func.isRequired
onSubmit: PropTypes.func.isRequired,
sendAnalytics: PropTypes.func.isRequired
};
RegistrationErrorStep.defaultProps = {

View file

@ -37,6 +37,14 @@ class UsernameStep extends React.Component {
this.usernameRemoteCache = {};
}
componentDidMount () {
// Send info to analytics when we aren't on the standalone page.
// If we are on the standalone join page, the page load will take care of this.
if (!window.location.pathname.includes('/join')) {
if (this.props.sendAnalytics) {
this.props.sendAnalytics('join-username-modal');
}
}
// automatically start with focus on username field
if (this.usernameInput) this.usernameInput.focus();
}
@ -282,7 +290,8 @@ class UsernameStep extends React.Component {
UsernameStep.propTypes = {
intl: intlShape,
onNextStep: PropTypes.func
onNextStep: PropTypes.func,
sendAnalytics: PropTypes.func.isRequired
};
const IntlUsernameStep = injectIntl(UsernameStep);

View file

@ -17,6 +17,12 @@ class WelcomeStep extends React.Component {
'validateForm'
]);
}
componentDidMount () {
if (this.props.sendAnalytics) {
this.props.sendAnalytics('join-welcome');
}
}
validateForm () {
return {};
}
@ -87,6 +93,7 @@ WelcomeStep.propTypes = {
email: PropTypes.string,
intl: intlShape,
onNextStep: PropTypes.func,
sendAnalytics: PropTypes.func,
username: PropTypes.string
};

View file

@ -1,5 +1,6 @@
const React = require('react');
const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx');
const {mountWithIntl} = require('../../helpers/intl-helpers.jsx');
const JoinFlowStep = require('../../../src/components/join-flow/join-flow-step.jsx');
const FormikInput = require('../../../src/components/formik-forms/formik-input.jsx');
const FormikCheckbox = require('../../../src/components/formik-forms/formik-checkbox.jsx');
@ -36,13 +37,17 @@ jest.mock('../../../src/lib/validate.js', () => (
const EmailStep = require('../../../src/components/join-flow/email-step.jsx');
describe('EmailStep test', () => {
const defaultProps = () => ({
sendAnalytics: jest.fn()
});
afterEach(() => {
jest.clearAllMocks();
});
test('send correct props to formik', () => {
const intlWrapper = shallowWithIntl(<EmailStep />);
const intlWrapper = shallowWithIntl(<EmailStep
{...defaultProps()}
/>);
const emailStepWrapper = intlWrapper.dive();
expect(emailStepWrapper.props().initialValues.subscribe).toBe(false);
expect(emailStepWrapper.props().initialValues.email).toBe('');
@ -53,7 +58,9 @@ describe('EmailStep test', () => {
});
test('props sent to JoinFlowStep', () => {
const intlWrapper = shallowWithIntl(<EmailStep />);
const intlWrapper = shallowWithIntl(<EmailStep
{...defaultProps()}
/>);
// Dive to get past the intl wrapper
const emailStepWrapper = intlWrapper.dive();
// Dive to get past the anonymous component.
@ -69,7 +76,9 @@ describe('EmailStep test', () => {
});
test('props sent to FormikInput for email', () => {
const intlWrapper = shallowWithIntl(<EmailStep />);
const intlWrapper = shallowWithIntl(<EmailStep
{...defaultProps()}
/>);
// Dive to get past the intl wrapper
const emailStepWrapper = intlWrapper.dive();
// Dive to get past the anonymous component.
@ -86,7 +95,10 @@ describe('EmailStep test', () => {
});
test('props sent to FormikCheckbox for subscribe', () => {
const intlWrapper = shallowWithIntl(<EmailStep />);
const intlWrapper = shallowWithIntl(<EmailStep
{...defaultProps()}
/>);
// Dive to get past the intl wrapper
const emailStepWrapper = intlWrapper.dive();
// Dive to get past the anonymous component.
@ -109,7 +121,10 @@ describe('EmailStep test', () => {
};
const formData = {item1: 'thing', item2: 'otherthing'};
const intlWrapper = shallowWithIntl(
<EmailStep />);
<EmailStep
{...defaultProps()}
/>);
const emailStepWrapper = intlWrapper.dive();
emailStepWrapper.instance().onCaptchaLoad(); // to setup catpcha state
@ -133,6 +148,7 @@ describe('EmailStep test', () => {
const formData = {item1: 'thing', item2: 'otherthing'};
const intlWrapper = shallowWithIntl(
<EmailStep
{...defaultProps()}
{...props}
/>);
@ -162,6 +178,7 @@ describe('EmailStep test', () => {
global.grecaptcha = null;
const intlWrapper = shallowWithIntl(
<EmailStep
{...defaultProps()}
{...props}
/>
);
@ -171,9 +188,20 @@ describe('EmailStep test', () => {
expect(props.onCaptchaError).toHaveBeenCalled();
});
test('Component logs analytics', () => {
const sendAnalyticsFn = jest.fn();
mountWithIntl(
<EmailStep
sendAnalytics={sendAnalyticsFn}
/>);
expect(sendAnalyticsFn).toHaveBeenCalledWith('join-email');
});
test('validateEmail test email empty', () => {
const intlWrapper = shallowWithIntl(
<EmailStep />);
<EmailStep
{...defaultProps()}
/>);
const emailStepWrapper = intlWrapper.dive();
const val = emailStepWrapper.instance().validateEmail('');
expect(val).toBe('general.required');
@ -181,7 +209,9 @@ describe('EmailStep test', () => {
test('validateEmail test email null', () => {
const intlWrapper = shallowWithIntl(
<EmailStep />);
<EmailStep
{...defaultProps()}
/>);
const emailStepWrapper = intlWrapper.dive();
const val = emailStepWrapper.instance().validateEmail(null);
expect(val).toBe('general.required');
@ -189,7 +219,9 @@ describe('EmailStep test', () => {
test('validateEmail test email undefined', () => {
const intlWrapper = shallowWithIntl(
<EmailStep />);
<EmailStep
{...defaultProps()}
/>);
const emailStepWrapper = intlWrapper.dive();
const val = emailStepWrapper.instance().validateEmail();
expect(val).toBe('general.required');
@ -204,7 +236,8 @@ describe('validateEmailRemotelyWithCache test with successful requests', () => {
test('validateEmailRemotelyWithCache calls validate.validateEmailRemotely', done => {
const intlWrapper = shallowWithIntl(
<EmailStep />);
<EmailStep />
);
const instance = intlWrapper.dive().instance();
instance.validateEmailRemotelyWithCache('some-email@some-domain.com')

View file

@ -67,6 +67,19 @@ describe('JoinFlow', () => {
});
});
test('sendAnalytics calls google analytics with correct params', () => {
const joinFlowInstance = getJoinFlowWrapper().instance();
global.window.ga = jest.fn();
global.window.GA_ID = '1234';
joinFlowInstance.sendAnalytics('page-path');
const obj = {
hitType: 'pageview',
page: 'page-path',
tid: '1234'
};
expect(global.window.ga).toHaveBeenCalledWith('send', obj);
});
test('handleAdvanceStep', () => {
const joinFlowInstance = getJoinFlowWrapper().instance();
joinFlowInstance.setState({formData: {username: 'ScratchCat123'}, step: 2});

View file

@ -1,5 +1,6 @@
import React from 'react';
import {shallowWithIntl} from '../../helpers/intl-helpers.jsx';
import {mountWithIntl} from '../../helpers/intl-helpers.jsx';
import JoinFlowStep from '../../../src/components/join-flow/join-flow-step';
import RegistrationErrorStep from '../../../src/components/join-flow/registration-error-step';
@ -9,6 +10,7 @@ describe('RegistrationErrorStep', () => {
const getRegistrationErrorStepWrapper = props => {
const wrapper = shallowWithIntl(
<RegistrationErrorStep
sendAnalytics={jest.fn()}
{...props}
/>
);
@ -61,6 +63,14 @@ describe('RegistrationErrorStep', () => {
expect(errMsgElement).toHaveLength(0);
});
test('logs to analytics', () => {
const analyticsFn = jest.fn();
mountWithIntl(
<RegistrationErrorStep
sendAnalytics={analyticsFn}
/>);
expect(analyticsFn).toHaveBeenCalledWith('join-error');
});
test('when canTryAgain is true, show tryAgain message', () => {
const props = {
canTryAgain: true,

View file

@ -1,5 +1,6 @@
const React = require('react');
const {shallowWithIntl} = require('../../helpers/intl-helpers.jsx');
const {mountWithIntl} = require('../../helpers/intl-helpers.jsx');
const requestSuccessResponse = {
requestSucceeded: true,
@ -22,6 +23,7 @@ let mockedValidateUsernameRemotely = jest.fn(() => (
/* eslint-enable no-undef */
));
jest.mock('../../../src/lib/validate.js', () => (
{
...(jest.requireActual('../../../src/lib/validate.js')),
@ -32,10 +34,19 @@ jest.mock('../../../src/lib/validate.js', () => (
// must come after validation mocks, so validate.js will be mocked before it is required
const UsernameStep = require('../../../src/components/join-flow/username-step.jsx');
describe('UsernameStep tests', () => {
const defaultProps = () => ({
sendAnalytics: jest.fn()
});
afterEach(() => {
jest.clearAllMocks();
});
test('send correct props to formik', () => {
const wrapper = shallowWithIntl(<UsernameStep />);
const wrapper = shallowWithIntl(<UsernameStep
{...defaultProps()}
/>);
const formikWrapper = wrapper.dive();
expect(formikWrapper.props().initialValues.username).toBe('');
expect(formikWrapper.props().initialValues.password).toBe('');
@ -47,6 +58,28 @@ describe('UsernameStep tests', () => {
expect(formikWrapper.props().onSubmit).toBe(formikWrapper.instance().handleValidSubmit);
});
test('Component does not log if path is /join', () => {
const sendAnalyticsFn = jest.fn();
global.window.history.pushState({}, '', '/join');
mountWithIntl(
<UsernameStep
sendAnalytics={sendAnalyticsFn}
/>);
expect(sendAnalyticsFn).not.toHaveBeenCalled();
});
test('Component logs analytics', () => {
// Make sure '/join' is NOT in the path
global.window.history.pushState({}, '', '/');
const sendAnalyticsFn = jest.fn();
mountWithIntl(
<UsernameStep
sendAnalytics={sendAnalyticsFn}
/>);
expect(sendAnalyticsFn).toHaveBeenCalledWith('join-username-modal');
});
test('handleValidSubmit passes formData to next step', () => {
const formikBag = {
setSubmitting: jest.fn()
@ -55,6 +88,7 @@ describe('UsernameStep tests', () => {
const mockedOnNextStep = jest.fn();
const wrapper = shallowWithIntl(
<UsernameStep
{...defaultProps()}
onNextStep={mockedOnNextStep}
/>
);