mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2025-02-17 00:21:20 -05:00
Merge pull request #8847 from MiroslavDionisiev/UEPR-71
feat: [UEPR-71] Added onboarding feature toggle
This commit is contained in:
commit
4ce19e566b
6 changed files with 180 additions and 19 deletions
3
.github/workflows/ci-cd.yml
vendored
3
.github/workflows/ci-cd.yml
vendored
|
@ -70,6 +70,9 @@ jobs:
|
|||
SCRATCH_ENV: ${{ vars.SCRATCH_ENV }}
|
||||
SORTING_HAT_HOST: ${{ vars.SORTING_HAT_HOST }}
|
||||
USER_GUIDING_ID: ${{ secrets.USER_GUIDING_ID }}
|
||||
ONBOARDING_TEST_ACTIVE: ${{ secrets.ONBOARDING_TEST_ACTIVE }}
|
||||
ONBOARDING_TESTING_STARTING_DATE: ${{ secrets.ONBOARDING_TESTING_STARTING_DATE }}
|
||||
ONBOARDING_TESTING_ENDING_DATE: ${{ secrets.ONBOARDING_TESTING_ENDING_DATE }}
|
||||
|
||||
# used by src/template-config.js
|
||||
GTM_ID: ${{ secrets.GTM_ID }}
|
||||
|
|
|
@ -1,7 +1,47 @@
|
|||
const ONBOARDING_TESTING_GROUP_A_NAME = 'Displayed Onboarding Journeys';
|
||||
const ONBOARDING_TESTING_GROUP_B_NAME = 'Not Displayed Onboarding Journeys';
|
||||
|
||||
const isBanned = user => user.banned;
|
||||
|
||||
const isAdmin = permissions => permissions.admin;
|
||||
|
||||
const isMuted = permissions => !!Object.keys(permissions.mute_status).length;
|
||||
|
||||
const isDateInOnboardingRange = date => {
|
||||
const dateToCompare = Date.parse(date);
|
||||
const startDate = Date.parse(process.env.ONBOARDING_TESTING_STARTING_DATE);
|
||||
const endDate = Date.parse(process.env.ONBOARDING_TESTING_ENDING_DATE);
|
||||
|
||||
return dateToCompare >= startDate && dateToCompare <= endDate;
|
||||
};
|
||||
|
||||
const isRegisteredInRange = user => {
|
||||
const dateOfJoin = user.dateJoined.split('T')[0];
|
||||
|
||||
return isDateInOnboardingRange(dateOfJoin);
|
||||
};
|
||||
|
||||
const isCurrentDayInRange = () => {
|
||||
const currentDate = new Date().toJSON()
|
||||
.split('T')[0];
|
||||
|
||||
return isDateInOnboardingRange(currentDate);
|
||||
};
|
||||
|
||||
const isUserEligible = (user, permissions) =>
|
||||
Object.keys(user).length !== 0 &&
|
||||
Object.keys(permissions).length !== 0 &&
|
||||
JSON.parse(process.env.ONBOARDING_TEST_ACTIVE) &&
|
||||
isRegisteredInRange(user) &&
|
||||
isCurrentDayInRange() &&
|
||||
!isAdmin(permissions) &&
|
||||
!isMuted(permissions) &&
|
||||
!isBanned(user);
|
||||
|
||||
const calculateAgeGroup = (birthYear, birthMonth) => {
|
||||
const today = new Date();
|
||||
let age = today.getFullYear() - parseInt(birthYear, 10);
|
||||
const monthDiff = (today.getMonth() + 1) - birthMonth;
|
||||
const monthDiff = today.getMonth() + 1 - birthMonth;
|
||||
if (monthDiff < 0) {
|
||||
age--;
|
||||
}
|
||||
|
@ -14,24 +54,38 @@ const calculateAgeGroup = (birthYear, birthMonth) => {
|
|||
return '[17+]';
|
||||
};
|
||||
|
||||
// add logic when implementing the eligibility check in [UEPR-71]
|
||||
const onboardingTestGroup = () => 'Displayed Onboarding Journeys';
|
||||
const onboardingTestGroup = user =>
|
||||
(user.id % 2 === 0 ?
|
||||
ONBOARDING_TESTING_GROUP_A_NAME :
|
||||
ONBOARDING_TESTING_GROUP_B_NAME);
|
||||
|
||||
export const shouldDisplayOnboarding = (user, permissions) =>
|
||||
user.id % 2 === 0 && isUserEligible(user, permissions);
|
||||
|
||||
export const triggerAnalyticsEvent = eventVaribles => {
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
|
||||
|
||||
window.dataLayer.push({
|
||||
...eventVaribles
|
||||
});
|
||||
};
|
||||
|
||||
export const sendUserProperties = user => {
|
||||
export const sendUserProperties = (user, permissions) => {
|
||||
if (!isUserEligible(user, permissions)) {
|
||||
window.dataLayer.push({
|
||||
testGroup: null,
|
||||
ageGroup: null,
|
||||
gender: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
|
||||
const {gender, birthYear, birthMonth} = user;
|
||||
|
||||
|
||||
window.dataLayer.push({
|
||||
testGroup: onboardingTestGroup(),
|
||||
testGroup: onboardingTestGroup(user),
|
||||
ageGroup: calculateAgeGroup(birthYear, birthMonth),
|
||||
gender: gender
|
||||
});
|
||||
|
|
|
@ -43,7 +43,7 @@ require('./preview.scss');
|
|||
const frameless = require('../../lib/frameless');
|
||||
const {useState, useCallback, useEffect} = require('react');
|
||||
const ProjectJourney = require('../../components/journeys/project-journey/project-journey.jsx');
|
||||
const {triggerAnalyticsEvent} = require('../../lib/onboarding.js');
|
||||
const {triggerAnalyticsEvent, shouldDisplayOnboarding} = require('../../lib/onboarding.js');
|
||||
|
||||
// disable enter key submission on formsy input fields; otherwise formsy thinks
|
||||
// we meant to trigger the "See inside" button. Instead, treat these keypresses
|
||||
|
@ -151,10 +151,15 @@ const PreviewPresentation = ({
|
|||
visibilityInfo
|
||||
}) => {
|
||||
const [hasSubmittedComment, setHasSubmittedComment] = useState(false);
|
||||
const [canViewProjectJourney, setCanViewProjectJourney] = useState(
|
||||
queryString.parse(location.search, {parseBooleans: true}).showJourney
|
||||
);
|
||||
const [canViewProjectJourney, setCanViewProjectJourney] = useState(false);
|
||||
const [shouldStopProject, setShouldStopProject] = useState(false);
|
||||
useEffect(() => {
|
||||
setCanViewProjectJourney(
|
||||
queryString.parse(location.search, {parseBooleans: true}).showJourney &&
|
||||
!userOwnsProject &&
|
||||
shouldDisplayOnboarding(user, permissions)
|
||||
);
|
||||
}, [userOwnsProject, user, permissions]);
|
||||
const shareDate = ((projectInfo.history && projectInfo.history.shared)) ? projectInfo.history.shared : '';
|
||||
const revisedDate = ((projectInfo.history && projectInfo.history.modified)) ? projectInfo.history.modified : '';
|
||||
const showInstructions = editable || projectInfo.instructions ||
|
||||
|
|
|
@ -47,7 +47,7 @@ const {useEffect, useState} = require('react');
|
|||
const EditorJourney = require('../../components/journeys/editor-journey/editor-journey.jsx');
|
||||
const {usePrevious} = require('react-use');
|
||||
const TutorialsHighlight = require('../../components/journeys/tutorials-highlight/tutorials-highlight.jsx');
|
||||
const {triggerAnalyticsEvent, sendUserProperties} = require('../../lib/onboarding.js');
|
||||
const {triggerAnalyticsEvent, sendUserProperties, shouldDisplayOnboarding} = require('../../lib/onboarding.js');
|
||||
|
||||
const IntlGUIWithProjectHandler = ({...props}) => {
|
||||
const [showJourney, setShowJourney] = useState(false);
|
||||
|
@ -61,11 +61,12 @@ const IntlGUIWithProjectHandler = ({...props}) => {
|
|||
(prevProjectId === 0 || prevProjectId === '0') &&
|
||||
props.projectId &&
|
||||
props.projectId !== '0' &&
|
||||
!isTutorialOpen
|
||||
!isTutorialOpen &&
|
||||
shouldDisplayOnboarding(props.user, props.permissions)
|
||||
) {
|
||||
setShowJourney(true);
|
||||
}
|
||||
}, [props.projectId, prevProjectId]);
|
||||
}, [props.projectId, prevProjectId, props.user, props.permissions]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -257,8 +258,8 @@ class Preview extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (!prevProps.user.id && this.props.user.id) {
|
||||
sendUserProperties(this.props.user);
|
||||
if (!prevProps.user.id && this.props.user.id && this.props.permissions) {
|
||||
sendUserProperties(this.props.user, this.props.permissions);
|
||||
}
|
||||
}
|
||||
componentWillUnmount () {
|
||||
|
@ -547,7 +548,7 @@ class Preview extends React.Component {
|
|||
}
|
||||
|
||||
const showJourney = queryString.parse(location.search, {parseBooleans: true}).showJourney;
|
||||
if (showJourney) {
|
||||
if (showJourney && shouldDisplayOnboarding(this.props.user, this.props.permissions)) {
|
||||
triggerAnalyticsEvent({
|
||||
event: 'tutorial-played',
|
||||
playedProject: this.props.projectInfo.title
|
||||
|
@ -666,7 +667,7 @@ class Preview extends React.Component {
|
|||
}
|
||||
handleRemix () {
|
||||
const showJourney = queryString.parse(location.search, {parseBooleans: true}).showJourney;
|
||||
if (showJourney) {
|
||||
if (showJourney && shouldDisplayOnboarding(this.props.user, this.props.permissions)) {
|
||||
triggerAnalyticsEvent({
|
||||
event: 'tutorial-remixed',
|
||||
remixedProject: this.props.projectInfo.title
|
||||
|
|
89
test/unit/lib/onboarding.test.js
Normal file
89
test/unit/lib/onboarding.test.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
import {shouldDisplayOnboarding} from '../../../src/lib/onboarding';
|
||||
|
||||
describe('unit test lib/onboarding.js', () => {
|
||||
const startDate = new Date();
|
||||
const endDate = new Date(startDate);
|
||||
endDate.setDate(endDate.getDate() + 7);
|
||||
|
||||
let user;
|
||||
let permissions;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.ONBOARDING_TEST_ACTIVE = 'true';
|
||||
process.env.ONBOARDING_TESTING_STARTING_DATE = startDate.toJSON().split('T')[0];
|
||||
process.env.ONBOARDING_TESTING_ENDING_DATE = endDate.toJSON().split('T')[0];
|
||||
|
||||
user = {id: 2, dateJoined: startDate.toJSON(), banned: false};
|
||||
permissions = {admin: false, mute_status: {}, new_scratcher: true};
|
||||
});
|
||||
|
||||
describe('#shouldDisplayOnboarding', () => {
|
||||
describe('when user is eligible to view onboarding journeys', () => {
|
||||
describe('when there is time frame for A/B testing', () => {
|
||||
test('returns true', () => {
|
||||
expect(shouldDisplayOnboarding(user, permissions)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user is not eligible to view onboarding journeys', () => {
|
||||
describe('when feature flag is toggled off', () => {
|
||||
test('returns false', () => {
|
||||
process.env.ONBOARDING_TEST_ACTIVE = 'false';
|
||||
expect(shouldDisplayOnboarding(user, permissions)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user is in other testing group', () => {
|
||||
test('returns false', () => {
|
||||
user.id = 1;
|
||||
expect(shouldDisplayOnboarding(user, permissions)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user is registered outside of time frame', () => {
|
||||
test('returns false', () => {
|
||||
const currentDate = new Date();
|
||||
currentDate.setDate(currentDate.getDate() - 1);
|
||||
user.dateJoined = currentDate.toJSON();
|
||||
expect(shouldDisplayOnboarding(user, permissions)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user is admin', () => {
|
||||
test('returns false', () => {
|
||||
permissions.admin = true;
|
||||
expect(shouldDisplayOnboarding(user, permissions)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user is muted', () => {
|
||||
test('returns false', () => {
|
||||
permissions.mute_status = {showWarning: true};
|
||||
expect(shouldDisplayOnboarding(user, permissions)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user is banned', () => {
|
||||
test('returns false', () => {
|
||||
user.banned = true;
|
||||
expect(shouldDisplayOnboarding(user, permissions)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user is empty', () => {
|
||||
test('returns false', () => {
|
||||
user = {};
|
||||
expect(shouldDisplayOnboarding(user, permissions)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when permissions is empty', () => {
|
||||
test('returns false', () => {
|
||||
permissions = {};
|
||||
expect(shouldDisplayOnboarding(user, permissions)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -286,7 +286,16 @@ module.exports = {
|
|||
'process.env.GTM_ENV_AUTH': `"${process.env.GTM_ENV_AUTH || ''}"`,
|
||||
'process.env.GTM_ID': process.env.GTM_ID ? `"${process.env.GTM_ID}"` : null,
|
||||
'process.env.USER_GUIDING_ID': `"${process.env.USER_GUIDING_ID || ''}"`,
|
||||
'process.env.SORTING_HAT_HOST': `"${process.env.SORTING_HAT_HOST || ''}"`
|
||||
'process.env.SORTING_HAT_HOST': `"${process.env.SORTING_HAT_HOST || ''}"`,
|
||||
'process.env.ONBOARDING_TEST_ACTIVE': `"${
|
||||
process.env.ONBOARDING_TEST_ACTIVE || true
|
||||
}"`,
|
||||
'process.env.ONBOARDING_TESTING_STARTING_DATE': `"${
|
||||
process.env.ONBOARDING_TESTING_STARTING_DATE || '2024-01-20'
|
||||
}"`,
|
||||
'process.env.ONBOARDING_TESTING_ENDING_DATE': `"${
|
||||
process.env.ONBOARDING_TESTING_ENDING_DATE || '2030-11-20'
|
||||
}"`
|
||||
})
|
||||
])
|
||||
.concat(process.env.ANALYZE_BUNDLE === 'true' ? [
|
||||
|
|
Loading…
Reference in a new issue