fix: [UEPR-177] prevent multiple submission of feedback answers

This commit is contained in:
MiroslavDionisiev 2025-04-01 15:00:01 +03:00
commit 8a73da07b1
15 changed files with 115 additions and 86 deletions

View file

@ -84,11 +84,12 @@ jobs:
ONBOARDING_TESTING_ENDING_DATE: "${{ vars.ONBOARDING_TESTING_ENDING_DATE }}" ONBOARDING_TESTING_ENDING_DATE: "${{ vars.ONBOARDING_TESTING_ENDING_DATE }}"
QUALITATIVE_FEEDBACK_ACTIVE: "${{ vars.QUALITATIVE_FEEDBACK_ACTIVE }}" QUALITATIVE_FEEDBACK_ACTIVE: "${{ vars.QUALITATIVE_FEEDBACK_ACTIVE }}"
QUALITATIVE_FEEDBACK_STARTING_DATE: "${{ vars.QUALITATIVE_FEEDBACK_STARTING_DATE }}" QUALITATIVE_FEEDBACK_STARTING_DATE: "${{ vars.QUALITATIVE_FEEDBACK_STARTING_DATE }}"
QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_RATE: "${{ vars.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_RATE }}" QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_FREQUENCY: "${{ vars.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_FREQUENCY }}"
QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_RATE: "${{ vars.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_RATE }}" QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_FREQUENCY: "${{ vars.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_FREQUENCY }}"
QUALITATIVE_FEEDBACK_DEBUGGING_USER_RATE: "${{ vars.QUALITATIVE_FEEDBACK_DEBUGGING_USER_RATE }}" QUALITATIVE_FEEDBACK_DEBUGGING_USER_FREQUENCY: "${{ vars.QUALITATIVE_FEEDBACK_DEBUGGING_USER_FREQUENCY }}"
QUALITATIVE_FEEDBACK_TUTORIALS_USER_RATE: "${{ vars.QUALITATIVE_FEEDBACK_TUTORIALS_USER_RATE }}" QUALITATIVE_FEEDBACK_TUTORIALS_USER_FREQUENCY: "${{ vars.QUALITATIVE_FEEDBACK_TUTORIALS_USER_FREQUENCY }}"
QUALITATIVE_FEEDBACK_ENDING_DATE: "${{ vars.QUALITATIVE_FEEDBACK_ENDING_DATE }}" QUALITATIVE_FEEDBACK_ENDING_DATE: "${{ vars.QUALITATIVE_FEEDBACK_ENDING_DATE }}"
IDEAS_GENERATOR_SOURCE: "${{ vars.IDEAS_GENERATOR_SOURCE }}"
# used by src/template-config.js # used by src/template-config.js
GTM_ID: ${{ secrets.GTM_ID }} GTM_ID: ${{ secrets.GTM_ID }}

View file

