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 }}"
QUALITATIVE_FEEDBACK_ACTIVE: "${{ vars.QUALITATIVE_FEEDBACK_ACTIVE }}"
QUALITATIVE_FEEDBACK_STARTING_DATE: "${{ vars.QUALITATIVE_FEEDBACK_STARTING_DATE }}"
QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_RATE: "${{ vars.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_RATE }}"
QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_RATE: "${{ vars.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_RATE }}"
QUALITATIVE_FEEDBACK_DEBUGGING_USER_RATE: "${{ vars.QUALITATIVE_FEEDBACK_DEBUGGING_USER_RATE }}"
QUALITATIVE_FEEDBACK_TUTORIALS_USER_RATE: "${{ vars.QUALITATIVE_FEEDBACK_TUTORIALS_USER_RATE }}"
QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_FREQUENCY: "${{ vars.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_FREQUENCY }}"
QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_FREQUENCY: "${{ vars.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_FREQUENCY }}"
QUALITATIVE_FEEDBACK_DEBUGGING_USER_FREQUENCY: "${{ vars.QUALITATIVE_FEEDBACK_DEBUGGING_USER_FREQUENCY }}"
QUALITATIVE_FEEDBACK_TUTORIALS_USER_FREQUENCY: "${{ vars.QUALITATIVE_FEEDBACK_TUTORIALS_USER_FREQUENCY }}"
QUALITATIVE_FEEDBACK_ENDING_DATE: "${{ vars.QUALITATIVE_FEEDBACK_ENDING_DATE }}"
IDEAS_GENERATOR_SOURCE: "${{ vars.IDEAS_GENERATOR_SOURCE }}"
# used by src/template-config.js
GTM_ID: ${{ secrets.GTM_ID }}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,8 +4,8 @@ import {hideQualitativeFeedback} from '../../../redux/qualitative-feedback.js';
import {
QUALITATIVE_FEEDBACK_DATA,
QUALITATIVE_FEEDBACK_QUESTION_ID
} from './qualitative_feedback_data.js';
import {QualitativeFeedback} from './qualitative_feedback.jsx';
} from './qualitative-feedback-data.js';
import {QualitativeFeedback} from './qualitative-feedback.jsx';
import {connect} from 'react-redux';
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';
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(
Math.random() * feedbackUserRate
);
@ -35,11 +38,11 @@ const isUserEligibleForFeedback = (user, permissions, feedbackQuestionId, feedba
JSON.parse(process.env.QUALITATIVE_FEEDBACK_ACTIVE) &&
canShowFeedbackWidget(feedbackUserRate) &&
isCurrentDayInRange() &&
hasGivenFeedback(feedbackQuestionId)
notRespondedToFeedback(feedbackQuestionId)
);
const isFeedbackDisplayed = feedbacksDisplayed =>
!Object.entries(feedbacksDisplayed).some(feedback =>
Object.entries(feedbacksDisplayed).some(feedback =>
feedback.value
);
@ -51,7 +54,7 @@ export const shouldDisplayFeedbackWidget = (
feedbacksDisplayed
) => (
isUserEligibleForFeedback(user, permissions, feedbackQuestionId, feedbackUserRate) &&
isFeedbackDisplayed(feedbacksDisplayed)
!isFeedbackDisplayed(feedbacksDisplayed)
);
export const sendUserPropertiesForFeedback = (user, permissions, shouldDisplayFeedback) => {

View file

@ -17,7 +17,7 @@ export const isDateInRange = (date, startDate, endDate) => {
export const calculateAgeGroup = (birthYear, birthMonth) => {
if (!birthMonth || !birthYear) {
return '[unset]';
return '[no-data]';
}
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 = {
[QUALITATIVE_FEEDBACK_QUESTION_ID.ideasGenerator]: false,

View file

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

View file

@ -47,7 +47,7 @@ const TutorialsHighlight = require('../../components/journeys/tutorials-highligh
const {sendUserPropertiesForOnboarding, shouldDisplayOnboarding} = require('../../lib/onboarding.js');
const {triggerAnalyticsEvent} = require('../../lib/google-analytics-utils.js');
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 {displayQualitativeFeedback} = require('../../redux/qualitative-feedback.js');
const {DebuggingFeedback} = require('../../components/modal/feedback/debugging-feedback.jsx');
@ -73,7 +73,7 @@ const IntlGUIWithProjectHandler = ({...props}) => {
}, [props.projectId, prevProjectId, props.user, props.permissions]);
const displayGuiFeedback = useCallback((feedbackQuestionId, feedbackUserRate) => {
const shoulDisplayFeedback = shouldDisplayFeedbackWidget(
const shouldDisplayFeedback = shouldDisplayFeedbackWidget(
props.user,
props.permissions,
feedbackQuestionId,
@ -81,11 +81,11 @@ const IntlGUIWithProjectHandler = ({...props}) => {
props.feedback
);
if (shoulDisplayFeedback) {
if (shouldDisplayFeedback) {
sendUserPropertiesForFeedback(
props.user,
props.permissions,
shoulDisplayFeedback
shouldDisplayFeedback
);
props.displayFeedback(feedbackQuestionId);
}
@ -95,14 +95,14 @@ const IntlGUIWithProjectHandler = ({...props}) => {
<>
<IntlGUI
// eslint-disable-next-line react/jsx-no-bind
displayDebugFeedback={() => displayGuiFeedback(
onDebugModalClose={() => displayGuiFeedback(
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
displayTutorialsFeedback={() => displayGuiFeedback(
onTutorialSelect={() => displayGuiFeedback(
QUALITATIVE_FEEDBACK_QUESTION_ID.tutorials,
process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_RATE
process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_FREQUENCY
)}
{...props}
/>
@ -305,18 +305,18 @@ class Preview extends React.Component {
sendUserPropertiesForOnboarding(this.props.user, this.props.permissions);
const fromStarterProjectsPage = queryString.parse(location.search).fromStarterProjectsPage === 'true';
const shoulDisplayFeedback = shouldDisplayFeedbackWidget(
const shouldDisplayFeedback = shouldDisplayFeedbackWidget(
this.props.user,
this.props.permissions,
QUALITATIVE_FEEDBACK_QUESTION_ID.starterProjects,
process.env.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_RATE,
process.env.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_FREQUENCY,
this.props.feedback
);
if (fromStarterProjectsPage && shoulDisplayFeedback) {
if (fromStarterProjectsPage && shouldDisplayFeedback) {
sendUserPropertiesForFeedback(
this.props.user,
this.props.permissions,
shoulDisplayFeedback
shouldDisplayFeedback
);
this.props.displayFeedback(
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_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': `"${
process.env.QUALITATIVE_FEEDBACK_IDEAS_GENERATOR_USER_RATE || 2
// Given user frequency X, show qualitative feedback to 1 in X users
'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_RATE || 2
'process.env.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_FREQUENCY': `"${
process.env.QUALITATIVE_FEEDBACK_STARTER_PROJECTS_USER_FREQUENCY || 2
}"`,
'process.env.QUALITATIVE_FEEDBACK_DEBUGGING_USER_RATE': `"${
process.env.QUALITATIVE_FEEDBACK_DEBUGGING_USER_RATE || 2
'process.env.QUALITATIVE_FEEDBACK_DEBUGGING_USER_FREQUENCY': `"${
process.env.QUALITATIVE_FEEDBACK_DEBUGGING_USER_FREQUENCY || 2
}"`,
'process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_RATE': `"${
process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_RATE || 2
'process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_FREQUENCY': `"${
process.env.QUALITATIVE_FEEDBACK_TUTORIALS_USER_FREQUENCY || 2
}"`,
'process.env.IDEAS_GENERATOR_SOURCE': `"${
process.env.IDEAS_GENERATOR_SOURCE || 'https://scratch.mit.edu/projects/1108790117'