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: allow triggering sessions when events occur #1523

Merged
merged 12 commits into from
Nov 15, 2024
2 changes: 1 addition & 1 deletion cypress/e2e/session-recording.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,7 @@ describe('Session recording', () => {
cy.posthog().invoke('capture', 'test_registered_property')
cy.phCaptures({ full: true }).then((captures) => {
expect((captures || []).map((c) => c.event)).to.deep.equal(['$pageview', 'test_registered_property'])
expect(captures[1]['properties']['$session_recording_start_reason']).to.equal('sampling_override')
expect(captures[1]['properties']['$session_recording_start_reason']).to.equal('sampling_overridden')
})

cy.resetPhCaptures()
Expand Down
72 changes: 72 additions & 0 deletions src/__tests__/extensions/replay/sessionrecording.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {
import Mock = jest.Mock
import { ConsentManager } from '../../../consent'
import { waitFor } from '@testing-library/preact'
import { SimpleEventEmitter } from '../../../utils/simple-event-emitter'

// Type and source defined here designate a non-user-generated recording event

Expand Down Expand Up @@ -185,6 +186,7 @@ describe('SessionRecording', () => {
let onFeatureFlagsCallback: ((flags: string[], variants: Record<string, string | boolean>) => void) | null
let removeCaptureHookMock: Mock
let addCaptureHookMock: Mock
let simpleEventEmitter: SimpleEventEmitter

const addRRwebToWindow = () => {
assignableWindow.__PosthogExtensions__.rrweb = {
Expand Down Expand Up @@ -239,6 +241,8 @@ describe('SessionRecording', () => {
removeCaptureHookMock = jest.fn()
addCaptureHookMock = jest.fn().mockImplementation(() => removeCaptureHookMock)

simpleEventEmitter = new SimpleEventEmitter()
// TODO we really need to make this a real posthog instance :cry:
posthog = {
get_property: (property_key: string): Property | undefined => {
return postHogPersistence?.['props'][property_key]
Expand All @@ -261,6 +265,10 @@ describe('SessionRecording', () => {
},
} as unknown as ConsentManager,
register_for_session() {},
_internalEventEmitter: simpleEventEmitter,
on: (event, cb) => {
return simpleEventEmitter.on(event, cb)
},
} as Partial<PostHog> as PostHog

loadScriptMock.mockImplementation((_ph, _path, callback) => {
Expand Down Expand Up @@ -1883,6 +1891,7 @@ describe('SessionRecording', () => {
loadScriptMock.mockImplementation((_ph, _path, callback) => {
callback()
})
sessionRecording = new SessionRecording(posthog)

sessionRecording.afterDecideResponse(makeDecideResponse({ sessionRecording: { endpoint: '/s/' } }))
sessionRecording.startIfEnabledOrStop()
Expand Down Expand Up @@ -2250,4 +2259,67 @@ describe('SessionRecording', () => {
])
})
})

describe('Event triggering', () => {
beforeEach(() => {
sessionRecording.startIfEnabledOrStop()
})

it('flushes buffer and starts when sees event', async () => {
sessionRecording.afterDecideResponse(
makeDecideResponse({
sessionRecording: {
endpoint: '/s/',
eventTriggers: ['$exception'],
},
})
)

expect(sessionRecording['status']).toBe('buffering')

// Emit some events before hitting blocked URL
_emit(createIncrementalSnapshot({ data: { source: 1 } }))
_emit(createIncrementalSnapshot({ data: { source: 2 } }))

expect(sessionRecording['buffer'].data).toHaveLength(2)

simpleEventEmitter.emit('eventCaptured', { event: 'not-$exception' })

expect(sessionRecording['status']).toBe('buffering')

simpleEventEmitter.emit('eventCaptured', { event: '$exception' })

expect(sessionRecording['status']).toBe('active')
expect(sessionRecording['buffer'].data).toHaveLength(0)
})

it('starts if sees an event but still waiting for a URL', async () => {
sessionRecording.afterDecideResponse(
makeDecideResponse({
sessionRecording: {
endpoint: '/s/',
eventTriggers: ['$exception'],
urlTriggers: [{ url: 'start-on-me', matching: 'regex' }],
},
})
)

expect(sessionRecording['status']).toBe('buffering')

// Emit some events before hitting blocked URL
_emit(createIncrementalSnapshot({ data: { source: 1 } }))
_emit(createIncrementalSnapshot({ data: { source: 2 } }))

expect(sessionRecording['buffer'].data).toHaveLength(2)

simpleEventEmitter.emit('eventCaptured', { event: 'not-$exception' })

expect(sessionRecording['status']).toBe('buffering')

simpleEventEmitter.emit('eventCaptured', { event: '$exception' })

// even though still waiting for URL to trigger
expect(sessionRecording['status']).toBe('active')
})
})
})
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export const SESSION_ID = '$sesid'
export const SESSION_RECORDING_IS_SAMPLED = '$session_is_sampled'
export const SESSION_RECORDING_URL_TRIGGER_ACTIVATED_SESSION = '$session_recording_url_trigger_activated_session'
export const SESSION_RECORDING_URL_TRIGGER_STATUS = '$session_recording_url_trigger_status'
export const SESSION_RECORDING_EVENT_TRIGGER_ACTIVATED_SESSION = '$session_recording_event_trigger_activated_session'
export const SESSION_RECORDING_EVENT_TRIGGER_STATUS = '$session_recording_event_trigger_status'
export const ENABLED_FEATURE_FLAGS = '$enabled_feature_flags'
export const PERSISTENCE_EARLY_ACCESS_FEATURES = '$early_access_features'
export const STORED_PERSON_PROPERTIES_KEY = '$stored_person_properties'
Expand Down
Loading
Loading