diff --git a/src/__tests__/surveys.test.ts b/src/__tests__/surveys.test.ts index 88b75af9a..11a156080 100644 --- a/src/__tests__/surveys.test.ts +++ b/src/__tests__/surveys.test.ts @@ -1,7 +1,7 @@ /// import { SURVEYS_REQUEST_TIMEOUT_MS } from '../constants' -import { generateSurveys } from '../extensions/surveys' +import { generateSurveys, getNextSurveyStep } from '../extensions/surveys' import { canActivateRepeatedly, getDisplayOrderChoices, @@ -927,8 +927,8 @@ describe('surveys', () => { { type: SurveyQuestionType.Open, question: 'Question A' }, { type: SurveyQuestionType.Open, question: 'Question B' }, ] as SurveyQuestion[] - expect(surveys.getNextSurveyStep(survey, 0, 'Some response')).toEqual(1) - expect(surveys.getNextSurveyStep(survey, 1, 'Some response')).toEqual(SurveyQuestionBranchingType.End) + expect(getNextSurveyStep(survey, 0, 'Some response')).toEqual(1) + expect(getNextSurveyStep(survey, 1, 'Some response')).toEqual(SurveyQuestionBranchingType.End) }) it('should branch out to `end`', () => { @@ -940,7 +940,7 @@ describe('surveys', () => { }, { type: SurveyQuestionType.Open, question: 'Question B' }, ] as SurveyQuestion[] - expect(surveys.getNextSurveyStep(survey, 0, 'Some response')).toEqual(SurveyQuestionBranchingType.End) + expect(getNextSurveyStep(survey, 0, 'Some response')).toEqual(SurveyQuestionBranchingType.End) }) it('should branch out to a specific question', () => { @@ -953,7 +953,7 @@ describe('surveys', () => { { type: SurveyQuestionType.Open, question: 'Question B' }, { type: SurveyQuestionType.Open, question: 'Question C' }, ] as SurveyQuestion[] - expect(surveys.getNextSurveyStep(survey, 0, 'Some response')).toEqual(2) + expect(getNextSurveyStep(survey, 0, 'Some response')).toEqual(2) }) // Single-choice @@ -973,9 +973,9 @@ describe('surveys', () => { { type: SurveyQuestionType.Open, question: 'Why no?' }, { type: SurveyQuestionType.Open, question: 'Why maybe?' }, ] as unknown[] as SurveyQuestion[] - expect(surveys.getNextSurveyStep(survey, 0, 'Yes')).toEqual(1) - expect(surveys.getNextSurveyStep(survey, 0, 'No')).toEqual(2) - expect(surveys.getNextSurveyStep(survey, 0, 'Maybe')).toEqual(3) + expect(getNextSurveyStep(survey, 0, 'Yes')).toEqual(1) + expect(getNextSurveyStep(survey, 0, 'No')).toEqual(2) + expect(getNextSurveyStep(survey, 0, 'Maybe')).toEqual(3) }) // Response-based branching, scale 1-3 @@ -995,9 +995,9 @@ describe('surveys', () => { { type: SurveyQuestionType.Open, question: 'Glad to hear that. Tell us more!' }, ] as unknown[] as SurveyQuestion[] - expect(surveys.getNextSurveyStep(survey, 0, 1)).toEqual(1) - expect(surveys.getNextSurveyStep(survey, 0, 2)).toEqual(2) - expect(surveys.getNextSurveyStep(survey, 0, 3)).toEqual(3) + expect(getNextSurveyStep(survey, 0, 1)).toEqual(1) + expect(getNextSurveyStep(survey, 0, 2)).toEqual(2) + expect(getNextSurveyStep(survey, 0, 3)).toEqual(3) }) // Response-based branching, scale 1-5 @@ -1017,9 +1017,9 @@ describe('surveys', () => { { type: SurveyQuestionType.Open, question: 'Glad to hear that. Tell us more!' }, ] as unknown as SurveyQuestion[] - expect(surveys.getNextSurveyStep(survey, 0, 1)).toEqual(1) - expect(surveys.getNextSurveyStep(survey, 0, 3)).toEqual(2) - expect(surveys.getNextSurveyStep(survey, 0, 5)).toEqual(3) + expect(getNextSurveyStep(survey, 0, 1)).toEqual(1) + expect(getNextSurveyStep(survey, 0, 3)).toEqual(2) + expect(getNextSurveyStep(survey, 0, 5)).toEqual(3) }) // Response-based branching, scale 1-7 @@ -1039,13 +1039,13 @@ describe('surveys', () => { { type: SurveyQuestionType.Open, question: 'Great! What did you enjoy the most?' }, ] as unknown as SurveyQuestion[] - expect(surveys.getNextSurveyStep(survey, 0, 1)).toEqual(1) - expect(surveys.getNextSurveyStep(survey, 0, 2)).toEqual(1) - expect(surveys.getNextSurveyStep(survey, 0, 3)).toEqual(1) - expect(surveys.getNextSurveyStep(survey, 0, 4)).toEqual(2) - expect(surveys.getNextSurveyStep(survey, 0, 5)).toEqual(3) - expect(surveys.getNextSurveyStep(survey, 0, 6)).toEqual(3) - expect(surveys.getNextSurveyStep(survey, 0, 7)).toEqual(3) + expect(getNextSurveyStep(survey, 0, 1)).toEqual(1) + expect(getNextSurveyStep(survey, 0, 2)).toEqual(1) + expect(getNextSurveyStep(survey, 0, 3)).toEqual(1) + expect(getNextSurveyStep(survey, 0, 4)).toEqual(2) + expect(getNextSurveyStep(survey, 0, 5)).toEqual(3) + expect(getNextSurveyStep(survey, 0, 6)).toEqual(3) + expect(getNextSurveyStep(survey, 0, 7)).toEqual(3) }) // Response-based branching, scale 0-10 (NPS) @@ -1065,9 +1065,9 @@ describe('surveys', () => { { type: SurveyQuestionType.Open, question: 'Glad to hear that. Tell us more!' }, ] as unknown as SurveyQuestion[] - expect(surveys.getNextSurveyStep(survey, 0, 1)).toEqual(1) - expect(surveys.getNextSurveyStep(survey, 0, 8)).toEqual(2) - expect(surveys.getNextSurveyStep(survey, 0, 10)).toEqual(3) + expect(getNextSurveyStep(survey, 0, 1)).toEqual(1) + expect(getNextSurveyStep(survey, 0, 8)).toEqual(2) + expect(getNextSurveyStep(survey, 0, 10)).toEqual(3) }) it('should display questions in the order AGCEHDFB', () => { @@ -1121,7 +1121,7 @@ describe('surveys', () => { for (let i = 0; i < survey.questions.length; i++) { const currentQuestion = survey.questions[currentStep] actualOrder.push(currentQuestion.question) - currentStep = surveys.getNextSurveyStep(survey, currentStep, 'Some response') + currentStep = getNextSurveyStep(survey, currentStep, 'Some response') } expect(desiredOrder).toEqual(actualOrder) @@ -1180,7 +1180,7 @@ describe('surveys', () => { for (const answer of answers) { const currentQuestion = survey.questions[currentStep] actualOrder.push(currentQuestion.question) - currentStep = surveys.getNextSurveyStep(survey, currentStep, answer) + currentStep = getNextSurveyStep(survey, currentStep, answer) } expect(desiredOrder).toEqual(actualOrder) expect(currentStep).toEqual(SurveyQuestionBranchingType.End) @@ -1193,7 +1193,7 @@ describe('surveys', () => { for (const answer of answers) { const currentQuestion = survey.questions[currentStep] actualOrder.push(currentQuestion.question) - currentStep = surveys.getNextSurveyStep(survey, currentStep, answer) + currentStep = getNextSurveyStep(survey, currentStep, answer) } expect(desiredOrder).toEqual(actualOrder) expect(currentStep).toEqual(SurveyQuestionBranchingType.End) @@ -1206,7 +1206,7 @@ describe('surveys', () => { for (const answer of answers) { const currentQuestion = survey.questions[currentStep] actualOrder.push(currentQuestion.question) - currentStep = surveys.getNextSurveyStep(survey, currentStep, answer) + currentStep = getNextSurveyStep(survey, currentStep, answer) } expect(desiredOrder).toEqual(actualOrder) expect(currentStep).toEqual(SurveyQuestionBranchingType.End) @@ -1223,7 +1223,7 @@ describe('surveys', () => { for (const answer of answers) { const currentQuestion = survey.questions[currentStep] actualOrder.push(currentQuestion.question) - currentStep = surveys.getNextSurveyStep(survey, currentStep, answer) + currentStep = getNextSurveyStep(survey, currentStep, answer) } expect(desiredOrder).toEqual(actualOrder) expect(currentStep).toEqual(SurveyQuestionBranchingType.End) @@ -1244,7 +1244,7 @@ describe('surveys', () => { { type: SurveyQuestionType.Open, question: 'Seems you are not completely happy. Tell us more!' }, { type: SurveyQuestionType.Open, question: 'Glad to hear that. Tell us more!' }, ] as unknown as SurveyQuestion[] - expect(() => surveys.getNextSurveyStep(survey, 0, 1)).toThrow('The scale must be one of: 3, 5, 7, 10') + expect(() => getNextSurveyStep(survey, 0, 1)).toThrow('The scale must be one of: 3, 5, 7, 10') }) it('should throw an error for a response value out of the valid range', () => { @@ -1262,13 +1262,13 @@ describe('surveys', () => { { type: SurveyQuestionType.Open, question: 'Seems you are not completely happy. Tell us more!' }, { type: SurveyQuestionType.Open, question: 'Glad to hear that. Tell us more!' }, ] as unknown as SurveyQuestion[] - expect(() => surveys.getNextSurveyStep(survey, 0, 20)).toThrow('The response must be in range 1-3') + expect(() => getNextSurveyStep(survey, 0, 20)).toThrow('The response must be in range 1-3') ;(survey.questions[0] as RatingSurveyQuestion).scale = 5 - expect(() => surveys.getNextSurveyStep(survey, 0, 20)).toThrow('The response must be in range 1-5') + expect(() => getNextSurveyStep(survey, 0, 20)).toThrow('The response must be in range 1-5') ;(survey.questions[0] as RatingSurveyQuestion).scale = 7 - expect(() => surveys.getNextSurveyStep(survey, 0, 20)).toThrow('The response must be in range 1-7') + expect(() => getNextSurveyStep(survey, 0, 20)).toThrow('The response must be in range 1-7') ;(survey.questions[0] as RatingSurveyQuestion).scale = 10 - expect(() => surveys.getNextSurveyStep(survey, 0, 20)).toThrow('The response must be in range 0-10') + expect(() => getNextSurveyStep(survey, 0, 20)).toThrow('The response must be in range 0-10') }) it('should throw an error for if a response value in a rating question is not an integer', () => { @@ -1286,10 +1286,8 @@ describe('surveys', () => { { type: SurveyQuestionType.Open, question: 'Seems you are not completely happy. Tell us more!' }, { type: SurveyQuestionType.Open, question: 'Glad to hear that. Tell us more!' }, ] as unknown as SurveyQuestion[] - expect(() => surveys.getNextSurveyStep(survey, 0, '2')).toThrow('The response type must be an integer') - expect(() => surveys.getNextSurveyStep(survey, 0, 'some_string')).toThrow( - 'The response type must be an integer' - ) + expect(() => getNextSurveyStep(survey, 0, '2')).toThrow('The response type must be an integer') + expect(() => getNextSurveyStep(survey, 0, 'some_string')).toThrow('The response type must be an integer') }) }) 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/extensions/surveys.tsx b/src/extensions/surveys.tsx index ec97061f2..5dc1d1c3c 100644 --- a/src/extensions/surveys.tsx +++ b/src/extensions/surveys.tsx @@ -44,6 +44,109 @@ const logger = createLogger('[Surveys]') const window = _window as Window & typeof globalThis const document = _document as Document +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 SurveyManager { private posthog: PostHog private surveyInFocus: string | null @@ -663,18 +766,7 @@ export function Questions({ setQuestionsResponses({ ...questionsResponses, [responseKey]: res }) - // Old SDK, no branching - if (!posthog.getNextSurveyStep) { - const isLastDisplayedQuestion = displayQuestionIndex === survey.questions.length - 1 - if (isLastDisplayedQuestion) { - sendSurveyEvent({ ...questionsResponses, [responseKey]: res }, survey, posthog) - } else { - setCurrentQuestionIndex(displayQuestionIndex + 1) - } - return - } - - const nextStep = posthog.getNextSurveyStep(survey, displayQuestionIndex, res) + const nextStep = getNextSurveyStep(survey, displayQuestionIndex, res) if (nextStep === SurveyQuestionBranchingType.End) { sendSurveyEvent({ ...questionsResponses, [responseKey]: res }, survey, posthog) } else { diff --git a/src/posthog-core.ts b/src/posthog-core.ts index 707f0ce3d..621bc3599 100644 --- a/src/posthog-core.ts +++ b/src/posthog-core.ts @@ -25,7 +25,7 @@ import { PostHogExceptions } from './posthog-exceptions' import { PostHogFeatureFlags } from './posthog-featureflags' import { PostHogPersistence } from './posthog-persistence' import { PostHogSurveys } from './posthog-surveys' -import { Survey, SurveyCallback, SurveyQuestionBranchingType } from './posthog-surveys-types' +import { SurveyCallback } from './posthog-surveys-types' import { RateLimiter } from './rate-limiter' import { RemoteConfigLoader } from './remote-config' import { extendURLParams, request, SUPPORTS_REQUEST } from './request' @@ -1343,15 +1343,6 @@ export class PostHog { this.surveys.canRenderSurvey(surveyId) } - /** Get the next step of the survey: a question index or `end` */ - getNextSurveyStep( - survey: Survey, - currentQuestionIndex: number, - response: string | string[] | number | null - ): number | SurveyQuestionBranchingType.End { - return this.surveys.getNextSurveyStep(survey, currentQuestionIndex, response) - } - /** * Identify a user with a unique ID instead of a PostHog * randomly generated distinct_id. If the method is never called, diff --git a/src/posthog-surveys.ts b/src/posthog-surveys.ts index 0c279fcf7..eb7d0896a 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, - SurveyMatchType, - SurveyQuestionBranchingType, - SurveyQuestionType, -} from './posthog-surveys-types' +import { Survey, SurveyCallback, SurveyMatchType } from './posthog-surveys-types' import { RemoteConfig } from './types' import { Info } from './utils/event-utils' import { assignableWindow, document, userAgent, window } from './utils/globals' @@ -32,109 +26,6 @@ export const surveyValidationMap: Record targets.every((target) => value !== target), } -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 -} - function defaultMatchType(matchType?: SurveyMatchType): SurveyMatchType { return matchType ?? 'icontains' } @@ -402,7 +293,6 @@ export class PostHogSurveys { return this.instance.featureFlags.isFeatureEnabled(value) }) } - getNextSurveyStep = getNextSurveyStep // this method is lazily loaded onto the window to avoid loading preact and other dependencies if surveys is not enabled private _canActivateRepeatedly(survey: Survey) {