Skip to content

Commit 531f8d4

Browse files
fix: Fix race between end of session and features aborting (#1487)
Co-authored-by: Chunwai Li <cli@newrelic.com>
1 parent 50f4ace commit 531f8d4

File tree

11 files changed

+8
-114
lines changed

11 files changed

+8
-114
lines changed

src/common/harvest/harvester.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { VERSION } from '../constants/env'
88
import { globalScope, isWorkerScope } from '../constants/runtime'
99
import { handle } from '../event-emitter/handle'
1010
import { eventListenerOpts } from '../event-listener/event-listener-opts'
11-
import { SESSION_EVENTS } from '../session/constants'
1211
import { now } from '../timing/now'
1312
import { subscribeToEOL } from '../unload/eol'
1413
import { cleanURL } from '../url/clean-url'
@@ -36,10 +35,6 @@ export class Harvester {
3635
this.initializedAggregates.forEach(aggregateInst => this.triggerHarvestFor(aggregateInst, { isFinalHarvest: true }))
3736
/* This callback should run in bubble phase, so that that CWV api, like "onLCP", is called before the final harvest so that emitted timings are part of last outgoing. */
3837
}, false)
39-
40-
/* Flush all buffered data if session resets and give up retries. This should be synchronous to ensure that the correct `session` value is sent.
41-
Since session-reset generates a new session ID and the ID is grabbed at send-time, any delays or retries would cause the payload to be sent under the wrong session ID. */
42-
agentRef.ee.on(SESSION_EVENTS.RESET, () => this.initializedAggregates.forEach(aggregateInst => this.triggerHarvestFor(aggregateInst, { forceNoRetry: true })))
4338
}
4439

