mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2025-02-17 08:31:23 -05:00
Merge pull request #8830 from MiroslavDionisiev/UEPR-51
[UERP-51] Google Analytics events for onboarding journeys
This commit is contained in:
commit
7c58c33d12
4 changed files with 119 additions and 11 deletions
|
@ -6,6 +6,7 @@ const DriverJourney = require('../driver-journey/driver-journey.jsx');
|
||||||
const {defineMessages, useIntl} = require('react-intl');
|
const {defineMessages, useIntl} = require('react-intl');
|
||||||
const {useMemo, useState, useCallback} = require('react');
|
const {useMemo, useState, useCallback} = require('react');
|
||||||
const PropTypes = require('prop-types');
|
const PropTypes = require('prop-types');
|
||||||
|
const {triggerAnalyticsEvent} = require('../../../lib/onboarding.js');
|
||||||
|
|
||||||
require('./editor-journey.scss');
|
require('./editor-journey.scss');
|
||||||
|
|
||||||
|
@ -92,8 +93,21 @@ const messages = defineMessages({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const STEP_NAMES = [
|
||||||
|
'pick-genre-step',
|
||||||
|
'game-step',
|
||||||
|
'animation-step',
|
||||||
|
'music-step',
|
||||||
|
'clicker-game-step',
|
||||||
|
'pong-game-step',
|
||||||
|
'animate-character-step',
|
||||||
|
'make-fly-animation-step',
|
||||||
|
'record-sound-step',
|
||||||
|
'make-music-step'
|
||||||
|
];
|
||||||
|
|
||||||
const projectIds = {
|
const projectIds = {
|
||||||
clicker: '10000252',
|
clicker: '10128368',
|
||||||
pong: '10128515',
|
pong: '10128515',
|
||||||
animateCharacter: '10128067',
|
animateCharacter: '10128067',
|
||||||
makeItFly: '114019829',
|
makeItFly: '114019829',
|
||||||
|
@ -138,6 +152,14 @@ const EditorJourney = ({onActivateDeck, setCanViewTutorialsHighlight, setShowJou
|
||||||
));
|
));
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const pickStep = useCallback((stepNumber, editorJourneyStep) => {
|
||||||
|
triggerAnalyticsEvent({
|
||||||
|
event: 'editor-journey-step',
|
||||||
|
editorJourneyStep: editorJourneyStep
|
||||||
|
});
|
||||||
|
driverObj.moveTo(stepNumber);
|
||||||
|
}, driverObj);
|
||||||
|
|
||||||
const createStep = useCallback((projectId, tutorialId) => ({
|
const createStep = useCallback((projectId, tutorialId) => ({
|
||||||
title: intl.formatMessage(messages.createStepTitle),
|
title: intl.formatMessage(messages.createStepTitle),
|
||||||
showButtons: ['close'],
|
showButtons: ['close'],
|
||||||
|
@ -149,6 +171,10 @@ const EditorJourney = ({onActivateDeck, setCanViewTutorialsHighlight, setShowJou
|
||||||
imgSrc: '/images/onboarding-journeys/Tutorials-Icon.svg',
|
imgSrc: '/images/onboarding-journeys/Tutorials-Icon.svg',
|
||||||
text: intl.formatMessage(messages.tutorialButtonText),
|
text: intl.formatMessage(messages.tutorialButtonText),
|
||||||
handleOnClick: () => {
|
handleOnClick: () => {
|
||||||
|
triggerAnalyticsEvent({
|
||||||
|
event: 'editor-journey-step',
|
||||||
|
editorJourneyStep: `${tutorialId}-Open-Tutorial`
|
||||||
|
});
|
||||||
onActivateDeck(tutorialId);
|
onActivateDeck(tutorialId);
|
||||||
setShowJourney(false);
|
setShowJourney(false);
|
||||||
driverObj.destroy();
|
driverObj.destroy();
|
||||||
|
@ -167,6 +193,10 @@ const EditorJourney = ({onActivateDeck, setCanViewTutorialsHighlight, setShowJou
|
||||||
imgSrc: '/images/onboarding-journeys/On-Own-Icon.svg',
|
imgSrc: '/images/onboarding-journeys/On-Own-Icon.svg',
|
||||||
text: intl.formatMessage(messages.onMyOwnButtonText),
|
text: intl.formatMessage(messages.onMyOwnButtonText),
|
||||||
handleOnClick: () => {
|
handleOnClick: () => {
|
||||||
|
triggerAnalyticsEvent({
|
||||||
|
event: 'editor-journey-step',
|
||||||
|
editorJourneyStep: `${tutorialId}-On-My-Own`
|
||||||
|
});
|
||||||
setCanViewTutorialsHighlight(true);
|
setCanViewTutorialsHighlight(true);
|
||||||
setShowJourney(false);
|
setShowJourney(false);
|
||||||
driverObj.destroy();
|
driverObj.destroy();
|
||||||
|
@ -181,6 +211,14 @@ const EditorJourney = ({onActivateDeck, setCanViewTutorialsHighlight, setShowJou
|
||||||
() => ({
|
() => ({
|
||||||
popoverClass: 'gui-journey',
|
popoverClass: 'gui-journey',
|
||||||
overlayOpacity: 0,
|
overlayOpacity: 0,
|
||||||
|
onDestroyStarted: () => {
|
||||||
|
const stepName = STEP_NAMES[driverObj.getActiveIndex()] || '';
|
||||||
|
triggerAnalyticsEvent({
|
||||||
|
event: 'editor-journey-step',
|
||||||
|
editorJourneyStep: `${stepName}-closed`
|
||||||
|
});
|
||||||
|
driverObj.destroy();
|
||||||
|
},
|
||||||
onDestroyed: () => {
|
onDestroyed: () => {
|
||||||
setShowJourney(false);
|
setShowJourney(false);
|
||||||
},
|
},
|
||||||
|
@ -195,17 +233,17 @@ const EditorJourney = ({onActivateDeck, setCanViewTutorialsHighlight, setShowJou
|
||||||
{
|
{
|
||||||
imgSrc: '/images/onboarding-journeys/Games-Icon.svg',
|
imgSrc: '/images/onboarding-journeys/Games-Icon.svg',
|
||||||
text: intl.formatMessage(messages.gameButtonText),
|
text: intl.formatMessage(messages.gameButtonText),
|
||||||
handleOnClick: () => driverObj.moveTo(1)
|
handleOnClick: () => pickStep(1, 'Games')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
imgSrc: '/images/onboarding-journeys/Animation-Icon.svg',
|
imgSrc: '/images/onboarding-journeys/Animation-Icon.svg',
|
||||||
text: intl.formatMessage(messages.animiationButtonText),
|
text: intl.formatMessage(messages.animiationButtonText),
|
||||||
handleOnClick: () => driverObj.moveTo(2)
|
handleOnClick: () => pickStep(2, 'Animation')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
imgSrc: '/images/onboarding-journeys/Music-Icon.svg',
|
imgSrc: '/images/onboarding-journeys/Music-Icon.svg',
|
||||||
text: intl.formatMessage(messages.musicButtonText),
|
text: intl.formatMessage(messages.musicButtonText),
|
||||||
handleOnClick: () => driverObj.moveTo(3)
|
handleOnClick: () => pickStep(3, 'Music')
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
@ -223,12 +261,12 @@ const EditorJourney = ({onActivateDeck, setCanViewTutorialsHighlight, setShowJou
|
||||||
{
|
{
|
||||||
imgSrc: '/images/onboarding-journeys/Clicker-Game.jpg',
|
imgSrc: '/images/onboarding-journeys/Clicker-Game.jpg',
|
||||||
text: intl.formatMessage(messages.clickerGameButtonText),
|
text: intl.formatMessage(messages.clickerGameButtonText),
|
||||||
handleOnClick: () => driverObj.moveTo(4)
|
handleOnClick: () => pickStep(4, 'Clicker-Game')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
imgSrc: '/images/onboarding-journeys/Pong-Game.jpg',
|
imgSrc: '/images/onboarding-journeys/Pong-Game.jpg',
|
||||||
text: intl.formatMessage(messages.pongGameButtonText),
|
text: intl.formatMessage(messages.pongGameButtonText),
|
||||||
handleOnClick: () => driverObj.moveTo(5)
|
handleOnClick: () => pickStep(5, 'Pong-Game')
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
@ -246,12 +284,12 @@ const EditorJourney = ({onActivateDeck, setCanViewTutorialsHighlight, setShowJou
|
||||||
{
|
{
|
||||||
imgSrc: '/images/onboarding-journeys/Character-Animation.jpg',
|
imgSrc: '/images/onboarding-journeys/Character-Animation.jpg',
|
||||||
text: intl.formatMessage(messages.characterAnimationButtonText),
|
text: intl.formatMessage(messages.characterAnimationButtonText),
|
||||||
handleOnClick: () => driverObj.moveTo(6)
|
handleOnClick: () => pickStep(6, 'Character-Animation')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
imgSrc: '/images/onboarding-journeys/Fly-Animation.jpg',
|
imgSrc: '/images/onboarding-journeys/Fly-Animation.jpg',
|
||||||
text: intl.formatMessage(messages.flyAnimationButtonText),
|
text: intl.formatMessage(messages.flyAnimationButtonText),
|
||||||
handleOnClick: () => driverObj.moveTo(7)
|
handleOnClick: () => pickStep(7, 'Fly-Animation')
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
@ -269,12 +307,12 @@ const EditorJourney = ({onActivateDeck, setCanViewTutorialsHighlight, setShowJou
|
||||||
{
|
{
|
||||||
imgSrc: '/images/onboarding-journeys/Record-Music.jpg',
|
imgSrc: '/images/onboarding-journeys/Record-Music.jpg',
|
||||||
text: intl.formatMessage(messages.recordSoundButtonText),
|
text: intl.formatMessage(messages.recordSoundButtonText),
|
||||||
handleOnClick: () => driverObj.moveTo(8)
|
handleOnClick: () => pickStep(8, 'Record-Music')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
imgSrc: '/images/onboarding-journeys/Make-Music.jpg',
|
imgSrc: '/images/onboarding-journeys/Make-Music.jpg',
|
||||||
text: intl.formatMessage(messages.makeMusicButtonText),
|
text: intl.formatMessage(messages.makeMusicButtonText),
|
||||||
handleOnClick: () => driverObj.moveTo(9)
|
handleOnClick: () => pickStep(9, 'Make-Music')
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|
38
src/lib/onboarding.js
Normal file
38
src/lib/onboarding.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
const calculateAgeGroup = (birthYear, birthMonth) => {
|
||||||
|
const today = new Date();
|
||||||
|
let age = today.getFullYear() - parseInt(birthYear, 10);
|
||||||
|
const monthDiff = (today.getMonth() + 1) - birthMonth;
|
||||||
|
if (monthDiff < 0) {
|
||||||
|
age--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (age <= 10) {
|
||||||
|
return '[00-10]';
|
||||||
|
} else if (age <= 16) {
|
||||||
|
return '[11-16]';
|
||||||
|
}
|
||||||
|
return '[17+]';
|
||||||
|
};
|
||||||
|
|
||||||
|
// add logic when implementing the eligibility check in [UEPR-71]
|
||||||
|
const onboardingTestGroup = () => 'Displayed Onboarding Journeys';
|
||||||
|
|
||||||
|
export const triggerAnalyticsEvent = eventVaribles => {
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
|
||||||
|
window.dataLayer.push({
|
||||||
|
...eventVaribles
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendUserProperties = user => {
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
|
||||||
|
const {gender, birthYear, birthMonth} = user;
|
||||||
|
|
||||||
|
window.dataLayer.push({
|
||||||
|
testGroup: onboardingTestGroup(),
|
||||||
|
ageGroup: calculateAgeGroup(birthYear, birthMonth),
|
||||||
|
gender: gender
|
||||||
|
});
|
||||||
|
};
|
|
@ -41,8 +41,9 @@ const projectShape = require('./projectshape.jsx').projectShape;
|
||||||
require('./preview.scss');
|
require('./preview.scss');
|
||||||
|
|
||||||
const frameless = require('../../lib/frameless');
|
const frameless = require('../../lib/frameless');
|
||||||
const {useState, useCallback} = require('react');
|
const {useState, useCallback, useEffect} = require('react');
|
||||||
const ProjectJourney = require('../../components/journeys/project-journey/project-journey.jsx');
|
const ProjectJourney = require('../../components/journeys/project-journey/project-journey.jsx');
|
||||||
|
const {triggerAnalyticsEvent} = require('../../lib/onboarding.js');
|
||||||
|
|
||||||
// disable enter key submission on formsy input fields; otherwise formsy thinks
|
// disable enter key submission on formsy input fields; otherwise formsy thinks
|
||||||
// we meant to trigger the "See inside" button. Instead, treat these keypresses
|
// we meant to trigger the "See inside" button. Instead, treat these keypresses
|
||||||
|
@ -235,6 +236,15 @@ const PreviewPresentation = ({
|
||||||
}
|
}
|
||||||
}, [hasSubmittedComment, user]);
|
}, [hasSubmittedComment, user]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (canViewProjectJourney && projectInfo.title) {
|
||||||
|
triggerAnalyticsEvent({
|
||||||
|
event: 'editor-journey-step',
|
||||||
|
editorJourneyStep: `${projectInfo.title}-Starter-Project`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [canViewProjectJourney, projectInfo.title]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="preview">
|
<div className="preview">
|
||||||
{showEmailConfirmationModal && <EmailConfirmationModal
|
{showEmailConfirmationModal && <EmailConfirmationModal
|
||||||
|
|
|
@ -47,6 +47,7 @@ const {useEffect, useState} = require('react');
|
||||||
const EditorJourney = require('../../components/journeys/editor-journey/editor-journey.jsx');
|
const EditorJourney = require('../../components/journeys/editor-journey/editor-journey.jsx');
|
||||||
const {usePrevious} = require('react-use');
|
const {usePrevious} = require('react-use');
|
||||||
const TutorialsHighlight = require('../../components/journeys/tutorials-highlight/tutorials-highlight.jsx');
|
const TutorialsHighlight = require('../../components/journeys/tutorials-highlight/tutorials-highlight.jsx');
|
||||||
|
const {triggerAnalyticsEvent, sendUserProperties} = require('../../lib/onboarding.js');
|
||||||
|
|
||||||
const IntlGUIWithProjectHandler = ({...props}) => {
|
const IntlGUIWithProjectHandler = ({...props}) => {
|
||||||
const [showJourney, setShowJourney] = useState(false);
|
const [showJourney, setShowJourney] = useState(false);
|
||||||
|
@ -255,6 +256,10 @@ class Preview extends React.Component {
|
||||||
false // Do not show cloud/username alerts again
|
false // Do not show cloud/username alerts again
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!prevProps.user.id && this.props.user.id) {
|
||||||
|
sendUserProperties(this.props.user);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
this.removeEventListeners();
|
this.removeEventListeners();
|
||||||
|
@ -540,6 +545,15 @@ class Preview extends React.Component {
|
||||||
if (!this.state.greenFlagRecorded) {
|
if (!this.state.greenFlagRecorded) {
|
||||||
this.props.logProjectView(this.props.projectInfo.id, this.props.authorUsername, this.props.user.token);
|
this.props.logProjectView(this.props.projectInfo.id, this.props.authorUsername, this.props.user.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showJourney = queryString.parse(location.search, {parseBooleans: true}).showJourney;
|
||||||
|
if (showJourney) {
|
||||||
|
triggerAnalyticsEvent({
|
||||||
|
event: 'tutorial-played',
|
||||||
|
playedProject: this.props.projectInfo.title
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
showUsernameBlockAlert: false,
|
showUsernameBlockAlert: false,
|
||||||
showCloudDataAlert: false,
|
showCloudDataAlert: false,
|
||||||
|
@ -651,6 +665,14 @@ class Preview extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handleRemix () {
|
handleRemix () {
|
||||||
|
const showJourney = queryString.parse(location.search, {parseBooleans: true}).showJourney;
|
||||||
|
if (showJourney) {
|
||||||
|
triggerAnalyticsEvent({
|
||||||
|
event: 'tutorial-remixed',
|
||||||
|
remixedProject: this.props.projectInfo.title
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Update the state first before starting the remix to show spinner
|
// Update the state first before starting the remix to show spinner
|
||||||
this.setState({isRemixing: true}, () => {
|
this.setState({isRemixing: true}, () => {
|
||||||
this.props.remixProject();
|
this.props.remixProject();
|
||||||
|
|
Loading…
Reference in a new issue