mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-22 15:17:53 -05:00
Merge pull request #3503 from picklesrus/join-ga
Add analytics logging to join flow.
This commit is contained in:
commit
b845010025
12 changed files with 182 additions and 20 deletions
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue