diff --git a/cypress.config.ts b/cypress.config.ts
index 60b03cffc..402c5664d 100644
--- a/cypress.config.ts
+++ b/cypress.config.ts
@@ -4,11 +4,6 @@ export default defineConfig({
defaultCommandTimeout: 2000,
numTestsKeptInMemory: 0,
e2e: {
- // We've imported your old cypress plugins here.
- // You may want to clean this up later by importing these.
- setupNodeEvents(on, config) {
- // eslint-disable-next-line @typescript-eslint/no-var-requires
- return require('./cypress/plugins/index.js')(on, config)
- },
+ specPattern: 'cypress/e2e/**/*.cy.{js,ts}',
},
})
diff --git a/cypress/e2e/capture.cy.js b/cypress/e2e/capture.cy.ts
similarity index 72%
rename from cypress/e2e/capture.cy.js
rename to cypress/e2e/capture.cy.ts
index 1087803fe..349c70e4c 100644
--- a/cypress/e2e/capture.cy.js
+++ b/cypress/e2e/capture.cy.ts
@@ -1,39 +1,15 @@
///
+// @ts-expect-error - you totally can import the package JSON
import { version } from '../../package.json'
import { getBase64EncodedPayload, getGzipEncodedPayload } from '../support/compression'
+import { start } from '../support/setup'
const urlWithVersion = new RegExp(`&ver=${version}`)
describe('Event capture', () => {
- given('options', () => ({}))
- given('sessionRecording', () => false)
- given('supportedCompression', () => ['gzip-js'])
-
- // :TRICKY: Use a custom start command over beforeEach to deal with given2 not being ready yet.
- const start = ({ waitForDecide = true } = {}) => {
- cy.intercept('POST', '**/decide/*', {
- config: {
- enable_collect_everything: true,
- },
- editorParams: {},
- featureFlags: ['session-recording-player'],
- isAuthenticated: false,
- sessionRecording: given.sessionRecording,
- supportedCompression: given.supportedCompression,
- excludedDomains: [],
- autocaptureExceptions: false,
- }).as('decide')
-
- cy.visit('./playground/cypress-full')
- cy.posthogInit(given.options)
- if (waitForDecide) {
- cy.wait('@decide')
- }
- }
-
it('captures pageviews, autocapture, custom events', () => {
- start()
+ start({})
cy.get('[data-cy-custom-event-button]').click()
cy.phCaptures().should('have.length', 3)
@@ -51,12 +27,13 @@ describe('Event capture', () => {
describe('autocapture config', () => {
it('dont capture click when configured not to', () => {
- given('options', () => ({
- autocapture: {
- dom_event_allowlist: ['change'],
+ start({
+ options: {
+ autocapture: {
+ dom_event_allowlist: ['change'],
+ },
},
- }))
- start()
+ })
cy.get('[data-cy-custom-event-button]').click()
cy.phCaptures().should('have.length', 2)
@@ -65,12 +42,13 @@ describe('Event capture', () => {
})
it('capture clicks when configured to', () => {
- given('options', () => ({
- autocapture: {
- dom_event_allowlist: ['click'],
+ start({
+ options: {
+ autocapture: {
+ dom_event_allowlist: ['click'],
+ },
},
- }))
- start()
+ })
cy.get('[data-cy-custom-event-button]').click()
cy.phCaptures().should('have.length', 3)
@@ -80,12 +58,13 @@ describe('Event capture', () => {
})
it('collect on url', () => {
- given('options', () => ({
- autocapture: {
- url_allowlist: ['.*playground/cypress'],
+ start({
+ options: {
+ autocapture: {
+ url_allowlist: ['.*playground/cypress'],
+ },
},
- }))
- start()
+ })
cy.get('[data-cy-custom-event-button]').click()
cy.phCaptures().should('have.length', 3)
@@ -95,12 +74,13 @@ describe('Event capture', () => {
})
it('dont collect on url', () => {
- given('options', () => ({
- autocapture: {
- url_allowlist: ['.*dontcollect'],
+ start({
+ options: {
+ autocapture: {
+ url_allowlist: ['.*dontcollect'],
+ },
},
- }))
- start()
+ })
cy.get('[data-cy-custom-event-button]').click()
cy.phCaptures().should('have.length', 2)
@@ -109,12 +89,13 @@ describe('Event capture', () => {
})
it('collect button elements', () => {
- given('options', () => ({
- autocapture: {
- element_allowlist: ['button'],
+ start({
+ options: {
+ autocapture: {
+ element_allowlist: ['button'],
+ },
},
- }))
- start()
+ })
cy.get('[data-cy-custom-event-button]').click()
cy.phCaptures().should('have.length', 3)
@@ -124,12 +105,13 @@ describe('Event capture', () => {
})
it('dont collect on button elements', () => {
- given('options', () => ({
- autocapture: {
- element_allowlist: ['a'],
+ start({
+ options: {
+ autocapture: {
+ element_allowlist: ['a'],
+ },
},
- }))
- start()
+ })
cy.get('[data-cy-custom-event-button]').click()
cy.phCaptures().should('have.length', 2)
@@ -138,12 +120,13 @@ describe('Event capture', () => {
})
it('collect with data attribute', () => {
- given('options', () => ({
- autocapture: {
- css_attribute_allowlist: ['[data-cy-custom-event-button]'],
+ start({
+ options: {
+ autocapture: {
+ css_selector_allowlist: ['[data-cy-custom-event-button]'],
+ },
},
- }))
- start()
+ })
cy.get('[data-cy-custom-event-button]').click()
cy.phCaptures().should('have.length', 3)
@@ -153,12 +136,13 @@ describe('Event capture', () => {
})
it('dont collect with data attribute', () => {
- given('options', () => ({
- autocapture: {
- css_selector_allowlist: ['[nope]'],
+ start({
+ options: {
+ autocapture: {
+ css_selector_allowlist: ['[nope]'],
+ },
},
- }))
- start()
+ })
cy.get('[data-cy-custom-event-button]').click()
cy.phCaptures().should('have.length', 2)
@@ -168,7 +152,7 @@ describe('Event capture', () => {
})
it('captures $feature_flag_called', () => {
- start()
+ start({})
cy.get('[data-cy-feature-flag-button]').click()
@@ -176,9 +160,7 @@ describe('Event capture', () => {
})
it('captures rage clicks', () => {
- given('options', () => ({ rageclick: true }))
-
- start()
+ start({ options: { rageclick: true } })
cy.get('body').click(100, 100).click(98, 102).click(101, 103)
@@ -186,14 +168,14 @@ describe('Event capture', () => {
})
describe('group analytics', () => {
- given('options', () => ({
- loaded: (posthog) => {
- posthog.group('company', 'id:5')
- },
- }))
-
it('includes group information in all event payloads', () => {
- start()
+ start({
+ options: {
+ loaded: (posthog) => {
+ posthog.group('company', 'id:5')
+ },
+ },
+ })
cy.get('[data-cy-custom-event-button]').click()
@@ -204,9 +186,7 @@ describe('Event capture', () => {
})
it('doesnt capture rage clicks when autocapture is disabled', () => {
- given('options', () => ({ rageclick: true, autocapture: false }))
-
- start()
+ start({ options: { rageclick: true, autocapture: false } })
cy.get('body').click(100, 100).click(98, 102).click(101, 103)
@@ -214,13 +194,14 @@ describe('Event capture', () => {
})
it('makes a single decide request', () => {
- start()
+ start({})
cy.get('@decide.all').then((calls) => {
expect(calls.length).to.equal(1)
})
cy.phCaptures().should('include', '$pageview')
+ // @ts-expect-error - TS is wrong that get returns HTMLElement here
cy.get('@decide').should(({ request }) => {
const payload = getBase64EncodedPayload(request)
expect(payload.token).to.equal('test_token')
@@ -228,36 +209,9 @@ describe('Event capture', () => {
})
})
- describe('session recording enabled from API', () => {
- given('sessionRecording', () => ({
- endpoint: '/ses/',
- }))
-
- it('captures $snapshot events', () => {
- start()
- // de-flake the test
- cy.wait(100)
- cy.phCaptures().should('include', '$snapshot')
- })
-
- describe('but disabled from config', () => {
- given('options', () => ({ disable_session_recording: true }))
-
- it('does not capture $snapshot events', () => {
- start()
-
- cy.wait(1000)
-
- cy.phCaptures().should('not.include', '$snapshot')
- })
- })
- })
-
describe('opting out of autocapture', () => {
- given('options', () => ({ autocapture: false }))
-
it('captures pageviews, custom events', () => {
- start({ waitForDecide: false })
+ start({ options: { autocapture: false }, waitForDecide: false })
cy.wait(50)
cy.get('[data-cy-custom-event-button]').click()
@@ -266,6 +220,7 @@ describe('Event capture', () => {
cy.phCaptures().should('include', 'custom-event')
cy.wait('@capture')
+ // @ts-expect-error - TS is wrong that get returns HTMLElement here
cy.get('@capture').should(({ request }) => {
const captures = getBase64EncodedPayload(request)
@@ -275,10 +230,8 @@ describe('Event capture', () => {
})
describe('opting out of pageviews', () => {
- given('options', () => ({ capture_pageview: false }))
-
it('captures autocapture, custom events', () => {
- start()
+ start({ options: { capture_pageview: false } })
cy.get('[data-cy-custom-event-button]').click()
cy.reload()
@@ -291,7 +244,7 @@ describe('Event capture', () => {
describe('user opts out after start', () => {
it('does not send any autocapture/custom events after that', () => {
- start()
+ start({})
cy.posthog().invoke('opt_out_capturing')
@@ -303,9 +256,13 @@ describe('Event capture', () => {
})
it('does not send session recording events', () => {
- given('sessionRecording', () => true)
-
- start()
+ start({
+ decideResponseOverrides: {
+ sessionRecording: {
+ endpoint: '/ses/',
+ },
+ },
+ })
cy.posthog().invoke('opt_out_capturing')
cy.resetPhCaptures()
@@ -318,14 +275,14 @@ describe('Event capture', () => {
describe('decoding the payload', () => {
describe('gzip-js supported', () => {
it('contains the correct payload after an event', () => {
- start()
+ start({})
// Pageview will be sent immediately
cy.wait('@capture').should(({ request }) => {
expect(request.headers['content-type']).to.eql('application/x-www-form-urlencoded')
expect(request.url).to.match(urlWithVersion)
const data = decodeURIComponent(request.body.match(/data=(.*)/)[1])
- const captures = JSON.parse(Buffer.from(data, 'base64'))
+ const captures = JSON.parse(Buffer.from(data, 'base64').toString())
expect(captures['event']).to.equal('$pageview')
})
@@ -351,9 +308,8 @@ describe('Event capture', () => {
})
describe('advanced_disable_decide config', () => {
- given('options', () => ({ advanced_disable_decide: true }))
it('does not autocapture anything when /decide is disabled', () => {
- start({ waitForDecide: false })
+ start({ options: { advanced_disable_decide: true }, waitForDecide: false })
cy.get('body').click(100, 100).click(98, 102).click(101, 103)
cy.get('[data-cy-custom-event-button]').click()
@@ -365,7 +321,7 @@ describe('Event capture', () => {
})
it('does not capture session recordings', () => {
- start({ waitForDecide: false })
+ start({ options: { advanced_disable_decide: true }, waitForDecide: false })
cy.get('[data-cy-custom-event-button]').click()
cy.wait('@capture')
@@ -383,21 +339,22 @@ describe('Event capture', () => {
})
describe('subsequent decide calls', () => {
- given('options', () => ({
- loaded: (posthog) => {
- posthog.identify('new-id')
- posthog.group('company', 'id:5', { id: 5, company_name: 'Awesome Inc' })
- posthog.group('playlist', 'id:77', { length: 8 })
- },
- }))
-
it('makes a single decide request on start', () => {
- start()
+ start({
+ options: {
+ loaded: (posthog) => {
+ posthog.identify('new-id')
+ posthog.group('company', 'id:5', { id: 5, company_name: 'Awesome Inc' })
+ posthog.group('playlist', 'id:77', { length: 8 })
+ },
+ },
+ })
cy.get('@decide.all').then((calls) => {
expect(calls.length).to.equal(1)
})
+ // @ts-expect-error - TS is wrong that get returns HTMLElement here
cy.get('@decide').should(({ request }) => {
const payload = getBase64EncodedPayload(request)
expect(payload).to.deep.equal({
@@ -417,7 +374,15 @@ describe('Event capture', () => {
})
it('does a single decide call on following changes', () => {
- start()
+ start({
+ options: {
+ loaded: (posthog) => {
+ posthog.identify('new-id')
+ posthog.group('company', 'id:5', { id: 5, company_name: 'Awesome Inc' })
+ posthog.group('playlist', 'id:77', { length: 8 })
+ },
+ },
+ })
cy.wait(200)
cy.get('@decide.all').then((calls) => {
diff --git a/cypress/e2e/identify.cy.js b/cypress/e2e/identify.cy.ts
similarity index 89%
rename from cypress/e2e/identify.cy.js
rename to cypress/e2e/identify.cy.ts
index 1f59bbc7e..92ae88711 100644
--- a/cypress/e2e/identify.cy.js
+++ b/cypress/e2e/identify.cy.ts
@@ -1,24 +1,16 @@
///
-function setup(initOptions) {
- cy.visit('./playground/cypress-full')
- cy.posthogInit({ ...initOptions })
- // reset and clear device ID so that we can test the uuid format
- // without worrying about the previous test's device ID
- cy.posthog().invoke('reset', true)
- cy.wait('@decide')
-}
+import { start } from '../support/setup'
describe('identify()', () => {
beforeEach(() => {
- setup()
+ start({})
})
it('uses the v7 uuid format', () => {
cy.posthog().invoke('capture', 'an-anonymous-event')
cy.phCaptures({ full: true }).then((events) => {
- cy.log(events)
- let deviceIds = new Set(events.map((e) => e.properties['$device_id']))
+ const deviceIds = new Set(events.map((e) => e.properties['$device_id']))
expect(deviceIds.size).to.eql(1)
const [deviceId] = deviceIds
expect(deviceId.length).to.be.eql(36)
diff --git a/cypress/e2e/opting-out.cy.ts b/cypress/e2e/opting-out.cy.ts
new file mode 100644
index 000000000..23715a6cf
--- /dev/null
+++ b/cypress/e2e/opting-out.cy.ts
@@ -0,0 +1,122 @@
+import { assertWhetherPostHogRequestsWereCalled } from '../support/assertions'
+
+describe('opting out', () => {
+ describe('session recording', () => {
+ beforeEach(() => {
+ cy.intercept('POST', '**/decide/*', {
+ config: { enable_collect_everything: false },
+ editorParams: {},
+ featureFlags: ['session-recording-player'],
+ isAuthenticated: false,
+ sessionRecording: {
+ endpoint: '/ses/',
+ },
+ capture_performance: true,
+ }).as('decide')
+
+ cy.visit('./playground/cypress')
+ })
+
+ it('does not capture events without init', () => {
+ cy.get('[data-cy-input]').type('hello world! ')
+
+ assertWhetherPostHogRequestsWereCalled({
+ '@recorder': false,
+ '@decide': false,
+ '@session-recording': false,
+ })
+
+ cy.get('[data-cy-input]')
+ .type('hello posthog!')
+ .then(() => {
+ cy.phCaptures({ full: true }).then((captures) => {
+ expect(captures || []).to.deep.equal([])
+ })
+ })
+ })
+
+ it('does not capture events when config opts out by default', () => {
+ cy.posthogInit({ opt_out_capturing_by_default: true })
+
+ assertWhetherPostHogRequestsWereCalled({
+ '@recorder': false,
+ '@decide': true,
+ '@session-recording': false,
+ })
+
+ cy.get('[data-cy-input]')
+ .type('hello posthog!')
+ .then(() => {
+ cy.phCaptures({ full: true }).then((captures) => {
+ expect(captures || []).to.deep.equal([])
+ })
+ })
+ })
+
+ it('does not capture recordings when config disables session recording', () => {
+ cy.posthogInit({ disable_session_recording: true })
+
+ assertWhetherPostHogRequestsWereCalled({
+ '@recorder': false,
+ '@decide': true,
+ '@session-recording': false,
+ })
+
+ cy.get('[data-cy-input]')
+ .type('hello posthog!')
+ .then(() => {
+ cy.phCaptures().then((captures) => {
+ expect(captures || []).to.deep.equal(['$pageview'])
+ })
+ })
+ })
+
+ // TODO: after opting in the onCapture hook isn't being called
+ // so we're not able to assert on behaviour anymore
+ // but observing it all works ok
+ it.skip('can start recording after starting opted out', () => {
+ cy.posthogInit({ opt_out_capturing_by_default: true })
+
+ assertWhetherPostHogRequestsWereCalled({
+ '@recorder': false,
+ '@decide': true,
+ '@session-recording': false,
+ })
+
+ cy.posthog().invoke('opt_in_capturing')
+ // TODO: should we require this call?
+ cy.posthog().invoke('startSessionRecording')
+
+ cy.phCaptures({ full: true }).then((captures) => {
+ expect((captures || []).map((c) => c.event)).to.deep.equal(['$opt_in'])
+ })
+
+ assertWhetherPostHogRequestsWereCalled({
+ '@recorder': true,
+ '@decide': true,
+ // no call to session-recording yet
+ })
+
+ cy.resetPhCaptures()
+
+ cy.get('[data-cy-input]')
+ .type('hello posthog!')
+ .wait(200)
+ .then(() => {
+ cy.phCaptures({ full: true }).then((captures) => {
+ // should be a pageview and a $snapshot
+ expect(captures.map((c) => c.event)).to.deep.equal(['$snapshot'])
+
+ expect(captures[1]['properties']['$snapshot_data']).to.have.length.above(33).and.below(38)
+ // a meta and then a full snapshot
+ expect(captures[1]['properties']['$snapshot_data'][0].type).to.equal(4) // meta
+ expect(captures[1]['properties']['$snapshot_data'][1].type).to.equal(2) // full_snapshot
+ expect(captures[1]['properties']['$snapshot_data'][2].type).to.equal(5) // custom event with options
+ // Making a set from the rest should all be 3 - incremental snapshots
+ const incrementalSnapshots = captures[1]['properties']['$snapshot_data'].slice(3)
+ expect(new Set(incrementalSnapshots.map((s) => s.type))).to.deep.equal(new Set([3]))
+ })
+ })
+ })
+ })
+})
diff --git a/cypress/e2e/session-recording.cy.js b/cypress/e2e/session-recording.cy.ts
similarity index 90%
rename from cypress/e2e/session-recording.cy.js
rename to cypress/e2e/session-recording.cy.ts
index 83bc810a5..6b033503f 100644
--- a/cypress/e2e/session-recording.cy.js
+++ b/cypress/e2e/session-recording.cy.ts
@@ -1,35 +1,22 @@
///
import { _isNull } from '../../src/utils/type-utils'
-
-function onPageLoad() {
- cy.posthogInit(given.options)
- cy.wait('@decide')
- cy.wait('@recorder')
-}
+import { start } from '../support/setup'
describe('Session recording', () => {
- given('options', () => ({}))
-
describe('array.full.js', () => {
- beforeEach(() => {
- cy.intercept('POST', '**/decide/*', {
- config: { enable_collect_everything: false },
- editorParams: {},
- featureFlags: ['session-recording-player'],
- isAuthenticated: false,
- sessionRecording: {
- endpoint: '/ses/',
+ it('captures session events', () => {
+ start({
+ decideResponseOverrides: {
+ config: { enable_collect_everything: false },
+ isAuthenticated: false,
+ sessionRecording: {
+ endpoint: '/ses/',
+ },
+ capturePerformance: true,
},
- capture_performance: true,
- }).as('decide')
-
- cy.visit('./playground/cypress-full')
- cy.posthogInit(given.options)
- cy.wait('@decide')
- })
+ })
- it('captures session events', () => {
cy.get('[data-cy-input]').type('hello world! ')
cy.wait(500)
cy.get('[data-cy-input]')
@@ -55,20 +42,18 @@ describe('Session recording', () => {
describe('array.js', () => {
beforeEach(() => {
- cy.intercept('POST', '**/decide/*', {
- config: { enable_collect_everything: false },
- editorParams: {},
- featureFlags: ['session-recording-player'],
- isAuthenticated: false,
- sessionRecording: {
- endpoint: '/ses/',
+ start({
+ decideResponseOverrides: {
+ config: { enable_collect_everything: false },
+ isAuthenticated: false,
+ sessionRecording: {
+ endpoint: '/ses/',
+ },
+ capturePerformance: true,
},
- supportedCompression: ['gzip', 'lz64'],
- capture_performance: true,
- }).as('decide')
-
- cy.visit('./playground/cypress')
- onPageLoad()
+ url: './playground/cypress',
+ })
+ cy.wait('@recorder')
})
it('captures session events', () => {
@@ -95,7 +80,7 @@ describe('Session recording', () => {
})
it('captures snapshots when the mouse moves', () => {
- let sessionId = null
+ let sessionId: string | null = null
// cypress time handling can confuse when to run full snapshot, let's force that to happen...
cy.get('[data-cy-input]').type('hello world! ')
@@ -158,7 +143,7 @@ describe('Session recording', () => {
})
it('continues capturing to the same session when the page reloads', () => {
- let sessionId = null
+ let sessionId: string | null = null
// cypress time handling can confuse when to run full snapshot, let's force that to happen...
cy.get('[data-cy-input]').type('hello world! ')
@@ -178,7 +163,9 @@ describe('Session recording', () => {
cy.resetPhCaptures()
// and refresh the page
cy.reload()
- onPageLoad()
+ cy.posthogInit({})
+ cy.wait('@decide')
+ cy.wait('@recorder')
cy.get('body')
.trigger('mousemove', { clientX: 200, clientY: 300 })
@@ -231,7 +218,7 @@ describe('Session recording', () => {
})
it('rotates sessions after 24 hours', () => {
- let firstSessionId = null
+ let firstSessionId: string | null = null
// first we start a session and give it some activity
cy.get('[data-cy-input]').type('hello world! ')
diff --git a/cypress/e2e/surveys.cy.js b/cypress/e2e/surveys.cy.ts
similarity index 98%
rename from cypress/e2e/surveys.cy.js
rename to cypress/e2e/surveys.cy.ts
index 789aea9ab..24bb76955 100644
--- a/cypress/e2e/surveys.cy.js
+++ b/cypress/e2e/surveys.cy.ts
@@ -1,14 +1,13 @@
///
import { getBase64EncodedPayload } from '../support/compression'
-function onPageLoad() {
- cy.posthogInit(given.options)
+function onPageLoad(options = {}) {
+ cy.posthogInit(options)
cy.wait('@decide')
cy.wait('@surveys')
}
describe('Surveys', () => {
- given('options', () => ({}))
beforeEach(() => {
cy.intercept('POST', '**/decide/*', {
config: { enable_collect_everything: false },
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
deleted file mode 100644
index 6d7d44d5f..000000000
--- a/cypress/plugins/index.js
+++ /dev/null
@@ -1,21 +0,0 @@
-///
-// ***********************************************************
-// This example plugins/index.js can be used to load plugins
-//
-// You can change the location of this file or turn off loading
-// the plugins file with the 'pluginsFile' configuration option.
-//
-// You can read more here:
-// https://on.cypress.io/plugins-guide
-// ***********************************************************
-
-// This function is called when a project is opened or re-opened (e.g. due to
-// the project's config changing)
-
-/**
- * @type {Cypress.PluginConfig}
- */
-module.exports = (on, config) => {
- // `on` is used to hook into various events Cypress emits
- // `config` is the resolved Cypress config
-}
diff --git a/cypress/support/assertions.ts b/cypress/support/assertions.ts
new file mode 100644
index 000000000..506b2d737
--- /dev/null
+++ b/cypress/support/assertions.ts
@@ -0,0 +1,18 @@
+/**
+ * Receives an object with keys as the name of the route and values as whether the route should have been called.
+ * e.g. { '@recorder': true, '@decide': false }
+ * the keys must match a `cy.intercept` alias
+ **/
+export function assertWhetherPostHogRequestsWereCalled(expectedCalls: Record) {
+ cy.wait(200)
+
+ for (const [key, value] of Object.entries(expectedCalls)) {
+ cy.get(key).then((interceptions) => {
+ if (value) {
+ expect(interceptions).to.be.an('object')
+ } else {
+ expect(interceptions).not.to.be.an('object')
+ }
+ })
+ }
+}
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
deleted file mode 100644
index 4c686d983..000000000
--- a/cypress/support/commands.js
+++ /dev/null
@@ -1,60 +0,0 @@
-// ***********************************************
-// This example commands.js shows you how to
-// create various custom commands and overwrite
-// existing commands.
-//
-// For more comprehensive examples of custom
-// commands please read more here:
-// https://on.cypress.io/custom-commands
-// ***********************************************
-//
-//
-// -- This is a parent command --
-// Cypress.Commands.add("login", (email, password) => { ... })
-//
-//
-// -- This is a child command --
-// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
-//
-//
-// -- This is a dual command --
-// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
-//
-//
-// -- This will overwrite an existing command --
-// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
-
-let $captures, $fullCaptures
-
-Cypress.Commands.add('posthog', () => cy.window().then(($window) => $window.posthog))
-
-Cypress.Commands.add('posthogInit', (options) => {
- $captures = []
- $fullCaptures = []
-
- cy.posthog().invoke('init', 'test_token', {
- api_host: location.origin,
- debug: true,
- _onCapture: (event, eventData) => {
- $captures.push(event)
- $fullCaptures.push(eventData)
- },
- ...options,
- })
-})
-
-Cypress.Commands.add('phCaptures', (options = {}) => {
- function resolve() {
- const result = options.full ? $fullCaptures : $captures
- return cy.verifyUpcomingAssertions(result, options, {
- onRetry: resolve,
- })
- }
-
- return resolve()
-})
-
-Cypress.Commands.add('resetPhCaptures', () => {
- $captures = []
- $fullCaptures = []
-})
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
new file mode 100644
index 000000000..2c82d7f45
--- /dev/null
+++ b/cypress/support/commands.ts
@@ -0,0 +1,35 @@
+let $captures, $fullCaptures
+
+Cypress.Commands.add('posthog', () => cy.window().then(($window) => ($window as any).posthog))
+
+Cypress.Commands.add('posthogInit', (options) => {
+ $captures = []
+ $fullCaptures = []
+
+ cy.posthog().invoke('init', 'test_token', {
+ api_host: location.origin,
+ debug: true,
+ _onCapture: (event, eventData) => {
+ $captures.push(event)
+ $fullCaptures.push(eventData)
+ },
+ ...options,
+ })
+})
+
+Cypress.Commands.add('phCaptures', (options = { full: false }) => {
+ function resolve() {
+ const result = options.full ? $fullCaptures : $captures
+ // @ts-expect-error TS can't find verifyUpcomingAssertions, but it's there 🤷
+ return cy.verifyUpcomingAssertions(result, options, {
+ onRetry: resolve,
+ })
+ }
+
+ return resolve()
+})
+
+Cypress.Commands.add('resetPhCaptures', () => {
+ $captures = []
+ $fullCaptures = []
+})
diff --git a/cypress/support/compression.js b/cypress/support/compression.ts
similarity index 61%
rename from cypress/support/compression.js
rename to cypress/support/compression.ts
index f603cbf9c..93d3fe1f2 100644
--- a/cypress/support/compression.js
+++ b/cypress/support/compression.ts
@@ -1,12 +1,12 @@
-import * as fflate from 'fflate'
+import { decompressSync, strFromU8 } from 'fflate'
export function getBase64EncodedPayload(request) {
const data = decodeURIComponent(request.body.match(/data=(.*)/)[1])
- return JSON.parse(Buffer.from(data, 'base64'))
+ return JSON.parse(Buffer.from(data, 'base64').toString())
}
export async function getGzipEncodedPayload(request) {
const data = new Uint8Array(await request.body)
- const decoded = fflate.strFromU8(fflate.decompressSync(data))
+ const decoded = strFromU8(decompressSync(data))
return JSON.parse(decoded)
}
diff --git a/cypress/support/e2e.js b/cypress/support/e2e.ts
similarity index 57%
rename from cypress/support/e2e.js
rename to cypress/support/e2e.ts
index dc197e752..82ddd8156 100644
--- a/cypress/support/e2e.js
+++ b/cypress/support/e2e.ts
@@ -1,27 +1,6 @@
-// ***********************************************************
-// This example support/index.js is processed and
-// loaded automatically before your test files.
-//
-// This is a great place to put global configuration and
-// behavior that modifies Cypress.
-//
-// You can change the location of this file or turn off
-// automatically serving support files with the
-// 'supportFile' configuration option.
-//
-// You can read more here:
-// https://on.cypress.io/configuration
-// ***********************************************************
-
-// Import commands.js using ES2015 syntax:
import './commands'
-import 'given2/setup'
-
-// Alternatively you can use CommonJS syntax:
-// require('./commands')
// Add console errors into cypress logs.
-// eslint-disable-next-line no-undef
Cypress.on('window:before:load', (win) => {
cy.spy(win.console, 'error')
cy.spy(win.console, 'warn')
diff --git a/cypress/support/index.ts b/cypress/support/index.ts
new file mode 100644
index 000000000..22e18cfb6
--- /dev/null
+++ b/cypress/support/index.ts
@@ -0,0 +1,32 @@
+///
+
+import { PostHog } from '../../src/posthog-core'
+import { PostHogConfig } from '../../src/types'
+
+declare global {
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace Cypress {
+ interface Chainable {
+ /**
+ * Custom command to get PostHog from the window
+ */
+ posthog(): Chainable
+
+ /**
+ * Custom command to initialize PostHog
+ */
+ posthogInit(options: Partial): void
+
+ /**
+ * custom command to get the events captured by posthog
+ * @param options pass full to get the whole event, omit or false to get just the name
+ */
+ phCaptures(options?: { full: boolean }): Chainable
+
+ /**
+ * custom command to reset the store of events captured by posthog
+ */
+ resetPhCaptures(): void
+ }
+ }
+}
diff --git a/cypress/support/setup.ts b/cypress/support/setup.ts
new file mode 100644
index 000000000..e5534eeb6
--- /dev/null
+++ b/cypress/support/setup.ts
@@ -0,0 +1,47 @@
+import { DecideResponse, PostHogConfig } from '../../src/types'
+
+export const start = ({
+ waitForDecide = true,
+ initPosthog = true,
+ resetOnInit = false,
+ options = {},
+ decideResponseOverrides = {
+ config: { enable_collect_everything: true },
+ sessionRecording: undefined,
+ isAuthenticated: false,
+ capturePerformance: true,
+ },
+ url = './playground/cypress-full',
+}: {
+ waitForDecide?: boolean
+ initPosthog?: boolean
+ resetOnInit?: boolean
+ options?: Partial
+ decideResponseOverrides?: Partial
+ url?: string
+}) => {
+ const decideResponse = {
+ editorParams: {},
+ featureFlags: ['session-recording-player'],
+ supportedCompression: ['gzip-js'],
+ excludedDomains: [],
+ autocaptureExceptions: false,
+ ...decideResponseOverrides,
+ config: { enable_collect_everything: true, ...decideResponseOverrides.config },
+ }
+ cy.intercept('POST', '**/decide/*', decideResponse).as('decide')
+
+ cy.visit(url)
+
+ if (initPosthog) {
+ cy.posthogInit(options)
+ }
+
+ if (resetOnInit) {
+ cy.posthog().invoke('reset', true)
+ }
+
+ if (waitForDecide) {
+ cy.wait('@decide')
+ }
+}
diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json
new file mode 100644
index 000000000..02963d41b
--- /dev/null
+++ b/cypress/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "target": "es2015",
+ "lib": ["es5", "dom"],
+ "types": ["cypress", "node"],
+ "moduleResolution": "node"
+ },
+ "include": ["**/*.ts"]
+}
diff --git a/package.json b/package.json
index 94e07a3fd..4a31df6f8 100644
--- a/package.json
+++ b/package.json
@@ -57,7 +57,7 @@
"@typescript-eslint/parser": "^6.19.0",
"babel-eslint": "10.1.0",
"babel-jest": "^26.6.3",
- "cypress": "13.5.1",
+ "cypress": "13.6.3",
"eslint": "8.56.0",
"eslint-config-posthog-js": "link:./eslint-rules",
"eslint-config-prettier": "^8.5.0",
diff --git a/src/extensions/replay/sessionrecording.ts b/src/extensions/replay/sessionrecording.ts
index 3b0f8a75d..c2a52606a 100644
--- a/src/extensions/replay/sessionrecording.ts
+++ b/src/extensions/replay/sessionrecording.ts
@@ -25,6 +25,7 @@ import { logger } from '../../utils/logger'
import { assignableWindow, window } from '../../utils/globals'
import { buildNetworkRequestOptions } from './config'
import { isLocalhost } from '../../utils/request-utils'
+import { userOptedOut } from '../../gdpr-utils'
const BASE_ENDPOINT = '/s/'
@@ -348,7 +349,8 @@ export class SessionRecording {
}
// We do not switch recorder versions midway through a recording.
- if (this._captureStarted || this.instance.config.disable_session_recording) {
+ // do not start if explicitly disabled or if the user has opted out
+ if (this._captureStarted || this.instance.config.disable_session_recording || userOptedOut(this.instance)) {
return
}
diff --git a/src/posthog-core.ts b/src/posthog-core.ts
index 2a99c13c8..8388c799e 100644
--- a/src/posthog-core.ts
+++ b/src/posthog-core.ts
@@ -1754,7 +1754,13 @@ export class PostHog {
}
if (this.sessionRecording && !_isUndefined(config.disable_session_recording)) {
- if (oldConfig.disable_session_recording !== config.disable_session_recording) {
+ const disable_session_recording_has_changed =
+ oldConfig.disable_session_recording !== config.disable_session_recording
+ // if opting back in, this config might not have changed
+ const try_enable_after_opt_in =
+ !userOptedOut(this) && !config.disable_session_recording && !this.sessionRecording.started
+
+ if (disable_session_recording_has_changed || try_enable_after_opt_in) {
if (config.disable_session_recording) {
this.sessionRecording.stopRecording()
} else {
diff --git a/yarn.lock b/yarn.lock
index ec5d5e723..69bad5b3a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3749,13 +3749,6 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.37.tgz#abb38afa9d6e8a2f627a8cb52290b3c80fbe61ed"
integrity sha512-i1KGxqcvJaLQali+WuypQnXwcplhtNtjs66eNsZpp2P2FL/trJJxx/VWsM0YCL2iMoIJrbXje48lvIQAQ4p2ZA==
-"@types/node@^18.17.5":
- version "18.18.9"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.9.tgz#5527ea1832db3bba8eb8023ce8497b7d3f299592"
- integrity sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==
- dependencies:
- undici-types "~5.26.4"
-
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@@ -5317,14 +5310,13 @@ cuint@^0.2.2:
resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b"
integrity sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs=
-cypress@13.5.1:
- version "13.5.1"
- resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.5.1.tgz#8b19bf0b9f31ea43f78980b2479bd3f25197d5cc"
- integrity sha512-yqLViT0D/lPI8Kkm7ciF/x/DCK/H/DnogdGyiTnQgX4OVR2aM30PtK+kvklTOD1u3TuItiD9wUQAF8EYWtyZug==
+cypress@13.6.3:
+ version "13.6.3"
+ resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.6.3.tgz#54f03ca07ee56b2bc18211e7bd32abd2533982ba"
+ integrity sha512-d/pZvgwjAyZsoyJ3FOsJT5lDsqnxQ/clMqnNc++rkHjbkkiF2h9s0JsZSyyH4QXhVFW3zPFg82jD25roFLOdZA==
dependencies:
"@cypress/request" "^3.0.0"
"@cypress/xvfb" "^1.2.4"
- "@types/node" "^18.17.5"
"@types/sinonjs__fake-timers" "8.1.1"
"@types/sizzle" "^2.3.2"
arch "^2.2.0"
@@ -11874,11 +11866,6 @@ unbox-primitive@^1.0.2:
has-symbols "^1.0.3"
which-boxed-primitive "^1.0.2"
-undici-types@~5.26.4:
- version "5.26.5"
- resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
- integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
-
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"