Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(surveys): Make surveys site app native to posthog-js #801

Merged
merged 8 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ export default [
],
plugins: [...plugins],
},
{
input: 'src/loader-surveys.ts',
output: [
{
file: 'dist/surveys.js',
sourcemap: true,
format: 'iife',
name: 'posthog',
},
],
plugins: [...plugins],
},
{
input: 'src/loader-globals.ts',
output: [
Expand Down
151 changes: 151 additions & 0 deletions src/__tests__/extensions/surveys.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { createShadow, callSurveys, generateSurveys } from '../../extensions/surveys'

describe('survey display logic', () => {
beforeEach(() => {
// we have to manually reset the DOM before each test
document.getElementsByTagName('html')[0].innerHTML = ''
localStorage.clear()
jest.clearAllMocks()
})

test('createShadow', () => {
const surveyId = 'randomSurveyId'
const mockShadow = createShadow(`.survey-${surveyId}-form {}`, surveyId)
expect(mockShadow.mode).toBe('open')
expect(mockShadow.host.className).toBe(`PostHogSurvey${surveyId}`)
})

let mockSurveys = [
{
id: 'testSurvey1',
name: 'Test survey 1',
appearance: null,
questions: [
{
question: 'How satisfied are you with our newest product?',
description: 'This is a question description',
type: 'rating',
display: 'number',
scale: 10,
lower_bound_label: 'Not Satisfied',
upper_bound_label: 'Very Satisfied',
},
],
},
]
const mockPostHog = {
getActiveMatchingSurveys: jest.fn().mockImplementation((callback) => callback(mockSurveys)),
get_session_replay_url: jest.fn(),
capture: jest.fn().mockImplementation((eventName) => eventName),
}

test('does not show survey to user if they have dismissed it before', () => {
expect(localStorage.getItem(`seenSurvey_${mockSurveys[0].id}`)).toBe(null)
callSurveys(mockPostHog, false)
expect(mockPostHog.capture).toBeCalledTimes(1)
expect(mockPostHog.capture).toBeCalledWith('survey shown', {
$survey_id: 'testSurvey1',
$survey_name: 'Test survey 1',
sessionRecordingUrl: undefined,
})

// now we dismiss the survey
const cancelButton = document
.getElementsByClassName(`PostHogSurvey${mockSurveys[0].id}`)[0]
.shadowRoot.querySelectorAll('.form-cancel')[0]
cancelButton.click()
expect(mockPostHog.capture).toBeCalledTimes(2)
expect(mockPostHog.capture).toBeCalledWith('survey dismissed', {
$survey_id: 'testSurvey1',
$survey_name: 'Test survey 1',
sessionRecordingUrl: undefined,
})
expect(localStorage.getItem(`seenSurvey_${mockSurveys[0].id}`)).toBe('true')

// now we clear the DOM to imitate a new page load and call surveys again, and it should not show the survey
document.getElementsByTagName('html')[0].innerHTML = ''
callSurveys(mockPostHog, false)
expect(document.getElementsByClassName(`PostHogSurvey${mockSurveys[0].id}`)[0]).toBe(undefined)
// no additional capture events are called because the survey is not shown
expect(mockPostHog.capture).toBeCalledTimes(2)
})

test('does not show survey to user if they have already completed it', () => {
expect(localStorage.getItem(`seenSurvey_${mockSurveys[0].id}`)).toBe(null)
callSurveys(mockPostHog, false)
expect(mockPostHog.capture).toBeCalledTimes(1)
expect(mockPostHog.capture).toBeCalledWith('survey shown', {
$survey_id: 'testSurvey1',
$survey_name: 'Test survey 1',
sessionRecordingUrl: undefined,
})

// submit the survey
const submitButton = document
.getElementsByClassName(`PostHogSurvey${mockSurveys[0].id}`)[0]
.shadowRoot.querySelectorAll('.rating_1')[0]
submitButton.click()
expect(mockPostHog.capture).toBeCalledTimes(2)
expect(mockPostHog.capture).toBeCalledWith('survey sent', {
$survey_id: 'testSurvey1',
$survey_name: 'Test survey 1',
$survey_question: 'How satisfied are you with our newest product?',
$survey_response: 1,
sessionRecordingUrl: undefined,
})
expect(localStorage.getItem(`seenSurvey_${mockSurveys[0].id}`)).toBe('true')

// now we clear the DOM to imitate a new page load and call surveys again, and it should not show the survey
document.getElementsByTagName('html')[0].innerHTML = ''
callSurveys(mockPostHog, false)
expect(document.getElementsByClassName(`PostHogSurvey${mockSurveys[0].id}`)[0]).toBe(undefined)
// no additional capture events are called because the survey is not shown
expect(mockPostHog.capture).toBeCalledTimes(2)
})

test('does not show survey to user if they have seen it before and survey wait period is set', () => {
mockSurveys = [
{
id: 'testSurvey2',
name: 'Test survey 2',
appearance: null,
conditions: { seenSurveyWaitPeriodInDays: 10 },
questions: [
{
question: 'How was your experience?',
description: 'This is a question description',
type: 'rating',
display: 'emoji',
scale: 5,
lower_bound_label: 'Not Good',
upper_bound_label: 'Very Good',
},
],
},
]
expect(mockSurveys[0].conditions.seenSurveyWaitPeriodInDays).toBe(10)
expect(localStorage.getItem(`seenSurvey_${mockSurveys[0].id}`)).toBe(null)
callSurveys(mockPostHog, false)
expect(mockPostHog.capture).toBeCalledTimes(1)
expect(mockPostHog.capture).toBeCalledWith('survey shown', {
$survey_id: 'testSurvey2',
$survey_name: 'Test survey 2',
sessionRecordingUrl: undefined,
})
expect(localStorage.getItem('lastSeenSurveyDate').split('T')[0]).toBe(new Date().toISOString().split('T')[0])

document.getElementsByTagName('html')[0].innerHTML = ''
callSurveys(mockPostHog, false)
expect(document.getElementsByClassName(`PostHogSurvey${mockSurveys[0].id}`)[0]).toBe(undefined)
// no additional capture events are called because the survey is not shown
expect(mockPostHog.capture).toBeCalledTimes(1)
})

test('when url changes, callSurveys runs again', () => {
jest.useFakeTimers()
jest.spyOn(global, 'setInterval')
generateSurveys(mockPostHog)
expect(mockPostHog.getActiveMatchingSurveys).toBeCalledTimes(1)
expect(setInterval).toHaveBeenLastCalledWith(expect.any(Function), 1500)
})
})
3 changes: 2 additions & 1 deletion src/__tests__/surveys.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PostHogSurveys, SurveyQuestionType, SurveyType } from '../posthog-surveys'
import { PostHogSurveys } from '../posthog-surveys'
import { SurveyType, SurveyQuestionType } from '../posthog-surveys-types'
import { PostHogPersistence } from '../posthog-persistence'

describe('surveys', () => {
Expand Down
17 changes: 17 additions & 0 deletions src/decide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,23 @@ export class Decide {
this.instance['compression'] = compression
}

// Check if recorder.js is already loaded
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const surveysGenerator = window?.generateSurveys

if (response['surveys'] && !surveysGenerator) {
loadScript(this.instance.get_config('api_host') + `/static/surveys.js`, (err) => {
if (err) {
return console.error(`Could not load surveys script`, err)
}

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.generateSurveys(this.instance)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we scope this a little more perhaps? window.extendPostHogWithSurveys or something?

Should be incredibly low chance of a clash but still. "generateSurveys" is somewhat too generic

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, will do 👍

})
}

if (response['siteApps']) {
if (this.instance.get_config('opt_in_site_apps')) {
const apiHost = this.instance.get_config('api_host')
Expand Down
Loading
Loading