Skip to content

Commit 42a15e1

Browse files
fix: Set Session Replay first chunk flags more reliably (#740)
1 parent 2d7a363 commit 42a15e1

File tree

6 files changed

+23
-12
lines changed

6 files changed

+23
-12
lines changed

src/common/session/session-entity.component-test.js

+10-4
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ describe('constructor', () => {
8181
expiresAt: expect.any(Number),
8282
inactiveAt: expect.any(Number),
8383
sessionReplay: expect.any(Number),
84+
sessionReplaySentFirstChunk: expect.any(Boolean),
8485
sessionTraceMode: expect.any(Number)
8586
})
8687
})
@@ -94,6 +95,7 @@ describe('constructor', () => {
9495
inactiveAt: expect.any(Number),
9596
updatedAt: expect.any(Number),
9697
sessionReplay: expect.any(Number),
98+
sessionReplaySentFirstChunk: expect.any(Boolean),
9799
sessionTraceMode: expect.any(Number)
98100
}))
99101
})
@@ -108,7 +110,7 @@ describe('constructor', () => {
108110
test('expiresAt is the correct future timestamp - existing session', () => {
109111
const now = Date.now()
110112
jest.setSystemTime(now)
111-
const existingData = new LocalMemory({ [`${PREFIX}_${key}`]: { value, expiresAt: now + 5000, inactiveAt: Infinity, updatedAt: now, sessionReplay: 0, sessionTraceMode: 0, custom: {} } })
113+
const existingData = new LocalMemory({ [`${PREFIX}_${key}`]: { value, expiresAt: now + 5000, inactiveAt: Infinity, updatedAt: now, sessionReplay: 0, sessionReplaySentFirstChunk: false, sessionTraceMode: 0, custom: {} } })
112114
const session = new SessionEntity({ agentIdentifier, key, expiresMs: 100, storage: existingData })
113115
expect(session.state.expiresAt).toEqual(now + 5000)
114116
})
@@ -128,7 +130,7 @@ describe('constructor', () => {
128130
test('inactiveAt is the correct future timestamp - existing session', () => {
129131
const now = Date.now()
130132
jest.setSystemTime(now)
131-
const existingData = new LocalMemory({ [`${PREFIX}_${key}`]: { value, inactiveAt: now + 5000, expiresAt: Infinity, updatedAt: now, sessionReplay: 0, sessionTraceMode: 0, custom: {} } })
133+
const existingData = new LocalMemory({ [`${PREFIX}_${key}`]: { value, inactiveAt: now + 5000, expiresAt: Infinity, updatedAt: now, sessionReplay: 0, sessionReplaySentFirstChunk: false, sessionTraceMode: 0, custom: {} } })
132134
const session = new SessionEntity({ agentIdentifier, key, inactiveMs: 100, storage: existingData })
133135
expect(session.state.inactiveAt).toEqual(now + 5000)
134136
})
@@ -142,7 +144,7 @@ describe('constructor', () => {
142144
const newSession = new SessionEntity({ agentIdentifier, key, storage, expiresMs: 10 })
143145
expect(newSession.isNew).toBeTruthy()
144146

145-
const newStorage = new LocalMemory({ [`${PREFIX}_${key}`]: { value, expiresAt: Infinity, inactiveAt: Infinity, updatedAt: Date.now(), sessionReplay: 0, sessionTraceMode: 0, custom: {} } })
147+
const newStorage = new LocalMemory({ [`${PREFIX}_${key}`]: { value, expiresAt: Infinity, inactiveAt: Infinity, updatedAt: Date.now(), sessionReplay: 0, sessionReplaySentFirstChunk: false, sessionTraceMode: 0, custom: {} } })
146148
const existingSession = new SessionEntity({ agentIdentifier, key, expiresMs: 10, storage: newStorage })
147149
expect(existingSession.isNew).toBeFalsy()
148150
})
@@ -157,6 +159,7 @@ describe('constructor', () => {
157159
inactiveAt: expect.any(Number),
158160
updatedAt: expect.any(Number),
159161
sessionReplay: expect.any(Number),
162+
sessionReplaySentFirstChunk: expect.any(Boolean),
160163
sessionTraceMode: expect.any(Number)
161164
}))
162165
})
@@ -172,6 +175,7 @@ describe('constructor', () => {
172175
inactiveAt: expect.any(Number),
173176
updatedAt: expect.any(Number),
174177
sessionReplay: expect.any(Number),
178+
sessionReplaySentFirstChunk: expect.any(Boolean),
175179
sessionTraceMode: expect.any(Number)
176180
}))
177181
})
@@ -187,6 +191,7 @@ describe('constructor', () => {
187191
inactiveAt: expect.any(Number),
188192
updatedAt: expect.any(Number),
189193
sessionReplay: expect.any(Number),
194+
sessionReplaySentFirstChunk: expect.any(Boolean),
190195
sessionTraceMode: expect.any(Number)
191196
}))
192197
})
@@ -229,12 +234,13 @@ describe('read()', () => {
229234
expiresAt: expect.any(Number),
230235
inactiveAt: expect.any(Number),
231236
sessionReplay: expect.any(Number),
237+
sessionReplaySentFirstChunk: expect.any(Boolean),
232238
sessionTraceMode: expect.any(Number)
233239
}))
234240
})
235241

