diff --git a/src/__tests__/surveys.test.ts b/src/__tests__/surveys.test.ts index 1a465f73d..fc604e874 100644 --- a/src/__tests__/surveys.test.ts +++ b/src/__tests__/surveys.test.ts @@ -1,27 +1,26 @@ /// -import { PostHogSurveys } from '../posthog-surveys' -import { - SurveyType, - SurveyQuestionType, - Survey, - MultipleSurveyQuestion, - SurveyQuestionBranchingType, - SurveyQuestion, - RatingSurveyQuestion, -} from '../posthog-surveys-types' +import { generateSurveys, getNextSurveyStep } from '../extensions/surveys' import { canActivateRepeatedly, getDisplayOrderChoices, getDisplayOrderQuestions, } from '../extensions/surveys/surveys-utils' -import { PostHogPersistence } from '../posthog-persistence' import { PostHog } from '../posthog-core' +import { PostHogPersistence } from '../posthog-persistence' +import { PostHogSurveys } from '../posthog-surveys' +import { + MultipleSurveyQuestion, + RatingSurveyQuestion, + Survey, + SurveyQuestion, + SurveyQuestionBranchingType, + SurveyQuestionType, + SurveyType, +} from '../posthog-surveys-types' import { DecideResponse, PostHogConfig, Properties } from '../types' -import { window } from '../utils/globals' +import { assignableWindow, window } from '../utils/globals' import { RequestRouter } from '../utils/request-router' -import { assignableWindow } from '../utils/globals' -import { generateSurveys } from '../extensions/surveys' describe('surveys', () => { let config: PostHogConfig @@ -844,6 +843,9 @@ describe('surveys', () => { }) describe('branching logic', () => { + beforeEach(() => { + surveys.getNextSurveyStep = getNextSurveyStep + }) const survey: Survey = { name: 'My survey', description: '', diff --git a/src/entrypoints/surveys-preview.es.ts b/src/entrypoints/surveys-preview.es.ts index f716dcbfc..627e129b4 100644 --- a/src/entrypoints/surveys-preview.es.ts +++ b/src/entrypoints/surveys-preview.es.ts @@ -1,2 +1 @@ -export { renderFeedbackWidgetPreview, renderSurveysPreview } from '../extensions/surveys' -export { getNextSurveyStep } from '../posthog-surveys' +export { getNextSurveyStep, renderFeedbackWidgetPreview, renderSurveysPreview } from '../extensions/surveys' diff --git a/src/entrypoints/surveys.ts b/src/entrypoints/surveys.ts index 4baba2ca7..0a0fecc98 100644 --- a/src/entrypoints/surveys.ts +++ b/src/entrypoints/surveys.ts @@ -1,11 +1,12 @@ -import { generateSurveys } from '../extensions/surveys' +import { generateSurveys, getNextSurveyStep } from '../extensions/surveys' -import { assignableWindow } from '../utils/globals' import { canActivateRepeatedly } from '../extensions/surveys/surveys-utils' +import { assignableWindow } from '../utils/globals' assignableWindow.__PosthogExtensions__ = assignableWindow.__PosthogExtensions__ || {} assignableWindow.__PosthogExtensions__.canActivateRepeatedly = canActivateRepeatedly assignableWindow.__PosthogExtensions__.generateSurveys = generateSurveys +assignableWindow.__PosthogExtensions__.getNextSurveyStep = getNextSurveyStep // this used to be directly on window, but we moved it to __PosthogExtensions__ // it is still on window for backwards compatibility diff --git a/src/extensions/surveys.tsx b/src/extensions/surveys.tsx index 402da1bb4..d848e77d7 100644 --- a/src/extensions/surveys.tsx +++ b/src/extensions/surveys.tsx @@ -759,3 +759,106 @@ const getQuestionComponent = ({ return } + +function getRatingBucketForResponseValue(responseValue: number, scale: number) { + if (scale === 3) { + if (responseValue < 1 || responseValue > 3) { + throw new Error('The response must be in range 1-3') + } + + return responseValue === 1 ? 'negative' : responseValue === 2 ? 'neutral' : 'positive' + } else if (scale === 5) { + if (responseValue < 1 || responseValue > 5) { + throw new Error('The response must be in range 1-5') + } + + return responseValue <= 2 ? 'negative' : responseValue === 3 ? 'neutral' : 'positive' + } else if (scale === 7) { + if (responseValue < 1 || responseValue > 7) { + throw new Error('The response must be in range 1-7') + } + + return responseValue <= 3 ? 'negative' : responseValue === 4 ? 'neutral' : 'positive' + } else if (scale === 10) { + if (responseValue < 0 || responseValue > 10) { + throw new Error('The response must be in range 0-10') + } + + return responseValue <= 6 ? 'detractors' : responseValue <= 8 ? 'passives' : 'promoters' + } + + throw new Error('The scale must be one of: 3, 5, 7, 10') +} + +export function getNextSurveyStep( + survey: Survey, + currentQuestionIndex: number, + response: string | string[] | number | null +) { + const question = survey.questions[currentQuestionIndex] + const nextQuestionIndex = currentQuestionIndex + 1 + + if (!question.branching?.type) { + if (currentQuestionIndex === survey.questions.length - 1) { + return SurveyQuestionBranchingType.End + } + + return nextQuestionIndex + } + + if (question.branching.type === SurveyQuestionBranchingType.End) { + return SurveyQuestionBranchingType.End + } else if (question.branching.type === SurveyQuestionBranchingType.SpecificQuestion) { + if (Number.isInteger(question.branching.index)) { + return question.branching.index + } + } else if (question.branching.type === SurveyQuestionBranchingType.ResponseBased) { + // Single choice + if (question.type === SurveyQuestionType.SingleChoice) { + // :KLUDGE: for now, look up the choiceIndex based on the response + // TODO: once QuestionTypes.MultipleChoiceQuestion is refactored, pass the selected choiceIndex into this method + const selectedChoiceIndex = question.choices.indexOf(`${response}`) + + if (question.branching?.responseValues?.hasOwnProperty(selectedChoiceIndex)) { + const nextStep = question.branching.responseValues[selectedChoiceIndex] + + // Specific question + if (Number.isInteger(nextStep)) { + return nextStep + } + + if (nextStep === SurveyQuestionBranchingType.End) { + return SurveyQuestionBranchingType.End + } + + return nextQuestionIndex + } + } else if (question.type === SurveyQuestionType.Rating) { + if (typeof response !== 'number' || !Number.isInteger(response)) { + throw new Error('The response type must be an integer') + } + + const ratingBucket = getRatingBucketForResponseValue(response, question.scale) + + if (question.branching?.responseValues?.hasOwnProperty(ratingBucket)) { + const nextStep = question.branching.responseValues[ratingBucket] + + // Specific question + if (Number.isInteger(nextStep)) { + return nextStep + } + + if (nextStep === SurveyQuestionBranchingType.End) { + return SurveyQuestionBranchingType.End + } + + return nextQuestionIndex + } + } + + return nextQuestionIndex + } + + logger.warn('Falling back to next question index due to unexpected branching type') + return nextQuestionIndex +} diff --git a/src/posthog-surveys.ts b/src/posthog-surveys.ts index 6e1b167e7..816553ecc 100644 --- a/src/posthog-surveys.ts +++ b/src/posthog-surveys.ts @@ -1,13 +1,7 @@ import { SURVEYS } from './constants' import { getSurveySeenStorageKeys } from './extensions/surveys/surveys-utils' import { PostHog } from './posthog-core' -import { - Survey, - SurveyCallback, - SurveyQuestionBranchingType, - SurveyQuestionType, - SurveyUrlMatchType, -} from './posthog-surveys-types' +import { Survey, SurveyCallback, SurveyUrlMatchType } from './posthog-surveys-types' import { RemoteConfig } from './types' import { assignableWindow, document, window } from './utils/globals' import { createLogger } from './utils/logger' @@ -28,109 +22,6 @@ export const surveyUrlValidationMap: Record window?.location.href !== conditionsUrl, } -function getRatingBucketForResponseValue(responseValue: number, scale: number) { - if (scale === 3) { - if (responseValue < 1 || responseValue > 3) { - throw new Error('The response must be in range 1-3') - } - - return responseValue === 1 ? 'negative' : responseValue === 2 ? 'neutral' : 'positive' - } else if (scale === 5) { - if (responseValue < 1 || responseValue > 5) { - throw new Error('The response must be in range 1-5') - } - - return responseValue <= 2 ? 'negative' : responseValue === 3 ? 'neutral' : 'positive' - } else if (scale === 7) { - if (responseValue < 1 || responseValue > 7) { - throw new Error('The response must be in range 1-7') - } - - return responseValue <= 3 ? 'negative' : responseValue === 4 ? 'neutral' : 'positive' - } else if (scale === 10) { - if (responseValue < 0 || responseValue > 10) { - throw new Error('The response must be in range 0-10') - } - - return responseValue <= 6 ? 'detractors' : responseValue <= 8 ? 'passives' : 'promoters' - } - - throw new Error('The scale must be one of: 3, 5, 7, 10') -} - -export function getNextSurveyStep( - survey: Survey, - currentQuestionIndex: number, - response: string | string[] | number | null -) { - const question = survey.questions[currentQuestionIndex] - const nextQuestionIndex = currentQuestionIndex + 1 - - if (!question.branching?.type) { - if (currentQuestionIndex === survey.questions.length - 1) { - return SurveyQuestionBranchingType.End - } - - return nextQuestionIndex - } - - if (question.branching.type === SurveyQuestionBranchingType.End) { - return SurveyQuestionBranchingType.End - } else if (question.branching.type === SurveyQuestionBranchingType.SpecificQuestion) { - if (Number.isInteger(question.branching.index)) { - return question.branching.index - } - } else if (question.branching.type === SurveyQuestionBranchingType.ResponseBased) { - // Single choice - if (question.type === SurveyQuestionType.SingleChoice) { - // :KLUDGE: for now, look up the choiceIndex based on the response - // TODO: once QuestionTypes.MultipleChoiceQuestion is refactored, pass the selected choiceIndex into this method - const selectedChoiceIndex = question.choices.indexOf(`${response}`) - - if (question.branching?.responseValues?.hasOwnProperty(selectedChoiceIndex)) { - const nextStep = question.branching.responseValues[selectedChoiceIndex] - - // Specific question - if (Number.isInteger(nextStep)) { - return nextStep - } - - if (nextStep === SurveyQuestionBranchingType.End) { - return SurveyQuestionBranchingType.End - } - - return nextQuestionIndex - } - } else if (question.type === SurveyQuestionType.Rating) { - if (typeof response !== 'number' || !Number.isInteger(response)) { - throw new Error('The response type must be an integer') - } - - const ratingBucket = getRatingBucketForResponseValue(response, question.scale) - - if (question.branching?.responseValues?.hasOwnProperty(ratingBucket)) { - const nextStep = question.branching.responseValues[ratingBucket] - - // Specific question - if (Number.isInteger(nextStep)) { - return nextStep - } - - if (nextStep === SurveyQuestionBranchingType.End) { - return SurveyQuestionBranchingType.End - } - - return nextQuestionIndex - } - } - - return nextQuestionIndex - } - - logger.warn('Falling back to next question index due to unexpected branching type') - return nextQuestionIndex -} - export class PostHogSurveys { private _decideServerResponse?: boolean public _surveyEventReceiver: SurveyEventReceiver | null @@ -302,7 +193,13 @@ export class PostHogSurveys { return this.instance.featureFlags.isFeatureEnabled(value) }) } - getNextSurveyStep = getNextSurveyStep + getNextSurveyStep(survey: Survey, currentQuestionIndex: number, response: string | string[] | number | null) { + if (isNullish(assignableWindow.__PosthogExtensions__?.getNextSurveyStep)) { + logger.warn('init was not called') + return 0 + } + return assignableWindow.__PosthogExtensions__.getNextSurveyStep(survey, currentQuestionIndex, response) + } // this method is lazily loaded onto the window to avoid loading preact and other dependencies if surveys is not enabled private _canActivateRepeatedly(survey: Survey) { diff --git a/src/utils/globals.ts b/src/utils/globals.ts index 3daa7bb48..9bcbb9879 100644 --- a/src/utils/globals.ts +++ b/src/utils/globals.ts @@ -78,6 +78,7 @@ interface PostHogExtensions { rrwebPlugins?: { getRecordConsolePlugin: any; getRecordNetworkPlugin?: any } canActivateRepeatedly?: (survey: any) => boolean generateSurveys?: (posthog: PostHog) => any | undefined + getNextSurveyStep?: (survey: any, currentQuestionIndex: number, response: string | string[] | number | null) => any postHogWebVitalsCallbacks?: { onLCP: (metric: any) => void onCLS: (metric: any) => void