From 0bcf276e891473f336c00319fadc38f975826297 Mon Sep 17 00:00:00 2001 From: Brooke Date: Tue, 6 Dec 2022 11:43:13 -0800 Subject: [PATCH 1/3] TCA-755 #comment This commit adds the basic sprig html snippet and fixes some lint issues. #time 1h --- public/index.html | 53 +++++++++------ src-ts/App.tsx | 6 +- .../learn/free-code-camp/FreeCodeCamp.tsx | 65 +++++++++++++------ 3 files changed, 83 insertions(+), 41 deletions(-) diff --git a/public/index.html b/public/index.html index 7df78700f..546e6a325 100644 --- a/public/index.html +++ b/public/index.html @@ -2,24 +2,26 @@ - - - - - - - - - - - - - - - Topcoder Top Technology Talent On-Demand + Topcoder Top Technology Talent On-Demand - -
- + + + diff --git a/src-ts/App.tsx b/src-ts/App.tsx index ce3dad70d..7a81a3fe7 100644 --- a/src-ts/App.tsx +++ b/src-ts/App.tsx @@ -7,7 +7,7 @@ import { routeContext, RouteContextData } from './lib' const App: FC<{}> = () => { - const [ready, setReady]: [boolean, Dispatch>] = useState(false) + const [ready, setReady]: [boolean, Dispatch>] = useState(false) const { allRoutes, getRouteElement }: RouteContextData = useContext(routeContext) const routeElements: Array = allRoutes @@ -19,9 +19,9 @@ const App: FC<{}> = () => { useEffect(() => { if (ready) { - document.getElementById('root')?.classList.add('app-ready'); + document.getElementById('root')?.classList.add('app-ready') } - }, [ready]); + }, [ready]) return ( <> diff --git a/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx b/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx index 0e507be63..3bf8e2612 100644 --- a/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx +++ b/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx @@ -17,12 +17,15 @@ import { LoadingSpinner, profileContext, ProfileContextData, + textFormatGetSafeString, } from '../../../lib' import { CoursesProviderData, LearnLesson, LearnModule, LearnModuleProgress, + LearnModuleStatus, + LearnUserCertificationProgress, LessonProviderData, useGetCourses, useGetLesson, @@ -52,11 +55,14 @@ const FreeCodeCamp: FC<{}> = () => { const navigate: NavigateFunction = useNavigate() const routeParams: Params = useParams() - const providerParam: string = routeParams.provider ?? '' + const providerParam: string = textFormatGetSafeString(routeParams.provider) - const [certificationParam, setCourseParam]: [string, Dispatch>] = useState(routeParams.certification ?? '') - const [moduleParam, setModuleParam]: [string, Dispatch>] = useState(routeParams.module ?? '') - const [lessonParam, setLessonParam]: [string, Dispatch>] = useState(routeParams.lesson ?? '') + const [certificationParam, setCourseParam]: [string, Dispatch>] + = useState(textFormatGetSafeString(routeParams.certification)) + const [moduleParam, setModuleParam]: [string, Dispatch>] + = useState(textFormatGetSafeString(routeParams.module)) + const [lessonParam, setLessonParam]: [string, Dispatch>] + = useState(textFormatGetSafeString(routeParams.lesson)) const { certificationProgress: certificateProgress, @@ -83,11 +89,11 @@ const FreeCodeCamp: FC<{}> = () => { const ready: boolean = profileReady && courseDataReady && lessonReady && (!isLoggedIn || progressReady) - const certification: string = lesson?.course.certification ?? '' - const module: string = lesson?.module.title ?? '' + const certification: string = textFormatGetSafeString(lesson?.course.certification) + const module: string = textFormatGetSafeString(lesson?.module.title) const breadcrumb: Array = useLearnBreadcrumb([ { - name: lesson?.course.title ?? '', + name: textFormatGetSafeString(lesson?.course.title), url: getCoursePath(providerParam, certification), }, { @@ -96,7 +102,8 @@ const FreeCodeCamp: FC<{}> = () => { }, ]) - const currentModuleData: LearnModule | undefined = useMemo(() => courseData?.modules.find(d => d.key === moduleParam), [courseData, moduleParam]) + const currentModuleData: LearnModule | undefined + = useMemo(() => courseData?.modules.find(d => d.key === moduleParam), [courseData, moduleParam]) const currentStepIndex: number = useMemo(() => { if (!currentModuleData) { @@ -155,7 +162,7 @@ const FreeCodeCamp: FC<{}> = () => { } } - function handleFccLessonReady(lessonPath: string): void { + const handleFccLessonReady: (lessonPath: string) => void = useCallback((lessonPath: string) => { const [nLessonPath, modulePath, coursePath]: Array = lessonPath.replace(/\/$/, '') .split('/') @@ -195,9 +202,10 @@ const FreeCodeCamp: FC<{}> = () => { .then(setCertificateProgress) }, 500) } - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) - function handleFccLessonComplete(challengeUuid: string): void { + const handleFccLessonComplete: (challengeUuid: string) => void = useCallback((challengeUuid: string) => { const currentLesson: { [key: string]: string } = { lesson: lessonParam, module: moduleParam, @@ -209,15 +217,29 @@ const FreeCodeCamp: FC<{}> = () => { UserCertificationUpdateProgressActions.completeLesson, currentLesson, ) - .then(setCertificateProgress) + .then((progress: LearnUserCertificationProgress) => { + + setCertificateProgress(progress) + + // if this is the last lesson of the first module, show the survey + const firstModule: LearnModuleProgress = progress.modules[0] + + if (moduleParam === firstModule.module + && firstModule.moduleStatus === LearnModuleStatus.completed) { + + // TODO: use the Sprig SDK to send the event w/the user + window.Sprig('track', 'TCA First Module Completed') + } + }) } - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) /** * Handle the navigation away from the last step of the course in the FCC frame * @returns */ - function handleFccLastLessonNavigation(): void { + const handleFccLastLessonNavigation: () => void = useCallback(() => { if (!certificateProgress) { return } @@ -236,15 +258,18 @@ const FreeCodeCamp: FC<{}> = () => { // course is not completed yet, // so we find the first incomplete lesson // and redirect user to it for a continuous flow - const firstIncompleteModule: LearnModuleProgress | undefined = certificateProgress.modules.find(m => m.completedPercentage !== 100) - const moduleLessons: Array | undefined = courseData?.modules.find(m => m.key === firstIncompleteModule?.module)?.lessons + const firstIncompleteModule: LearnModuleProgress | undefined + = certificateProgress.modules.find(m => m.completedPercentage !== 100) + const moduleLessons: Array | undefined + = courseData?.modules.find(m => m.key === firstIncompleteModule?.module)?.lessons if (!firstIncompleteModule || !moduleLessons) { // case unknown, return return } const completedLessons: Array = firstIncompleteModule.completedLessons.map(l => l.dashedName) - const firstIncompleteLesson: LearnLesson | undefined = moduleLessons.find(l => !completedLessons.includes(l.dashedName)) + const firstIncompleteLesson: LearnLesson | undefined + = moduleLessons.find(l => !completedLessons.includes(l.dashedName)) if (!firstIncompleteLesson) { // case unknown, return return @@ -258,7 +283,8 @@ const FreeCodeCamp: FC<{}> = () => { ) navigate(nextLessonPath) - } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) useEffect(() => { @@ -300,7 +326,8 @@ const FreeCodeCamp: FC<{}> = () => { useEffect(() => { if (courseDataReady && courseData) { - const moduleParamData: LearnModule = courseData.modules.find(m => m.key === moduleParam) ?? courseData.modules[0] + const moduleParamData: LearnModule = courseData.modules.find(m => m.key === moduleParam) + ?? courseData.modules[0] const lessonParamExists: boolean = !!moduleParamData?.lessons.find(l => l.dashedName === lessonParam) if (!lessonParamExists) { From 375a554c1992443bb4824b25c1d74d49d5daeee7 Mon Sep 17 00:00:00 2001 From: Brooke Date: Tue, 6 Dec 2022 14:06:30 -0800 Subject: [PATCH 2/3] TCA-755 #comment This commit adds the Sprig SDK and creates a wrapper function for surveys. #time 1h --- package.json | 1 + public/index.html | 13 ---- .../environment.default.config.ts | 3 + .../environments/environment.prod.config.ts | 3 + src-ts/lib/functions/index.ts | 1 + .../lib/functions/survey-functions/index.ts | 1 + .../survey-functions/survey.functions.tsx | 22 +++++++ src-ts/lib/global-config.model.ts | 3 + src-ts/lib/index.ts | 1 + .../learn/free-code-camp/FreeCodeCamp.tsx | 66 ++++++++++++------- yarn.lock | 5 ++ 11 files changed, 82 insertions(+), 37 deletions(-) create mode 100644 src-ts/lib/functions/survey-functions/index.ts create mode 100644 src-ts/lib/functions/survey-functions/survey.functions.tsx diff --git a/package.json b/package.json index e247c0e8e..b2e953eb2 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "dependencies": { "@datadog/browser-logs": "^4.21.2", "@heroicons/react": "^1.0.6", + "@sprig-technologies/sprig-browser": "^2.20.1", "@stripe/react-stripe-js": "1.13.0", "@stripe/stripe-js": "1.41.0", "apexcharts": "^3.36.0", diff --git a/public/index.html b/public/index.html index 546e6a325..374c41cfc 100644 --- a/public/index.html +++ b/public/index.html @@ -46,19 +46,6 @@ To begin the development, run `npm start` or `yarn start`. To create a production bundle, use `npm run build` or `yarn build`. --> - - - diff --git a/src-ts/config/environments/environment.default.config.ts b/src-ts/config/environments/environment.default.config.ts index 0c0b89f95..3ee4c6d72 100644 --- a/src-ts/config/environments/environment.default.config.ts +++ b/src-ts/config/environments/environment.default.config.ts @@ -20,6 +20,9 @@ export const EnvironmentConfigDefault: EnvironmentConfigModel = { SERVICE: 'platform-ui', }, REAUTH_OFFSET: 55, + SPRIG: { + ENVIRONMENT_ID: 'bUcousVQ0-yF', + }, // TODO: Move stripe creds to .env file STRIPE: { ADMIN_TOKEN: diff --git a/src-ts/config/environments/environment.prod.config.ts b/src-ts/config/environments/environment.prod.config.ts index c1b9d8caa..03db09f04 100644 --- a/src-ts/config/environments/environment.prod.config.ts +++ b/src-ts/config/environments/environment.prod.config.ts @@ -18,6 +18,9 @@ export const EnvironmentConfigProd: EnvironmentConfigModel = { }, DISABLED_TOOLS: [], ENV: 'prod', + SPRIG: { + ENVIRONMENT_ID: 'a-IZBZ6-r7bU', + }, // TODO: Move stripe creds to .env file STRIPE: { ADMIN_TOKEN: diff --git a/src-ts/lib/functions/index.ts b/src-ts/lib/functions/index.ts index 3bf2087c5..26d8df237 100644 --- a/src-ts/lib/functions/index.ts +++ b/src-ts/lib/functions/index.ts @@ -10,5 +10,6 @@ export * from './error-functions' export * from './file-functions' export * from './logging-functions' export * from './text-format-functions' +export * from './survey-functions' export * from './user-functions' export * from './xhr-functions' diff --git a/src-ts/lib/functions/survey-functions/index.ts b/src-ts/lib/functions/survey-functions/index.ts new file mode 100644 index 000000000..b4deb0990 --- /dev/null +++ b/src-ts/lib/functions/survey-functions/index.ts @@ -0,0 +1 @@ +export { triggerForUser as surveyTriggerForUser } from './survey.functions' diff --git a/src-ts/lib/functions/survey-functions/survey.functions.tsx b/src-ts/lib/functions/survey-functions/survey.functions.tsx new file mode 100644 index 000000000..303aa9366 --- /dev/null +++ b/src-ts/lib/functions/survey-functions/survey.functions.tsx @@ -0,0 +1,22 @@ +import { sprig, SprigAPI } from '@sprig-technologies/sprig-browser' + +import { EnvironmentConfig } from '../../../config' + +const Sprig: SprigAPI = sprig.configure({ + environmentId: EnvironmentConfig.SPRIG.ENVIRONMENT_ID, +}) + +export function triggerForUser(surveyName: string, userId?: number): void { + + if (!userId) { + Sprig.track(surveyName) + return + } + + Sprig.identifyAndTrack({ + anonymousId: '', + eventName: surveyName, + metadata: undefined, + userId: `${userId}`, + }) +} diff --git a/src-ts/lib/global-config.model.ts b/src-ts/lib/global-config.model.ts index ddf9b8ce4..8d5576c9b 100644 --- a/src-ts/lib/global-config.model.ts +++ b/src-ts/lib/global-config.model.ts @@ -16,6 +16,9 @@ export interface GlobalConfig { SERVICE: string } REAUTH_OFFSET: number + SPRIG: { + ENVIRONMENT_ID: string + } STRIPE: { ADMIN_TOKEN: string API_KEY: string diff --git a/src-ts/lib/index.ts b/src-ts/lib/index.ts index ea3a4dd85..99e14a759 100644 --- a/src-ts/lib/index.ts +++ b/src-ts/lib/index.ts @@ -18,6 +18,7 @@ export { logError, logInfo, logInitialize, + surveyTriggerForUser, textFormatDateLocaleShortString, textFormatGetSafeString, textFormatMoneyLocaleString, diff --git a/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx b/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx index 3bf8e2612..3ecf233fa 100644 --- a/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx +++ b/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx @@ -17,6 +17,7 @@ import { LoadingSpinner, profileContext, ProfileContextData, + surveyTriggerForUser, textFormatGetSafeString, } from '../../../lib' import { @@ -203,43 +204,55 @@ const FreeCodeCamp: FC<{}> = () => { }, 500) } // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [ + certificateProgress, + lesson?.course.certificationId, + lesson?.course.id, + profile?.userId, + ]) const handleFccLessonComplete: (challengeUuid: string) => void = useCallback((challengeUuid: string) => { + const currentLesson: { [key: string]: string } = { lesson: lessonParam, module: moduleParam, uuid: challengeUuid, } - if (certificateProgress) { - userCertificationProgressUpdateAsync( - certificateProgress.id, - UserCertificationUpdateProgressActions.completeLesson, - currentLesson, - ) - .then((progress: LearnUserCertificationProgress) => { - setCertificateProgress(progress) + if (!certificateProgress) { + return + } - // if this is the last lesson of the first module, show the survey - const firstModule: LearnModuleProgress = progress.modules[0] + userCertificationProgressUpdateAsync( + certificateProgress.id, + UserCertificationUpdateProgressActions.completeLesson, + currentLesson, + ) + .then((progress: LearnUserCertificationProgress) => { - if (moduleParam === firstModule.module - && firstModule.moduleStatus === LearnModuleStatus.completed) { + setCertificateProgress(progress) - // TODO: use the Sprig SDK to send the event w/the user - window.Sprig('track', 'TCA First Module Completed') - } - }) - } + // if this is the last lesson of the first module, show the survey + const firstModule: LearnModuleProgress = progress.modules[0] + if (moduleParam === firstModule.module + && firstModule.moduleStatus === LearnModuleStatus.completed) { + + surveyTriggerForUser('TCA First Module Completed', profile?.userId) + } + }) // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [ + certificateProgress, + lessonParam, + moduleParam, + ]) /** * Handle the navigation away from the last step of the course in the FCC frame * @returns */ const handleFccLastLessonNavigation: () => void = useCallback(() => { + if (!certificateProgress) { return } @@ -284,7 +297,12 @@ const FreeCodeCamp: FC<{}> = () => { navigate(nextLessonPath) // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) + }, [ + certificateProgress, + certificationParam, + courseData?.modules, + providerParam, + ]) useEffect(() => { @@ -315,13 +333,13 @@ const FreeCodeCamp: FC<{}> = () => { ) navigate(completedPath) }) + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ certificateProgress, certificationParam, - navigate, - providerParam, profile?.handle, - setCertificateProgress, + profile?.userId, + providerParam, ]) useEffect(() => { @@ -341,13 +359,13 @@ const FreeCodeCamp: FC<{}> = () => { navigate(lessonPath) } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ certificationParam, courseData, courseDataReady, lessonParam, moduleParam, - navigate, providerParam, ]) diff --git a/yarn.lock b/yarn.lock index 9ebde0202..76fe3a862 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2070,6 +2070,11 @@ dependencies: "@sinonjs/commons" "^1.7.0" +"@sprig-technologies/sprig-browser@^2.20.1": + version "2.20.1" + resolved "https://registry.yarnpkg.com/@sprig-technologies/sprig-browser/-/sprig-browser-2.20.1.tgz#4c655052dc02514370e92886724cc740337b7d16" + integrity sha512-Qkt1yEhSz9FIXpDMjMzWklscF0HmKCmLebYDvVpJxqbEeYzCEQjzeD4WW5RTpvCgyuwzXT0At4Xx9UijCWAS8Q== + "@stripe/react-stripe-js@1.13.0": version "1.13.0" resolved "https://registry.yarnpkg.com/@stripe/react-stripe-js/-/react-stripe-js-1.13.0.tgz#8e0bde2b116870d2aca52976f46a92d81983e235" From a19ad9694d13ce4b21a7e410c6c4b26643068fc2 Mon Sep 17 00:00:00 2001 From: Brooke Date: Tue, 6 Dec 2022 16:28:04 -0800 Subject: [PATCH 3/3] TCA-755 #comment Update the trigger to run the first time any module is completed --- .../learn/free-code-camp/FreeCodeCamp.tsx | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx b/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx index 3ecf233fa..33829d44c 100644 --- a/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx +++ b/src-ts/tools/learn/free-code-camp/FreeCodeCamp.tsx @@ -223,6 +223,10 @@ const FreeCodeCamp: FC<{}> = () => { return } + // get the current module as it exists before it's completed + const currentModule: LearnModuleProgress | undefined = getModuleFromProgress(certificateProgress) + const certWasInProgress: boolean = currentModule?.moduleStatus !== LearnModuleStatus.completed + userCertificationProgressUpdateAsync( certificateProgress.id, UserCertificationUpdateProgressActions.completeLesson, @@ -231,14 +235,8 @@ const FreeCodeCamp: FC<{}> = () => { .then((progress: LearnUserCertificationProgress) => { setCertificateProgress(progress) + handleSurvey(certWasInProgress, progress) - // if this is the last lesson of the first module, show the survey - const firstModule: LearnModuleProgress = progress.modules[0] - if (moduleParam === firstModule.module - && firstModule.moduleStatus === LearnModuleStatus.completed) { - - surveyTriggerForUser('TCA First Module Completed', profile?.userId) - } }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [ @@ -247,6 +245,37 @@ const FreeCodeCamp: FC<{}> = () => { moduleParam, ]) + function getModuleFromProgress(certProgress: LearnUserCertificationProgress): + LearnModuleProgress | undefined { + + return certProgress.modules.find(m => m.module === moduleParam) + } + + function handleSurvey(certWasInProgress: boolean, progress: LearnUserCertificationProgress): void { + + // if the current module wasn't in progress, there's nothing to do + if (!certWasInProgress) { + return + } + + // if the updated module isn't completed now, there's nothing to do + const moduleResult: LearnModuleProgress | undefined = getModuleFromProgress(progress) + if (moduleResult?.moduleStatus !== LearnModuleStatus.completed) { + return + } + + // if there are any other modules that have been completed, there's nothing to do + if (progress.modules + .some(m => m.module !== moduleParam && m.moduleStatus === LearnModuleStatus.completed) + ) { + return + } + + // this is the last lesson to be completed in the first module completed, + // so it's good to show the trigger + surveyTriggerForUser('TCA First Module Completed', profile?.userId) + } + /** * Handle the navigation away from the last step of the course in the FCC frame * @returns