@ -59,9 +59,11 @@ const Carousel = props => {
href = `/studios/${item.id}/`; href = `/studios/${item.id}/`;
break; break;
case 'project': case 'project':
href = props.fromStarterProjectsPage ? href = `/projects/${item.id}${
`/projects/${item.id}?fromStarterProjectsPage=${props.fromStarterProjectsPage}` : props.fromStarterProjectsPage ?
`/projects/${item.id}/`; '?fromStarterProjectsPage=true' :
''
}`;
break; break;
default: default:
href = `/${item.type}/${item.id}/`; href = `/${item.type}/${item.id}/`;

View file

@ -4,8 +4,8 @@ import {hideQualitativeFeedback} from '../../../redux/qualitative-feedback.js';
import { import {
QUALITATIVE_FEEDBACK_DATA, QUALITATIVE_FEEDBACK_DATA,
QUALITATIVE_FEEDBACK_QUESTION_ID QUALITATIVE_FEEDBACK_QUESTION_ID
} from './qualitative_feedback_data.js'; } from './qualitative-feedback-data.js';
import {QualitativeFeedback} from './qualitative_feedback.jsx'; import {QualitativeFeedback} from './qualitative-feedback.jsx';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import {triggerAnalyticsEvent} from '../../../lib/google-analytics-utils.js'; import {triggerAnalyticsEvent} from '../../../lib/google-analytics-utils.js';

View file

@ -4,8 +4,8 @@ import {hideQualitativeFeedback} from '../../../redux/qualitative-feedback.js';
import { import {
QUALITATIVE_FEEDBACK_DATA, QUALITATIVE_FEEDBACK_DATA,
QUALITATIVE_FEEDBACK_QUESTION_ID QUALITATIVE_FEEDBACK_QUESTION_ID
} from './qualitative_feedback_data.js'; } from './qualitative-feedback-data.js';
import {QualitativeFeedback} from './qualitative_feedback.jsx'; import {QualitativeFeedback} from './qualitative-feedback.jsx';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import {triggerAnalyticsEvent} from '../../../lib/google-analytics-utils.js'; import {triggerAnalyticsEvent} from '../../../lib/google-analytics-utils.js';

View file

@ -10,7 +10,7 @@ import {Formik} from 'formik';
import classNames from 'classnames'; import classNames from 'classnames';
import FormikRadioButton from '../../formik-forms/formik-radio-button.jsx'; import FormikRadioButton from '../../formik-forms/formik-radio-button.jsx';
import './qualitative_feedback.scss'; import './qualitative-feedback.scss';
const FeedbackOption = ({ const FeedbackOption = ({
id, id,
@ -45,7 +45,12 @@ FeedbackOption.propTypes = {
value: PropTypes.string value: PropTypes.string
}; };
export const QualitativeFeedback = ({feedbackData, hideFeedback, isOpen, sendGAEvent}) => { export const QualitativeFeedback = ({
feedbackData,
hideFeedback,
isOpen,
sendGAEvent
}) => {
const [displayModal, setDisplayModal] = useState(false); const [displayModal, setDisplayModal] = useState(false);
const [_, setFilledFeedback] = useLocalStorage( const [_, setFilledFeedback] = useLocalStorage(
feedbackData.questionId, feedbackData.questionId,
@ -54,23 +59,37 @@ export const QualitativeFeedback = ({feedbackData, hideFeedback, isOpen, sendGAE
const intl = useIntl(); const intl = useIntl();
const onClose = useCallback(() => { const onClose = useCallback(() => {
sendGAEvent('closed'); if (displayModal) {
setDisplayModal(false);
sendGAEvent('closed');
}
setFilledFeedback(true); setFilledFeedback(true);
hideFeedback(); hideFeedback();
setDisplayModal(false); }, [
}, [setFilledFeedback, hideFeedback, setDisplayModal]); displayModal,
setFilledFeedback,
hideFeedback,
setDisplayModal,
sendGAEvent
]);
// TBD: add logic for sending events to GA
const onSubmit = useCallback( const onSubmit = useCallback(
formData => { formData => {
if (formData.feedback) { if (formData.feedback && displayModal) {
setDisplayModal(false);
sendGAEvent(formData.feedback); sendGAEvent(formData.feedback);
setFilledFeedback(true); setFilledFeedback(true);
hideFeedback(); hideFeedback();
setDisplayModal(false);
} }
}, },
[hideFeedback, setDisplayModal, setFilledFeedback] [
displayModal,
hideFeedback,
setDisplayModal,
setFilledFeedback,
sendGAEvent
]
); );
useEffect(() => { useEffect(() => {
@ -78,7 +97,7 @@ export const QualitativeFeedback = ({feedbackData, hideFeedback, isOpen, sendGAE
const timer = setTimeout(() => { const timer = setTimeout(() => {
setDisplayModal(true); setDisplayModal(true);
}, 5000); }, 5000);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [isOpen]); }, [isOpen]);
@ -102,33 +121,31 @@ export const QualitativeFeedback = ({feedbackData, hideFeedback, isOpen, sendGAE
}} }}
onSubmit={onSubmit} onSubmit={onSubmit}
> >
{({handleSubmit, setFieldValue, values}) => {({handleSubmit, setFieldValue, values}) => (
( <form
<form className="feedback-form"
className="feedback-form" onSubmit={handleSubmit}
onSubmit={handleSubmit} >
> <div className="feedback-question">
<div className="feedback-question"> <FormattedMessage id={feedbackData.questionId} />
<FormattedMessage id={feedbackData.questionId} /> </div>
</div> <div className="feedback-options">
<div className="feedback-options"> {feedbackData.options.map(option => (
{feedbackData.options.map(option => ( <FeedbackOption
<FeedbackOption key={option.value}
key={option.value} id={option.label}
id={option.label} label={intl.formatMessage({id: option.label})}
label={intl.formatMessage({id: option.label})} selectedValue={values.feedback}
selectedValue={values.feedback} value={option.value}
value={option.value} onSetFieldValue={setFieldValue}
onSetFieldValue={setFieldValue} />
/> ))}
))} </div>
</div> <Button className="feedback-submit">
<Button className="feedback-submit"> <FormattedMessage id="feedback.submit" />
<FormattedMessage id="feedback.submit" /> </Button>
</Button> </form>
</form> )}
)
}
</Formik> </Formik>
</ModalInnerContent> </ModalInnerContent>
</Modal> </Modal>

View file

@ -4,8 +4,8 @@ import {hideQualitativeFeedback} from '../../../redux/qualitative-feedback.js';
import { import {
QUALITATIVE_FEEDBACK_DATA, QUALITATIVE_FEEDBACK_DATA,
QUALITATIVE_FEEDBACK_QUESTION_ID QUALITATIVE_FEEDBACK_QUESTION_ID
} from './qualitative_feedback_data.js'; } from './qualitative-feedback-data.js';
import {QualitativeFeedback} from './qualitative_feedback.jsx'; import {QualitativeFeedback} from './qualitative-feedback.jsx';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import {triggerAnalyticsEvent} from '../../../lib/google-analytics-utils.js'; import {triggerAnalyticsEvent} from '../../../lib/google-analytics-utils.js';
@ -23,7 +23,7 @@ const StarterProjectsFeedback = ({hideFeedback, isOpen, projectName}) => {
projectName: projectName, projectName: projectName,
feedbackResponse: data feedbackResponse: data
}), }),
[] [projectName]
); );
return ( return (

View file

@ -4,8 +4,8 @@ import {hideQualitativeFeedback} from '../../../redux/qualitative-feedback.js';
import { import {
QUALITATIVE_FEEDBACK_DATA, QUALITATIVE_FEEDBACK_DATA,
QUALITATIVE_FEEDBACK_QUESTION_ID QUALITATIVE_FEEDBACK_QUESTION_ID
} from './qualitative_feedback_data.js'; } from './qualitative-feedback-data.js';
import {QualitativeFeedback} from './qualitative_feedback.jsx'; import {QualitativeFeedback} from './qualitative-feedback.jsx';
import {connect} from 'react-redux'; import {connect} from 'react-redux';
import {triggerAnalyticsEvent} from '../../../lib/google-analytics-utils.js'; import {triggerAnalyticsEvent} from '../../../lib/google-analytics-utils.js';

View file

@ -16,10 +16,13 @@ const isCurrentDayInRange = () => {
); );
}; };
const hasGivenFeedback = feedbackQuestionId => const notRespondedToFeedback = feedbackQuestionId =>
localStorageAvailable && localStorage.getItem(feedbackQuestionId) !== 'true'; localStorageAvailable && localStorage.getItem(feedbackQuestionId) !== 'true';
const canShowFeedbackWidget = feedbackUserRate => { const canShowFeedbackWidget = feedbackUserRate => {
// randomNumber will be in the range [0, feedbackUserRate - 1]
// and we are assering against one number from the range
// this way we simulate picking one from n users
const randomNum = Math.floor( const randomNum = Math.floor(
Math.random() * feedbackUserRate Math.random() * feedbackUserRate
); );
@ -35,11 +38,11 @@ const isUserEligibleForFeedback = (user, permissions, feedbackQuestionId, feedba
JSON.parse(process.env.QUALITATIVE_FEEDBACK_ACTIVE) && JSON.parse(process.env.QUALITATIVE_FEEDBACK_ACTIVE) &&
canShowFeedbackWidget(feedbackUserRate) && canShowFeedbackWidget(feedbackUserRate) &&
isCurrentDayInRange() && isCurrentDayInRange() &&
hasGivenFeedback(feedbackQuestionId) notRespondedToFeedback(feedbackQuestionId)
); );
const isFeedbackDisplayed = feedbacksDisplayed => const isFeedbackDisplayed = feedbacksDisplayed =>
!Object.entries(feedbacksDisplayed).some(feedback => Object.entries(feedbacksDisplayed).some(feedback =>
feedback.value feedback.value
); );
@ -51,7 +54,7 @@ export const shouldDisplayFeedbackWidget = (
feedbacksDisplayed feedbacksDisplayed
) => ( ) => (
isUserEligibleForFeedback(user, permissions, feedbackQuestionId, feedbackUserRate) && isUserEligibleForFeedback(user, permissions, feedbackQuestionId, feedbackUserRate) &&
isFeedbackDisplayed(feedbacksDisplayed) !isFeedbackDisplayed(feedbacksDisplayed)
); );
export const sendUserPropertiesForFeedback = (user, permissions, shouldDisplayFeedback) => { export const sendUserPropertiesForFeedback = (user, permissions, shouldDisplayFeedback) => {

View file

@ -17,7 +17,7 @@ export const isDateInRange = (date, startDate, endDate) => {
export const calculateAgeGroup = (birthYear, birthMonth) => { export const calculateAgeGroup = (birthYear, birthMonth) => {
if (!birthMonth || !birthYear) { if (!birthMonth || !birthYear) {
return '[unset]'; return '[no-data]';
} }
const today = new Date(); const today = new Date();

View file

@ -1,4 +1,4 @@
import {QUALITATIVE_FEEDBACK_QUESTION_ID} from '../components/modal/feedback/qualitative_feedback_data'; import {QUALITATIVE_FEEDBACK_QUESTION_ID} from '../components/modal/feedback/qualitative-feedback-data';
const initialState = { const initialState = {
[QUALITATIVE_FEEDBACK_QUESTION_ID.ideasGenerator]: false, [QUALITATIVE_FEEDBACK_QUESTION_ID.ideasGenerator]: false,

View file

@ -29,7 +29,7 @@ const {
const {useRef} = require('react'); const {useRef} = require('react');
const { const {
QUALITATIVE_FEEDBACK_QUESTION_ID QUALITATIVE_FEEDBACK_QUESTION_ID
} = require('../../components/modal/feedback/qualitative_feedback_data.js'); } = require('../../components/modal/feedback/qualitative-feedback-data.js');
const {shouldDisplayFeedbackWidget, sendUserPropertiesForFeedback} = require('../../lib/feedback.js'); const {shouldDisplayFeedbackWidget, sendUserPropertiesForFeedback} = require('../../lib/feedback.js');
require('./ideas.scss'); require('./ideas.scss');
@ -165,25 +165,30 @@ const Ideas = ({
} }
}; };
const shoulDisplayFeedback = shouldDisplayFeedbackWidget( const shouldDisplayFeedback = shouldDisplayFeedbackWidget(
user, user,
permissions, permissions,
QUALITATIVE_FEEDBACK_QUESTION_ID.ideasGenerator, QUALITATIVE_FEEDBACK_QUESTION_ID.ideasGenerator,
process.env.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_RATE, process.env.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_FREQUENCY,
feedback feedback
); );
if (iframe && shoulDisplayFeedback) { if (iframe && shouldDisplayFeedback) {
sendUserPropertiesForFeedback( sendUserPropertiesForFeedback(
user, user,
permissions, permissions,
shoulDisplayFeedback shouldDisplayFeedback
); );
iframe.addEventListener('load', onIframeLoad); iframe.addEventListener('load', onIframeLoad);
} }
return () => { return () => {
if (iframe) { if (iframe) {
iframe.contentWindow.document
.querySelectorAll('[class*="green-flag"]')
.forEach(element =>
element.removeEventListener('click', onGreenFlagClick)
);
iframe.removeEventListener('load', onIframeLoad); iframe.removeEventListener('load', onIframeLoad);
} }
}; };
@ -197,7 +202,7 @@ const Ideas = ({
<div className="banner-wrapper"> <div className="banner-wrapper">
<iframe <iframe
ref={iframeRef} ref={iframeRef}
src={`http://localhost:8333/projects/9999923/embed`} src={`${process.env.IDEAS_GENERATOR_SOURCE}/embed`}
width="485" width="485"
height="402" height="402"
allowfullscreen allowfullscreen

View file

@ -47,7 +47,7 @@ const TutorialsHighlight = require('../../components/journeys/tutorials-highligh
const {sendUserPropertiesForOnboarding, shouldDisplayOnboarding} = require('../../lib/onboarding.js'); const {sendUserPropertiesForOnboarding, shouldDisplayOnboarding} = require('../../lib/onboarding.js');
const {triggerAnalyticsEvent} = require('../../lib/google-analytics-utils.js'); const {triggerAnalyticsEvent} = require('../../lib/google-analytics-utils.js');
const {StarterProjectsFeedback} = require('../../components/modal/feedback/starter-projects-feedback.jsx'); const {StarterProjectsFeedback} = require('../../components/modal/feedback/starter-projects-feedback.jsx');
const {QUALITATIVE_FEEDBACK_QUESTION_ID} = require('../../components/modal/feedback/qualitative_feedback_data.js'); const {QUALITATIVE_FEEDBACK_QUESTION_ID} = require('../../components/modal/feedback/qualitative-feedback-data.js');
const {shouldDisplayFeedbackWidget, sendUserPropertiesForFeedback} = require('../../lib/feedback.js'); const {shouldDisplayFeedbackWidget, sendUserPropertiesForFeedback} = require('../../lib/feedback.js');
const {displayQualitativeFeedback} = require('../../redux/qualitative-feedback.js'); const {displayQualitativeFeedback} = require('../../redux/qualitative-feedback.js');
const {DebuggingFeedback} = require('../../components/modal/feedback/debugging-feedback.jsx'); const {DebuggingFeedback} = require('../../components/modal/feedback/debugging-feedback.jsx');
@ -73,7 +73,7 @@ const IntlGUIWithProjectHandler = ({...props}) => {
}, [props.projectId, prevProjectId, props.user, props.permissions]); }, [props.projectId, prevProjectId, props.user, props.permissions]);
const displayGuiFeedback = useCallback((feedbackQuestionId, feedbackUserRate) => { const displayGuiFeedback = useCallback((feedbackQuestionId, feedbackUserRate) => {
const shoulDisplayFeedback = shouldDisplayFeedbackWidget( const shouldDisplayFeedback = shouldDisplayFeedbackWidget(
props.user, props.user,
props.permissions, props.permissions,
feedbackQuestionId, feedbackQuestionId,
@ -81,11 +81,11 @@ const IntlGUIWithProjectHandler = ({...props}) => {
props.feedback props.feedback
); );
if (shoulDisplayFeedback) { if (shouldDisplayFeedback) {
sendUserPropertiesForFeedback( sendUserPropertiesForFeedback(
props.user, props.user,
props.permissions, props.permissions,
shoulDisplayFeedback shouldDisplayFeedback
); );
props.displayFeedback(feedbackQuestionId); props.displayFeedback(feedbackQuestionId);
} }
@ -95,14 +95,14 @@ const IntlGUIWithProjectHandler = ({...props}) => {
<> <>
<IntlGUI <IntlGUI
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
displayDebugFeedback={() => displayGuiFeedback( onDebugModalClose={() => displayGuiFeedback(
QUALITATIVE_FEEDBACK_QUESTION_ID.debugging, QUALITATIVE_FEEDBACK_QUESTION_ID.debugging,
process.env.QUALITATIVE_FEEDBACK_DEBUGGING_USER_RATE process.env.QUALITATIVE_FEEDBACK_DEBUGGING_USER_FREQUENCY
)} )}
// eslint-disable-next-line react/jsx-no-bind // eslint-disable-next-line react/jsx-no-bind
displayTutorialsFeedback={() => displayGuiFeedback( onTutorialSelect={() => displayGuiFeedback(
QUALITATIVE_FEEDBACK_QUESTION_ID.tutorials, QUALITATIVE_FEEDBACK_QUESTION_ID.tutorials,
process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_RATE process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_FREQUENCY
)} )}
{...props} {...props}
/> />
@ -305,18 +305,18 @@ class Preview extends React.Component {
sendUserPropertiesForOnboarding(this.props.user, this.props.permissions); sendUserPropertiesForOnboarding(this.props.user, this.props.permissions);
const fromStarterProjectsPage = queryString.parse(location.search).fromStarterProjectsPage === 'true'; const fromStarterProjectsPage = queryString.parse(location.search).fromStarterProjectsPage === 'true';
const shoulDisplayFeedback = shouldDisplayFeedbackWidget( const shouldDisplayFeedback = shouldDisplayFeedbackWidget(
this.props.user, this.props.user,
this.props.permissions, this.props.permissions,
QUALITATIVE_FEEDBACK_QUESTION_ID.starterProjects, QUALITATIVE_FEEDBACK_QUESTION_ID.starterProjects,
process.env.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_RATE, process.env.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_FREQUENCY,
this.props.feedback this.props.feedback
); );
if (fromStarterProjectsPage && shoulDisplayFeedback) { if (fromStarterProjectsPage && shouldDisplayFeedback) {
sendUserPropertiesForFeedback( sendUserPropertiesForFeedback(
this.props.user, this.props.user,
this.props.permissions, this.props.permissions,
shoulDisplayFeedback shouldDisplayFeedback
); );
this.props.displayFeedback( this.props.displayFeedback(
QUALITATIVE_FEEDBACK_QUESTION_ID.starterProjects QUALITATIVE_FEEDBACK_QUESTION_ID.starterProjects

View file

@ -308,19 +308,20 @@ module.exports = {
process.env.QUALITATIVE_FEEDBACK_STARTING_DATE || '2024-01-20' process.env.QUALITATIVE_FEEDBACK_STARTING_DATE || '2024-01-20'
}"`, }"`,
'process.env.QUALITATIVE_FEEDBACK_ENDING_DATE': `"${ 'process.env.QUALITATIVE_FEEDBACK_ENDING_DATE': `"${
process.env.QUALITATIVE_FEEDBACK_ENDING_DATE || '2030-11-20' process.env.QUALITATIVE_FEEDBACK_ENDING_DATE || '2024-11-20'
}"`, }"`,
'process.env.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_RATE': `"${ // Given user frequency X, show qualitative feedback to 1 in X users
process.env.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_RATE || 2 'process.env.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_FREQUENCY': `"${
process.env.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_FREQUENCY || 2
}"`, }"`,
'process.env.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_RATE': `"${ 'process.env.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_FREQUENCY': `"${
process.env.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_RATE || 2 process.env.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_FREQUENCY || 2
}"`, }"`,
'process.env.QUALITATIVE_FEEDBACK_DEBUGGING_USER_RATE': `"${ 'process.env.QUALITATIVE_FEEDBACK_DEBUGGING_USER_FREQUENCY': `"${
process.env.QUALITATIVE_FEEDBACK_DEBUGGING_USER_RATE || 2 process.env.QUALITATIVE_FEEDBACK_DEBUGGING_USER_FREQUENCY || 2
}"`, }"`,
'process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_RATE': `"${ 'process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_FREQUENCY': `"${
process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_RATE || 2 process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_FREQUENCY || 2
}"`, }"`,
'process.env.IDEAS_GENERATOR_SOURCE': `"${ 'process.env.IDEAS_GENERATOR_SOURCE': `"${
process.env.IDEAS_GENERATOR_SOURCE || 'https://scratch.mit.edu/projects/1108790117' process.env.IDEAS_GENERATOR_SOURCE || 'https://scratch.mit.edu/projects/1108790117'