236242
test('"pre-existing" sessions get data from read()', () => {
237-
const storage = new LocalMemory({ [`${PREFIX}_${key}`]: { value, expiresAt: Infinity, inactiveAt: Infinity, updatedAt: Date.now(), sessionReplay: 0, sessionTraceMode: 0, custom: {} } })
243+
const storage = new LocalMemory({ [`${PREFIX}_${key}`]: { value, expiresAt: Infinity, inactiveAt: Infinity, updatedAt: Date.now(), sessionReplay: 0, sessionReplaySentFirstChunk: false, sessionTraceMode: 0, custom: {} } })
238244
const session = new SessionEntity({ agentIdentifier, key, storage })
239245
expect(session.isNew).toBeFalsy()
240246
expect(session.read()).toEqual(expect.objectContaining({

src/common/session/session-entity.js

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const model = {
2525
expiresAt: 0,
2626
updatedAt: Date.now(),
2727
sessionReplay: MODE.OFF,
28+
sessionReplaySentFirstChunk: false,
2829
sessionTraceMode: MODE.OFF,
2930
custom: {}
3031
}

src/features/session_replay/aggregate/index.component-test.js

+4-2
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ describe('Session Replay', () => {
5454
primeSessionAndReplay()
5555
})
5656
afterEach(async () => {
57-
sr.abort()
57+
sr.abort('jest test manually aborted')
5858
jest.clearAllMocks()
5959
})
6060

@@ -177,7 +177,7 @@ describe('Session Replay', () => {
177177
})
178178

179179
test('Existing Session -- Should inherit mode from session entity and ignore samples', async () => {
180-
const storage = new LocalMemory({ NRBA_SESSION: { value: 'abcdefghijklmnop', expiresAt: Date.now() + 10000, inactiveAt: Date.now() + 10000, updatedAt: Date.now(), sessionReplay: MODE.FULL, sessionTraceMode: MODE.FULL, custom: {} } })
180+
const storage = new LocalMemory({ NRBA_SESSION: { value: 'abcdefghijklmnop', expiresAt: Date.now() + 10000, inactiveAt: Date.now() + 10000, updatedAt: Date.now(), sessionReplay: MODE.FULL, sessionReplaySentFirstChunk: true, sessionTraceMode: MODE.FULL, custom: {} } })
181181
session = new SessionEntity({ agentIdentifier, key: 'SESSION', storage })
182182
expect(session.isNew).toBeFalsy()
183183
primeSessionAndReplay(session)
@@ -235,6 +235,7 @@ describe('Session Replay', () => {
235235
const [harvestContents] = sr.prepareHarvest()
236236
expect(harvestContents.qs).toMatchObject(anyQuery)
237237
expect(harvestContents.qs.attributes.includes('content_encoding=gzip')).toEqual(true)
238+
expect(harvestContents.qs.attributes.includes('isFirstChunk=true')).toEqual(true)
238239
expect(harvestContents.body).toEqual(expect.any(Uint8Array))
239240
expect(JSON.parse(strFromU8(gunzipSync(harvestContents.body)))).toMatchObject(expect.any(Array))
240241
})
@@ -256,6 +257,7 @@ describe('Session Replay', () => {
256257
browser_monitoring_key: info.licenseKey
257258
})
258259
expect(harvestContents.qs.attributes.includes('content_encoding')).toEqual(false)
260+
expect(harvestContents.qs.attributes.includes('isFirstChunk')).toEqual(true)
259261
expect(harvestContents.body).toEqual(expect.any(Array))
260262
})
261263

src/features/session_replay/aggregate/index.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,6 @@ export class Aggregate extends AggregateBase {
5959
/** can shut off efforts to compress the data */
6060
this.shouldCompress = true
6161

62-
/** Payload metadata -- Should indicate that the payload being sent is the first of a session */
63-
this.isFirstChunk = false
6462
/** Payload metadata -- Should indicate that the payload being sent has a full DOM snapshot. This can happen
6563
* -- When the recording library begins recording, it starts by taking a DOM snapshot
6664
* -- When visibility changes from "hidden" -> "visible", it must capture a full snapshot for the replay to work correctly across tabs
@@ -100,6 +98,9 @@ export class Aggregate extends AggregateBase {
10098
this.ee.on(SESSION_EVENTS.PAUSE, () => { this.stopRecording() })
10199
// The SessionEntity class can emit a message indicating the session was resumed (visibility change). This feature must start running again (if already running) if that occurs.
102100
this.ee.on(SESSION_EVENTS.RESUME, () => {
101+
// if the mode changed on a different tab, it needs to update this instance to match
102+
const { session } = getRuntime(this.agentIdentifier)
103+
this.mode = session.state.sessionReplay
103104
if (!this.initialized || this.mode === MODE.OFF) return
104105
this.startRecording()
105106
})
@@ -200,8 +201,6 @@ export class Aggregate extends AggregateBase {
200201
}
201202
this.startRecording()
202203

203-
this.isFirstChunk = !!session.isNew
204-
205204
this.syncWithSessionManager({ sessionReplay: this.mode })
206205
}
207206

@@ -216,6 +215,8 @@ export class Aggregate extends AggregateBase {
216215
this.scheduler.opts.gzip = false
217216
}
218217
// TODO -- Gracefully handle the buffer for retries.
218+
const { session } = getRuntime(this.agentIdentifier)
219+
if (!session.state.sessionReplaySentFirstChunk) this.syncWithSessionManager({ sessionReplaySentFirstChunk: true })
219220
this.clearBuffer()
220221
return [payload]
221222
}
@@ -241,7 +242,7 @@ export class Aggregate extends AggregateBase {
241242
hasMeta: this.hasMeta,
242243
hasSnapshot: this.hasSnapshot,
243244
hasError: this.hasError,
244-
isFirstChunk: this.isFirstChunk,
245+
isFirstChunk: agentRuntime.session.state.sessionReplaySentFirstChunk === false,
245246
decompressedBytes: this.payloadBytesEstimation,
246247
'nr.rrweb.version': RRWEB_VERSION
247248
}, MAX_PAYLOAD_SIZE - this.payloadBytesEstimation).substring(1) // remove the leading '&'
@@ -262,7 +263,6 @@ export class Aggregate extends AggregateBase {
262263
/** Clears the buffer (this.events), and resets all payload metadata properties */
263264
clearBuffer () {
264265
this.events = []
265-
this.isFirstChunk = false
266266
this.hasSnapshot = false
267267
this.hasMeta = false
268268
this.hasError = false

tests/specs/session-manager.e2e.js

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ describe('newrelic session ID', () => {
1313
inactiveAt: expect.any(Number),
1414
updatedAt: expect.any(Number),
1515
sessionReplay: expect.any(Number),
16+
sessionReplaySentFirstChunk: expect.any(Boolean),
1617
sessionTraceMode: expect.any(Number),
1718
custom: expect.any(Object)
1819
})

tools/wdio/plugins/custom-commands.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export default class CustomCommands {
7373
expiresAt: agentEntry[1].runtime.session.state.expiresAt,
7474
updatedAt: agentEntry[1].runtime.session.state.updatedAt,
7575
sessionReplay: agentEntry[1].runtime.session.state.sessionReplay,
76+
sessionReplaySentFirstChunk: agentEntry[1].runtime.session.state.sessionReplaySentFirstChunk,
7677
sessionTraceMode: agentEntry[1].runtime.session.state.sessionTraceMode,
7778
custom: agentEntry[1].runtime.session.state.custom
7879
}

0 commit comments

Comments
 (0)