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]))
+ })
+})