diff --git a/cypress/e2e/capture.cy.ts b/cypress/e2e/capture.cy.ts deleted file mode 100644 index cb63c1ee4..000000000 --- a/cypress/e2e/capture.cy.ts +++ /dev/null @@ -1,198 +0,0 @@ -/// -// @ts-expect-error - you totally can import the package JSON -import { version } from '../../package.json' - -import { getBase64EncodedPayload, getGzipEncodedPayload, getPayload } from '../support/compression' -import { start } from '../support/setup' - -const urlWithVersion = new RegExp(`&ver=${version}`) - -describe('Event capture', () => { - describe('group analytics', () => { - it('includes group information in all event payloads', () => { - start({ - options: { - loaded: (posthog) => { - posthog.group('company', 'id:5') - }, - }, - }) - - cy.get('[data-cy-custom-event-button]').click() - - cy.phCaptures({ full: true }) - .should('have.length', 3) - .should('satisfy', (payloads) => payloads.every(({ properties }) => !!properties.$groups)) - }) - }) - - describe('when disabled', () => { - it('captures pageviews, custom events when autocapture disabled', () => { - start({ options: { autocapture: false }, waitForDecide: false }) - - cy.wait(50) - cy.get('[data-cy-custom-event-button]').click() - cy.phCaptures().should('have.length', 2) - cy.phCaptures().should('include', '$pageview') - cy.phCaptures().should('include', 'custom-event') - - cy.wait('@capture') - // @ts-expect-error - TS is wrong that get returns HTMLElement here - cy.get('@capture').should(async ({ request }) => { - const captures = await getPayload(request) - expect(captures['event']).to.equal('$pageview') - }) - }) - - it('captures autocapture, custom events when pageviews disabled', () => { - start({ options: { capture_pageview: false } }) - - cy.get('[data-cy-custom-event-button]').click() - cy.reload() - - cy.phCaptures().should('have.length', 2) - cy.phCaptures().should('include', '$autocapture') - cy.phCaptures().should('include', 'custom-event') - }) - - it('does not capture things when multiple disabled', () => { - start({ options: { capture_pageview: false, capture_pageleave: false, autocapture: false } }) - - cy.get('[data-cy-custom-event-button]').click() - cy.reload() - - cy.phCaptures().should('have.length', 1) - cy.phCaptures().should('include', 'custom-event') - }) - }) - - describe('decoding the payload', () => { - describe('gzip-js supported', () => { - it('contains the correct payload after an event', async () => { - start({}) - - // Pageview will be sent immediately - - cy.wait('@capture').should(async ({ request }) => { - expect(request.url).to.match(urlWithVersion) - - const data = await getPayload(request) - expect(data['event']).to.equal('$pageview') - }) - - // the code below is going to trigger an event capture - // we want to assert on the request - cy.intercept('POST', '/e/*', async (request) => { - expect(request.headers['content-type']).to.eq('text/plain') - const captures = await getGzipEncodedPayload(request) - expect(captures.map(({ event }) => event)).to.deep.equal(['$autocapture', 'custom-event']) - }).as('capture-assertion') - - cy.get('[data-cy-custom-event-button]').click() - cy.phCaptures().should('have.length', 3) - cy.phCaptures().should('include', '$pageview') - cy.phCaptures().should('include', '$autocapture') - cy.phCaptures().should('include', 'custom-event') - - cy.wait('@capture-assertion') - }) - }) - }) - - describe('advanced_disable_decide config', () => { - it('does not autocapture anything when /decide is disabled', () => { - start({ options: { autocapture: false, advanced_disable_decide: true }, waitForDecide: false }) - - cy.get('body').click(100, 100) - cy.get('body').click(98, 102) - cy.get('body').click(101, 103) - cy.get('[data-cy-custom-event-button]').click() - - // No autocapture events, still captures custom events - cy.phCaptures().should('have.length', 2) - cy.phCaptures().should('include', '$pageview') - cy.phCaptures().should('include', 'custom-event') - }) - - it('does not capture session recordings', () => { - start({ options: { advanced_disable_decide: true }, waitForDecide: false }) - - cy.get('[data-cy-custom-event-button]').click() - cy.wait('@capture') - - cy.get('[data-cy-input]') - .type('hello posthog!') - .then(() => { - cy.get('@session-recording.all').then((calls) => { - expect(calls.length).to.equal(0) - }) - }) - - cy.phCaptures().should('not.include', '$snapshot') - }) - }) - - describe('subsequent decide calls', () => { - it('makes a single decide request on 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({ - token: 'test_token', - distinct_id: 'new-id', - person_properties: {}, - $anon_distinct_id: payload.$anon_distinct_id, - groups: { - company: 'id:5', - playlist: 'id:77', - }, - group_properties: { - company: { id: 5, company_name: 'Awesome Inc' }, - playlist: { length: 8 }, - }, - }) - }) - }) - - it('does a single decide call on following changes', () => { - 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) => { - expect(calls.length).to.equal(1) - }) - - cy.posthog().invoke('group', 'company', 'id:6') - cy.posthog().invoke('group', 'playlist', 'id:77') - cy.posthog().invoke('group', 'anothergroup', 'id:99') - - cy.wait('@decide') - - cy.get('@decide.all').then((calls) => { - expect(calls.length).to.equal(2) - }) - }) - }) -}) diff --git a/playwright/autocapture-config.spec.ts b/playwright/autocapture-config.spec.ts new file mode 100644 index 000000000..499f8373f --- /dev/null +++ b/playwright/autocapture-config.spec.ts @@ -0,0 +1,145 @@ +import { test } from './utils/posthog-playwright-test-base' +import { start } from './utils/setup' + +const startOptions = { + options: {}, + decideResponseOverrides: { + sessionRecording: { + endpoint: '/ses/', + }, + capturePerformance: true, + }, + url: './playground/cypress/index.html', +} + +test.describe('autocapture config', () => { + test('do not capture click if not in allowlist', async ({ page, context }) => { + await start( + { + ...startOptions, + options: { + ...startOptions.options, + capture_pageview: false, + autocapture: { + dom_event_allowlist: ['change'], + }, + }, + }, + page, + context + ) + + await page.locator('[data-cy-custom-event-button]').click() + // no autocapture event from click + await page.expectCapturedEventsToBe(['custom-event']) + + await page.locator('[data-cy-input]').fill('hello posthog!') + // blur the input + await page.locator('body').click() + await page.expectCapturedEventsToBe(['custom-event', '$autocapture']) + }) + + test('capture clicks when configured to', async ({ page, context }) => { + await start( + { + ...startOptions, + options: { ...startOptions.options, autocapture: { dom_event_allowlist: ['click'] } }, + }, + page, + context + ) + + await page.locator('[data-cy-custom-event-button]').click() + await page.expectCapturedEventsToBe(['$pageview', '$autocapture', 'custom-event']) + + await page.locator('[data-cy-input]').fill('hello posthog!') + // blur the input + await page.locator('body').click() + // no change autocapture event + await page.expectCapturedEventsToBe(['$pageview', '$autocapture', 'custom-event']) + }) + + test('obeys url allowlist', async ({ page, context }) => { + await start( + { + ...startOptions, + options: { ...startOptions.options, autocapture: { url_allowlist: ['.*test-is-not-on-this.*'] } }, + }, + page, + context + ) + + await page.locator('[data-cy-custom-event-button]').click() + await page.expectCapturedEventsToBe(['$pageview', 'custom-event']) + + await page.resetCapturedEvents() + await start( + { + ...startOptions, + options: { ...startOptions.options, autocapture: { url_allowlist: ['.*cypress.*'] } }, + }, + page, + context + ) + + await page.locator('[data-cy-custom-event-button]').click() + await page.expectCapturedEventsToBe(['$pageview', '$autocapture', 'custom-event']) + }) + + test('obeys element allowlist', async ({ page, context }) => { + await start( + { + ...startOptions, + options: { ...startOptions.options, autocapture: { element_allowlist: ['button'] } }, + }, + page, + context + ) + + await page.locator('[data-cy-custom-event-button]').click() + await page.expectCapturedEventsToBe(['$pageview', '$autocapture', 'custom-event']) + + await page.resetCapturedEvents() + await start( + { + ...startOptions, + options: { ...startOptions.options, autocapture: { element_allowlist: ['input'] } }, + }, + page, + context + ) + + await page.locator('[data-cy-custom-event-button]').click() + await page.expectCapturedEventsToBe(['$pageview', 'custom-event']) + }) + + test('obeys css selector allowlist', async ({ page, context }) => { + await start( + { + ...startOptions, + options: { + ...startOptions.options, + autocapture: { css_selector_allowlist: ['[data-cy-custom-event-button]'] }, + }, + }, + page, + context + ) + + await page.locator('[data-cy-custom-event-button]').click() + await page.expectCapturedEventsToBe(['$pageview', '$autocapture', 'custom-event']) + + await page.resetCapturedEvents() + await start( + { + ...startOptions, + options: { ...startOptions.options, autocapture: { css_selector_allowlist: ['[data-cy-input]'] } }, + }, + page, + context + ) + + await page.locator('[data-cy-custom-event-button]').click() + await page.expectCapturedEventsToBe(['$pageview', 'custom-event']) + }) +}) diff --git a/playwright/capture.spec.ts b/playwright/capture.spec.ts index 1667ecd98..0a1568169 100644 --- a/playwright/capture.spec.ts +++ b/playwright/capture.spec.ts @@ -2,8 +2,17 @@ import { expect, test } from './utils/posthog-playwright-test-base' import { start } from './utils/setup' import { pollUntilEventCaptured } from './utils/event-capture-utils' import { Request } from '@playwright/test' -import { getBase64EncodedPayloadFromBody } from '../cypress/support/compression' -import { PostHog } from '../src/posthog-core' +import { decompressSync, strFromU8 } from 'fflate' + +function getGzipEncodedPayloady(req: Request): Record { + const data = req.postDataBuffer() + if (!data) { + throw new Error('Expected body to be present') + } + const decoded = strFromU8(decompressSync(data)) + + return JSON.parse(decoded) +} const startOptions = { options: {}, @@ -28,6 +37,28 @@ test.describe('event capture', () => { await page.expectCapturedEventsToBe(['$pageview', '$autocapture', 'custom-event', '$pageleave', '$pageview']) }) + test('contains the correct payload after an event', async ({ page, context }) => { + const captureRequests: Request[] = [] + + page.on('request', (request) => { + if (request.url().includes('/e/')) { + captureRequests.push(request) + } + }) + + await start({}, page, context) + + // Pageview will be sent immediately + await pollUntilEventCaptured(page, '$pageview') + expect(captureRequests.length).toEqual(1) + const captureRequest = captureRequests[0] + expect(captureRequest.headers()['content-type']).toEqual('text/plain') + expect(captureRequest.url()).toMatch(/gzip/) + const payload = getGzipEncodedPayloady(captureRequest) + expect(payload.event).toEqual('$pageview') + expect(Object.keys(payload.properties).length).toBeGreaterThan(0) + }) + test('captures $feature_flag_called event', async ({ page, context }) => { await start(startOptions, page, context) await page.click('[data-cy-feature-flag-button]') @@ -169,235 +200,4 @@ test.describe('event capture', () => { await pollUntilEventCaptured(page, 'custom-event') await page.expectCapturedEventsToBe(['custom-event']) }) - - test('makes decide request on start', async ({ page, context }) => { - // we want to grab any requests to decide so we can inspect their payloads - const decideRequests: Request[] = [] - - page.on('request', (request) => { - if (request.url().includes('/decide/')) { - decideRequests.push(request) - } - }) - - await start( - { - ...startOptions, - options: { - ...startOptions.options, - }, - runBeforePostHogInit: async (page) => { - // it's tricky to pass functions as args the way posthog config is passed in playwright - // so here we set the function on the window object - // and then call it in the loaded function during init - await page.evaluate(() => { - ;(window as any).__ph_loaded = (ph: PostHog) => { - ph.identify('new-id') - ph.group('company', 'id:5', { id: 5, company_name: 'Awesome Inc' }) - ph.group('playlist', 'id:77', { length: 8 }) - } - }) - }, - }, - page, - context - ) - - expect(decideRequests.length).toBe(1) - const decideRequest = decideRequests[0] - const decidePayload = getBase64EncodedPayloadFromBody(decideRequest.postData()) - expect(decidePayload).toEqual({ - token: 'test token', - distinct_id: 'new-id', - person_properties: {}, - $anon_distinct_id: decidePayload.$anon_distinct_id, - groups: { - company: 'id:5', - playlist: 'id:77', - }, - group_properties: { - company: { id: 5, company_name: 'Awesome Inc' }, - playlist: { length: 8 }, - }, - }) - }) - - test('does a single decide call on following changes', async ({ page, context }) => { - // we want to grab any requests to decide so we can inspect their payloads - const decideRequests: Request[] = [] - - page.on('request', (request) => { - if (request.url().includes('/decide/')) { - decideRequests.push(request) - } - }) - - await start( - { - ...startOptions, - options: { - ...startOptions.options, - }, - runBeforePostHogInit: async (page) => { - // it's tricky to pass functions as args the way posthog config is passed in playwright - // so here we set the function on the window object - // and then call it in the loaded function during init - await page.evaluate(() => { - ;(window as any).__ph_loaded = (ph: PostHog) => { - ph.identify('new-id') - ph.group('company', 'id:5', { id: 5, company_name: 'Awesome Inc' }) - ph.group('playlist', 'id:77', { length: 8 }) - } - }) - }, - }, - page, - context - ) - - expect(decideRequests.length).toBe(1) - - await page.waitingForNetworkCausedBy(['**/decide/**'], async () => { - await page.evaluate(() => { - const ph = (window as any).posthog - ph.group('company', 'id:6') - ph.group('playlist', 'id:77') - ph.group('anothergroup', 'id:99') - }) - }) - - expect(decideRequests.length).toBe(2) - }) - - test.describe('autocapture config', () => { - test('do not capture click if not in allowlist', async ({ page, context }) => { - await start( - { - ...startOptions, - options: { - ...startOptions.options, - capture_pageview: false, - autocapture: { - dom_event_allowlist: ['change'], - }, - }, - }, - page, - context - ) - - await page.locator('[data-cy-custom-event-button]').click() - // no autocapture event from click - await page.expectCapturedEventsToBe(['custom-event']) - - await page.locator('[data-cy-input]').fill('hello posthog!') - // blur the input - await page.locator('body').click() - await page.expectCapturedEventsToBe(['custom-event', '$autocapture']) - }) - - test('capture clicks when configured to', async ({ page, context }) => { - await start( - { - ...startOptions, - options: { ...startOptions.options, autocapture: { dom_event_allowlist: ['click'] } }, - }, - page, - context - ) - - await page.locator('[data-cy-custom-event-button]').click() - await page.expectCapturedEventsToBe(['$pageview', '$autocapture', 'custom-event']) - - await page.locator('[data-cy-input]').fill('hello posthog!') - // blur the input - await page.locator('body').click() - // no change autocapture event - await page.expectCapturedEventsToBe(['$pageview', '$autocapture', 'custom-event']) - }) - - test('obeys url allowlist', async ({ page, context }) => { - await start( - { - ...startOptions, - options: { ...startOptions.options, autocapture: { url_allowlist: ['.*test-is-not-on-this.*'] } }, - }, - page, - context - ) - - await page.locator('[data-cy-custom-event-button]').click() - await page.expectCapturedEventsToBe(['$pageview', 'custom-event']) - - await page.resetCapturedEvents() - await start( - { - ...startOptions, - options: { ...startOptions.options, autocapture: { url_allowlist: ['.*cypress.*'] } }, - }, - page, - context - ) - - await page.locator('[data-cy-custom-event-button]').click() - await page.expectCapturedEventsToBe(['$pageview', '$autocapture', 'custom-event']) - }) - - test('obeys element allowlist', async ({ page, context }) => { - await start( - { - ...startOptions, - options: { ...startOptions.options, autocapture: { element_allowlist: ['button'] } }, - }, - page, - context - ) - - await page.locator('[data-cy-custom-event-button]').click() - await page.expectCapturedEventsToBe(['$pageview', '$autocapture', 'custom-event']) - - await page.resetCapturedEvents() - await start( - { - ...startOptions, - options: { ...startOptions.options, autocapture: { element_allowlist: ['input'] } }, - }, - page, - context - ) - - await page.locator('[data-cy-custom-event-button]').click() - await page.expectCapturedEventsToBe(['$pageview', 'custom-event']) - }) - - test('obeys css selector allowlist', async ({ page, context }) => { - await start( - { - ...startOptions, - options: { - ...startOptions.options, - autocapture: { css_selector_allowlist: ['[data-cy-custom-event-button]'] }, - }, - }, - page, - context - ) - - await page.locator('[data-cy-custom-event-button]').click() - await page.expectCapturedEventsToBe(['$pageview', '$autocapture', 'custom-event']) - - await page.resetCapturedEvents() - await start( - { - ...startOptions, - options: { ...startOptions.options, autocapture: { css_selector_allowlist: ['[data-cy-input]'] } }, - }, - page, - context - ) - - await page.locator('[data-cy-custom-event-button]').click() - await page.expectCapturedEventsToBe(['$pageview', 'custom-event']) - }) - }) }) diff --git a/playwright/decide.spec.ts b/playwright/decide.spec.ts new file mode 100644 index 000000000..bc79d80b6 --- /dev/null +++ b/playwright/decide.spec.ts @@ -0,0 +1,136 @@ +import { expect, test } from './utils/posthog-playwright-test-base' +import { Request } from '@playwright/test' +import { start } from './utils/setup' +import { PostHog } from '../src/posthog-core' +import { getBase64EncodedPayloadFromBody } from '../cypress/support/compression' + +const startOptions = { + options: {}, + decideResponseOverrides: { + sessionRecording: { + endpoint: '/ses/', + }, + capturePerformance: true, + }, + url: './playground/cypress/index.html', +} + +test.describe('decide', () => { + test('makes decide request on start', async ({ page, context }) => { + // we want to grab any requests to decide so we can inspect their payloads + const decideRequests: Request[] = [] + + page.on('request', (request) => { + if (request.url().includes('/decide/')) { + decideRequests.push(request) + } + }) + + await start( + { + ...startOptions, + options: { + ...startOptions.options, + }, + runBeforePostHogInit: async (page) => { + // it's tricky to pass functions as args the way posthog config is passed in playwright + // so here we set the function on the window object + // and then call it in the loaded function during init + await page.evaluate(() => { + ;(window as any).__ph_loaded = (ph: PostHog) => { + ph.identify('new-id') + ph.group('company', 'id:5', { id: 5, company_name: 'Awesome Inc' }) + ph.group('playlist', 'id:77', { length: 8 }) + } + }) + }, + }, + page, + context + ) + + expect(decideRequests.length).toBe(1) + const decideRequest = decideRequests[0] + const decidePayload = getBase64EncodedPayloadFromBody(decideRequest.postData()) + expect(decidePayload).toEqual({ + token: 'test token', + distinct_id: 'new-id', + person_properties: {}, + $anon_distinct_id: decidePayload.$anon_distinct_id, + groups: { + company: 'id:5', + playlist: 'id:77', + }, + group_properties: { + company: { id: 5, company_name: 'Awesome Inc' }, + playlist: { length: 8 }, + }, + }) + }) + + test('does a single decide call on following changes', async ({ page, context }) => { + // we want to grab any requests to decide so we can inspect their payloads + const decideRequests: Request[] = [] + + page.on('request', (request) => { + if (request.url().includes('/decide/')) { + decideRequests.push(request) + } + }) + + await start( + { + ...startOptions, + options: { + ...startOptions.options, + }, + runBeforePostHogInit: async (page) => { + // it's tricky to pass functions as args the way posthog config is passed in playwright + // so here we set the function on the window object + // and then call it in the loaded function during init + await page.evaluate(() => { + ;(window as any).__ph_loaded = (ph: PostHog) => { + ph.identify('new-id') + ph.group('company', 'id:5', { id: 5, company_name: 'Awesome Inc' }) + ph.group('playlist', 'id:77', { length: 8 }) + } + }) + }, + }, + page, + context + ) + + expect(decideRequests.length).toBe(1) + + await page.waitingForNetworkCausedBy(['**/decide/**'], async () => { + await page.evaluate(() => { + const ph = (window as any).posthog + ph.group('company', 'id:6') + ph.group('playlist', 'id:77') + ph.group('anothergroup', 'id:99') + }) + }) + + expect(decideRequests.length).toBe(2) + }) + + test('does not capture session recordings when decide is disabled', async ({ page, context }) => { + await start({ options: { advanced_disable_decide: true }, waitForDecide: false }, page, context) + + await page.locator('[data-cy-custom-event-button]').click() + + const callsToSessionRecording = page.waitForResponse('**/ses/') + + await page.locator('[data-cy-input]').type('hello posthog!') + + void callsToSessionRecording.then(() => { + throw new Error('Session recording call was made and should not have been') + }) + await page.waitForTimeout(200) + + const capturedEvents = await page.capturedEvents() + // no snapshot events sent + expect(capturedEvents.map((x) => x.event)).toEqual(['$pageview', 'custom-event']) + }) +}) diff --git a/playwright/group-analytics.spec.ts b/playwright/group-analytics.spec.ts new file mode 100644 index 000000000..43e6bfb69 --- /dev/null +++ b/playwright/group-analytics.spec.ts @@ -0,0 +1,31 @@ +import { expect, test } from './utils/posthog-playwright-test-base' +import { start } from './utils/setup' +import { PostHog } from '../src/posthog-core' + +test.describe('group analytics', () => { + test('includes group information in all event payloads', async ({ page, context }) => { + await start( + { + runBeforePostHogInit: async (page) => { + // it's tricky to pass functions as args the way posthog config is passed in playwright + // so here we set the function on the window object + // and then call it in the loaded function during init + await page.evaluate(() => { + ;(window as any).__ph_loaded = (ph: PostHog) => { + ph.group('company', 'id:5') + } + }) + }, + }, + page, + context + ) + + await page.locator('[data-cy-custom-event-button]').click() + + const capturedEvents = await page.capturedEvents() + expect(capturedEvents).toHaveLength(3) + const hasGroups = new Set(capturedEvents.map((x) => !!x.properties.$groups)) + expect(hasGroups).toEqual(new Set([true])) + }) +})