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: report reason for recording start #1452

Merged
merged 10 commits into from
Oct 15, 2024
122 changes: 84 additions & 38 deletions cypress/e2e/session-recording.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,25 @@ function ensureActivitySendsSnapshots(initial = true) {
.wait('@session-recording')
.then(() => {
cy.phCaptures({ full: true }).then((captures) => {
expect(captures.map((c) => c.event)).to.deep.equal(['$snapshot'])
expect(captures[0]['properties']['$snapshot_data']).to.have.length.above(14).and.below(40)
const capturedSnapshot = captures.find((e) => e.event === '$snapshot')
expect(capturedSnapshot).not.to.be.undefined

expect(capturedSnapshot['properties']['$snapshot_data']).to.have.length.above(14).and.below(40)
// a meta and then a full snapshot
expect(captures[0]['properties']['$snapshot_data'][0].type).to.equal(4) // meta
expect(captures[0]['properties']['$snapshot_data'][1].type).to.equal(2) // full_snapshot
expect(capturedSnapshot['properties']['$snapshot_data'][0].type).to.equal(4) // meta
expect(capturedSnapshot['properties']['$snapshot_data'][1].type).to.equal(2) // full_snapshot

if (initial) {
expectSessionOptionsCustomEvent(captures[0]['properties']['$snapshot_data'][2])
expectPostHogConfigCustomEvent(captures[0]['properties']['$snapshot_data'][3])
expectSessionOptionsCustomEvent(capturedSnapshot['properties']['$snapshot_data'][2])
expectPostHogConfigCustomEvent(capturedSnapshot['properties']['$snapshot_data'][3])
} else {
expectSessionOptionsCustomEvent(captures[0]['properties']['$snapshot_data'][2])
expectPostHogConfigCustomEvent(captures[0]['properties']['$snapshot_data'][3])
expectSessionIdChangedCustomEvent(captures[0]['properties']['$snapshot_data'][4])
expectSessionOptionsCustomEvent(capturedSnapshot['properties']['$snapshot_data'][2])
expectPostHogConfigCustomEvent(capturedSnapshot['properties']['$snapshot_data'][3])
expectSessionIdChangedCustomEvent(capturedSnapshot['properties']['$snapshot_data'][4])
}

// Making a set from the rest should all be 3 - incremental snapshots
const remainder = captures[0]['properties']['$snapshot_data'].slice(initial ? 4 : 5)
const remainder = capturedSnapshot['properties']['$snapshot_data'].slice(initial ? 4 : 5)
expect(Array.from(new Set(remainder.map((s) => s.type)))).to.deep.equal([3])
})
})
Expand Down Expand Up @@ -112,6 +114,9 @@ describe('Session recording', () => {
describe('array.full.js', () => {
it('captures session events', () => {
start({
options: {
session_recording: {},
},
decideResponseOverrides: {
isAuthenticated: false,
sessionRecording: {
Expand All @@ -128,9 +133,13 @@ describe('Session recording', () => {
.type('hello posthog!')
.wait('@session-recording')
.then(() => {
cy.posthog().invoke('capture', 'test_registered_property')
cy.phCaptures({ full: true }).then((captures) => {
// should be a pageview and a $snapshot
expect(captures.map((c) => c.event)).to.deep.equal(['$pageview', '$snapshot'])
expect(captures.map((c) => c.event)).to.deep.equal([
'$pageview',
'$snapshot',
'test_registered_property',
])

expect(captures[1]['properties']['$snapshot_data']).to.have.length.above(33).and.below(40)
// a meta and then a full snapshot
Expand All @@ -141,6 +150,10 @@ describe('Session recording', () => {
// Making a set from the rest should all be 3 - incremental snapshots
const incrementalSnapshots = captures[1]['properties']['$snapshot_data'].slice(4)
expect(Array.from(new Set(incrementalSnapshots.map((s) => s.type)))).to.deep.eq([3])

expect(captures[2]['properties']['$session_recording_start_reason']).to.equal(
'recording_initialized'
)
})
})
})
Expand Down Expand Up @@ -180,6 +193,8 @@ describe('Session recording', () => {
loaded: (ph) => {
ph.sessionRecording._forceAllowLocalhostNetworkCapture = true
},

session_recording: {},
},
})

Expand Down Expand Up @@ -271,6 +286,9 @@ describe('Session recording', () => {
describe('array.js', () => {
beforeEach(() => {
start({
options: {
session_recording: {},
},
decideResponseOverrides: {
isAuthenticated: false,
sessionRecording: {
Expand Down Expand Up @@ -386,10 +404,11 @@ describe('Session recording', () => {
it('continues capturing to the same session when the page reloads', () => {
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! ')
cy.wait('@session-recording').then(() => {
cy.phCaptures({ full: true }).then((captures) => {
expect(captures.map((c) => c.event)).to.deep.equal(['$pageview', '$snapshot'])

captures.forEach((c) => {
if (isNull(sessionId)) {
sessionId = c.properties['$session_id']
Expand All @@ -404,7 +423,9 @@ describe('Session recording', () => {
cy.resetPhCaptures()
// and refresh the page
cy.reload()
cy.posthogInit({})
cy.posthogInit({
session_recording: {},
})
cy.wait('@decide')
cy.wait('@recorder-script')

Expand All @@ -418,10 +439,13 @@ describe('Session recording', () => {
cy.phCaptures({ full: true }).then((captures) => {
// should be a $snapshot for the current session
expect(captures.map((c) => c.event)).to.deep.equal(['$pageview', '$snapshot'])

expect(captures[0].properties['$session_id']).to.equal(sessionId)
expect(captures[1].properties['$session_id']).to.equal(sessionId)

expect(captures[1]['properties']['$snapshot_data']).to.have.length.above(0)
const capturedSnapshot = captures[1]
expect(capturedSnapshot.properties['$session_id']).to.equal(sessionId)

expect(capturedSnapshot['properties']['$snapshot_data']).to.have.length.above(0)

/**
* the snapshots will look a little like:
Expand All @@ -433,28 +457,28 @@ describe('Session recording', () => {

// page reloaded so we will start with a full snapshot
// 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(capturedSnapshot['properties']['$snapshot_data'][0].type).to.equal(4) // meta
expect(capturedSnapshot['properties']['$snapshot_data'][1].type).to.equal(2) // full_snapshot

// these custom events should always be in the same order, but computers
// we don't care if they are present and in a changing order
const customEvents = sortByTag([
captures[1]['properties']['$snapshot_data'][2],
captures[1]['properties']['$snapshot_data'][3],
captures[1]['properties']['$snapshot_data'][4],
capturedSnapshot['properties']['$snapshot_data'][2],
capturedSnapshot['properties']['$snapshot_data'][3],
capturedSnapshot['properties']['$snapshot_data'][4],
])
expectPageViewCustomEvent(customEvents[0])
expectPostHogConfigCustomEvent(customEvents[1])
expectSessionOptionsCustomEvent(customEvents[2])

const xPositions = []
for (let i = 5; i < captures[1]['properties']['$snapshot_data'].length; i++) {
expect(captures[1]['properties']['$snapshot_data'][i].type).to.equal(3)
expect(captures[1]['properties']['$snapshot_data'][i].data.source).to.equal(
for (let i = 5; i < capturedSnapshot['properties']['$snapshot_data'].length; i++) {
expect(capturedSnapshot['properties']['$snapshot_data'][i].type).to.equal(3)
expect(capturedSnapshot['properties']['$snapshot_data'][i].data.source).to.equal(
6,
JSON.stringify(captures[1]['properties']['$snapshot_data'][i])
JSON.stringify(capturedSnapshot['properties']['$snapshot_data'][i])
)
xPositions.push(captures[1]['properties']['$snapshot_data'][i].data.positions[0].x)
xPositions.push(capturedSnapshot['properties']['$snapshot_data'][i].data.positions[0].x)
}

// even though we trigger 4 events, only 2 snapshots should be captured
Expand All @@ -478,11 +502,20 @@ describe('Session recording', () => {
.type('hello posthog!')
.wait('@session-recording')
.then(() => {
cy.posthog().invoke('capture', 'test_registered_property')
cy.phCaptures({ full: true }).then((captures) => {
// should be a pageview and a $snapshot
expect(captures.map((c) => c.event)).to.deep.equal(['$pageview', '$snapshot'])
expect(captures.map((c) => c.event)).to.deep.equal([
'$pageview',
'$snapshot',
'test_registered_property',
])

expect(captures[1]['properties']['$session_id']).to.be.a('string')
firstSessionId = captures[1]['properties']['$session_id']

expect(captures[2]['properties']['$session_recording_start_reason']).to.equal(
'recording_initialized'
)
})
})

Expand All @@ -505,24 +538,29 @@ describe('Session recording', () => {
.type('hello posthog!')
.wait('@session-recording', { timeout: 10000 })
.then(() => {
cy.posthog().invoke('capture', 'test_registered_property')
cy.phCaptures({ full: true }).then((captures) => {
// should be a pageview and a $snapshot
expect(captures[0].event).to.equal('$snapshot')
const capturedSnapshot = captures[0]
expect(capturedSnapshot.event).to.equal('$snapshot')

expect(captures[0]['properties']['$session_id']).to.be.a('string')
expect(captures[0]['properties']['$session_id']).not.to.eq(firstSessionId)
expect(capturedSnapshot['properties']['$session_id']).to.be.a('string')
expect(capturedSnapshot['properties']['$session_id']).not.to.eq(firstSessionId)

expect(captures[0]['properties']['$snapshot_data']).to.have.length.above(0)
expect(captures[0]['properties']['$snapshot_data'][0].type).to.equal(4) // meta
expect(captures[0]['properties']['$snapshot_data'][1].type).to.equal(2) // full_snapshot
expect(capturedSnapshot['properties']['$snapshot_data']).to.have.length.above(0)
expect(capturedSnapshot['properties']['$snapshot_data'][0].type).to.equal(4) // meta
expect(capturedSnapshot['properties']['$snapshot_data'][1].type).to.equal(2) // full_snapshot

expect(captures[1].event).to.equal('test_registered_property')
expect(captures[1]['properties']['$session_recording_start_reason']).to.equal(
'session_id_changed'
)
})
})
})

it('starts a new recording after calling reset', () => {
cy.phCaptures({ full: true }).then((captures) => {
// should be a pageview at the beginning
expect(captures.map((c) => c.event)).to.deep.equal(['$pageview'])
expect(captures[0].event).to.eq('$pageview')
})
cy.resetPhCaptures()

Expand Down Expand Up @@ -553,6 +591,9 @@ describe('Session recording', () => {
describe('with sampling', () => {
beforeEach(() => {
start({
options: {
session_recording: {},
},
decideResponseOverrides: {
isAuthenticated: false,
sessionRecording: {
Expand All @@ -575,7 +616,6 @@ describe('Session recording', () => {
.wait(200) // can't wait on call to session recording, it's not going to happen
.then(() => {
cy.phCaptures({ full: true }).then((captures) => {
// should be a pageview and a $snapshot
expect(captures.map((c) => c.event)).to.deep.equal(['$pageview'])
})
})
Expand Down Expand Up @@ -611,6 +651,12 @@ describe('Session recording', () => {
// no call to session-recording yet
})

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')
})

cy.resetPhCaptures()

cy.get('[data-cy-input]').type('hello posthog!')
Expand Down
15 changes: 12 additions & 3 deletions src/__tests__/extensions/replay/sessionrecording.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
pluginEvent,
} from '@rrweb/types'
import Mock = jest.Mock
import { ConsentManager } from '../../../consent'

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

Expand Down Expand Up @@ -243,14 +244,22 @@ describe('SessionRecording', () => {
config: config,
capture: jest.fn(),
persistence: postHogPersistence,
onFeatureFlags: (cb: (flags: string[]) => void) => {
onFeatureFlags: (
cb: (flags: string[], variants: Record<string, string | boolean>) => void
): (() => void) => {
onFeatureFlagsCallback = cb
return () => {}
},
sessionManager: sessionManager,
requestRouter: new RequestRouter({ config } as any),
_addCaptureHook: addCaptureHookMock,
consent: { isOptedOut: () => false },
} as unknown as PostHog
consent: {
isOptedOut(): boolean {
return false
},
} as unknown as ConsentManager,
register_for_session() {},
} as Partial<PostHog> as PostHog

loadScriptMock.mockImplementation((_ph, _path, callback) => {
addRRwebToWindow()
Expand Down
Loading
Loading