4540
startTimer (harvestInterval = this.agentRef.init.harvest.interval) {

src/common/session/session-entity.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,10 @@ export class SessionEntity {
227227
// * stop recording (stn and sr)...
228228
// * delete the session and start over
229229
try {
230-
if (this.initialized) this.ee.emit(SESSION_EVENTS.RESET)
230+
if (this.initialized) {
231+
this.ee.emit(SESSION_EVENTS.RESET)
232+
this.state.numOfResets++
233+
}
231234
this.storage.remove(this.lookupKey)
232235
this.inactiveTimer?.abort?.()
233236
this.expiresTimer?.clear?.()
@@ -239,7 +242,7 @@ export class SessionEntity {
239242
storage: this.storage,
240243
expiresMs: this.expiresMs,
241244
inactiveMs: this.inactiveMs,
242-
numOfResets: ++this.state.numOfResets
245+
numOfResets: this.state.numOfResets
243246
})
244247
return this.read()
245248
} catch (e) {
@@ -256,10 +259,6 @@ export class SessionEntity {
256259
this.write({ ...existingData, inactiveAt: this.getFutureTimestamp(this.inactiveMs) })
257260
}
258261

259-
isAfterSessionExpiry (timestamp) {
260-
return this.state.numOfResets > 0 || (typeof timestamp === 'number' && typeof this.state.expiresAt === 'number' && timestamp >= this.state.expiresAt)
261-
}
262-
263262
/**
264263
* @param {number} timestamp
265264
* @returns {boolean}

src/features/session_replay/shared/recorder.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,6 @@ export class Recorder {
155155
/** Store a payload in the buffer (this.#events). This should be the callback to the recording lib noticing a mutation */
156156
store (event, isCheckout) {
157157
if (!event) return
158-
if (this.parent.agentRef.runtime.session?.isAfterSessionExpiry(event.timestamp)) {
159-
handle(SUPPORTABILITY_METRIC_CHANNEL, ['Session/Expired/SessionReplay/Seen'], undefined, FEATURE_NAMES.metrics, this.ee)
160-
return
161-
}
162158

163159
if (!(this.parent instanceof AggregateBase) && this.#preloaded.length) this.currentBufferTarget = this.#preloaded[this.#preloaded.length - 1]
164160
else this.currentBufferTarget = this.#events

src/features/session_trace/aggregate/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export class Aggregate extends AggregateBase {
103103
}
104104

105105
preHarvestChecks () {
106-
if (this.mode !== MODE.FULL) return // only allow harvest if running in full mode
106+
if (this.blocked || this.mode !== MODE.FULL) return // only allow harvest if running in full mode
107107
if (!this.timeKeeper?.ready) return // this should likely never happen, but just to be safe, we should never harvest if we cant correct time
108108
if (!this.agentRef.runtime.session) return // session entity is required for trace to run and continue running
109109
if (this.sessionId !== this.agentRef.runtime.session.state.value || this.ptid !== this.agentRef.runtime.ptid) {

src/features/session_trace/aggregate/trace/storage.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,6 @@ export class TraceStorage {
4141
this.parent = parent
4242
}
4343

44-
isAfterSessionExpiry (entryTimestamp) {
45-
return this.parent.agentRef.runtime?.session?.isAfterSessionExpiry((this.parent.timeKeeper?.ready && this.parent.timeKeeper.convertRelativeTimestamp(entryTimestamp)) ?? undefined)
46-
}
47-
4844
/** Central function called by all the other store__ & addToTrace API to append a trace node. */
4945
storeSTN (stn) {
5046
if (this.parent.blocked) return
@@ -53,10 +49,6 @@ export class TraceStorage {
5349
const openedSpace = this.trimSTNs(ERROR_MODE_SECONDS_WINDOW) // but maybe we could make some space by discarding irrelevant nodes if we're in sessioned Error mode
5450
if (openedSpace === 0) return
5551
}
56-
if (this.isAfterSessionExpiry(stn.s)) {
57-
this.parent.reportSupportabilityMetric('Session/Expired/SessionTrace/Seen')
58-
return
59-
}
6052

6153
if (this.trace[stn.n]) this.trace[stn.n].push(stn)
6254
else this.trace[stn.n] = [stn]

tests/components/session-entity.test.js

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -295,28 +295,3 @@ describe('syncCustomAttribute()', () => {
295295
expect(session.read().custom?.test).toEqual(undefined)
296296
})
297297
})
298-
299-
describe('isAfterSessionExpiry', () => {
300-
test('returns true if session has expired before', () => {
301-
const now = Date.now()
302-
jest.setSystemTime(now)
303-
const session = new SessionEntity({ agentIdentifier, key, storage, expiresMs: 10 })
304-
jest.advanceTimersByTime(11)
305-
expect(session.state.numOfResets).toEqual(1)
306-
expect(session.isAfterSessionExpiry()).toEqual(true)
307-
})
308-
309-
test('returns true if event timestamp is equal to session expiry', () => {
310-
const now = Date.now()
311-
jest.setSystemTime(now)
312-
const session = new SessionEntity({ agentIdentifier, key, storage, expiresMs: 10 })
313-
expect(session.isAfterSessionExpiry(now + 10)).toEqual(true)
314-
})
315-
316-
test('returns false if session has never expired AND event timestamp occurs before session expiry', () => {
317-
const now = Date.now()
318-
jest.setSystemTime(now)
319-
const session = new SessionEntity({ agentIdentifier, key, storage, expiresMs: 10 })
320-
expect(session.isAfterSessionExpiry(now + 9)).toEqual(false)
321-
})
322-
})

tests/components/setup-agent.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,4 @@ function resetAggregator (agentIdentifier) {
101101
function resetSession (agentIdentifier) {
102102
const agent = getNREUMInitializedAgent(agentIdentifier)
103103
agent.runtime.session.reset()
104-
agent.runtime.session.state.numOfResets = 0 // avoid stopping harvests for session resets
105104
}

tests/specs/session-replay/session-state.e2e.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,16 @@ describe('session manager state behavior', () => {
4848

4949
describe('when session ends', () => {
5050
it('should end recording and clear the buffer', async () => {
51+
const sessionReplayCapture = await browser.testHandle.createNetworkCaptures('bamServer', { test: testBlobReplayRequest })
5152
await browser.enableSessionReplay()
52-
await browser.url(await browser.testHandle.assetURL('rrweb-record.html', { init: { session: { expiresMs: 7500 }, session_replay: { enabled: true } } }))
53+
await browser.url(await browser.testHandle.assetURL('rrweb-instrumented.html', { init: { session: { expiresMs: 7500 }, session_replay: { enabled: true } } }))
5354
.then(() => browser.waitForSessionReplayRecording())
5455

5556
// session has started, replay should have set mode to "FULL"
5657
const { agentSessions: oldSession } = await browser.getAgentSessionInfo()
5758
const oldSessionClass = Object.values(oldSession)[0]
5859
expect(oldSessionClass.sessionReplayMode).toEqual(MODE.FULL)
5960

60-
const sessionReplayCapture = await browser.testHandle.createNetworkCaptures('bamServer', { test: testBlobReplayRequest })
6161
const [sessionReplayHarvests] = await Promise.all([
6262
sessionReplayCapture.waitForResult({ timeout: 10000 }),
6363
browser.execute(function () {

tests/unit/common/harvest/harvester.test.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ test('Harvester does not start timer loop on initialization', () => {
2525

2626
const harvester = new Harvester(fakeAgent)
2727
expect(mockEolCb).not.toBeUndefined()
28-
expect(fakeAgent.ee.on).toHaveBeenCalledTimes(1)
2928
expect(harvester.agentRef).toEqual(fakeAgent)
3029
expect(global.setTimeout).not.toHaveBeenCalled()
3130
})

tests/unit/features/session_replay/shared/recorder.test.js

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,6 @@
11
import { Recorder } from '../../../../../src/features/session_replay/shared/recorder'
22

33
describe('recorder', () => {
4-
test('should not store event if past session expiry', async () => {
5-
const event = { timestamp: 123 }
6-
const recorder = new Recorder({
7-
agentRef: {
8-
init: {
9-
session_replay: {
10-
stylesheets: true
11-
}
12-
},
13-
runtime: {
14-
session: {
15-
state: {
16-
expiresAt: 123
17-
},
18-
isAfterSessionExpiry: () => true
19-
}
20-
}
21-
},
22-
reportSupportabilityMetric: jest.fn()
23-
})
24-
25-
recorder.store(event)
26-
27-
expect(recorder.getEvents().events).toHaveLength(0)
28-
})
29-
304
// it's possible for events collection to start before session is initialized
315
test('should store event if no session present', async () => {
326
const event = { timestamp: 123 }

0 commit comments

Comments
 (0)