Skip to content

Commit

Permalink
fix: Tap session entity into storage api for changes across tabs (#741)
Browse files Browse the repository at this point in the history
  • Loading branch information
metal-messiah authored Oct 3, 2023
1 parent e6c0a1a commit 81bedc6
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 4 deletions.
20 changes: 19 additions & 1 deletion src/common/session/session-entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { getModeledObject } from '../config/state/configurable'
import { handle } from '../event-emitter/handle'
import { SUPPORTABILITY_METRIC_CHANNEL } from '../../features/metrics/constants'
import { FEATURE_NAMES } from '../../loaders/features/features'
import { windowAddEventListener } from '../event-listener/event-listener-opts'

export const MODE = {
OFF: 0,
Expand All @@ -32,7 +33,13 @@ const model = {
export const SESSION_EVENTS = {
PAUSE: 'session-pause',
RESET: 'session-reset',
RESUME: 'session-resume'
RESUME: 'session-resume',
UPDATE: 'session-update'
}

export const SESSION_EVENT_TYPES = {
SAME_TAB: 'same-tab',
CROSS_TAB: 'cross-tab'
}

export class SessionEntity {
Expand All @@ -58,6 +65,16 @@ export class SessionEntity {
this.ee = ee.get(agentIdentifier)
wrapEvents(this.ee)
this.setup(opts)

if (isBrowserScope) {
windowAddEventListener('storage', (event) => {
if (event.key === this.lookupKey) {
const obj = typeof event.newValue === 'string' ? JSON.parse(event.newValue) : event.newValue
this.sync(obj)
this.ee.emit(SESSION_EVENTS.UPDATE, [SESSION_EVENT_TYPES.CROSS_TAB, this.state])
}
})
}
}

setup ({ value = generateRandomHexString(16), expiresMs = DEFAULT_EXPIRES_MS, inactiveMs = DEFAULT_INACTIVE_MS }) {
Expand Down Expand Up @@ -190,6 +207,7 @@ export class SessionEntity {
//
// TODO - compression would need happen here if we decide to do it
this.storage.set(this.lookupKey, stringify(this.state))
this.ee.emit(SESSION_EVENTS.UPDATE, [SESSION_EVENT_TYPES.SAME_TAB, this.state])
return data
} catch (e) {
// storage is inaccessible
Expand Down
11 changes: 9 additions & 2 deletions src/features/session_replay/aggregate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
import { FEATURE_NAME } from '../constants'
import { stringify } from '../../../common/util/stringify'
import { getConfigurationValue, getInfo, getRuntime } from '../../../common/config/config'
import { SESSION_EVENTS, MODE } from '../../../common/session/session-entity'
import { SESSION_EVENTS, MODE, SESSION_EVENT_TYPES } from '../../../common/session/session-entity'
import { AggregateBase } from '../../utils/aggregate-base'
import { sharedChannel } from '../../../common/constants/shared-channel'
import { obj as encodeObj } from '../../../common/url/encode'
Expand Down Expand Up @@ -123,6 +123,12 @@ export class Aggregate extends AggregateBase {
this.startRecording()
})

this.ee.on(SESSION_EVENTS.UPDATE, (type, data) => {
if (!this.initialized || this.blocked || type !== SESSION_EVENT_TYPES.CROSS_TAB) return
if (this.mode !== MODE.OFF && data.sessionReplay === MODE.OFF) this.abort('Session Entity was set to OFF on another tab')
this.mode = data.sessionReplay
})

// Bespoke logic for new endpoint. This will change as downstream dependencies become solidified.
this.scheduler = new HarvestScheduler('browser/blobs', {
onFinished: this.onHarvestFinished.bind(this),
Expand All @@ -137,12 +143,13 @@ export class Aggregate extends AggregateBase {
this.hasError = true
this.errorNoticed = true
// run once
if (this.mode === MODE.ERROR) {
if (this.mode === MODE.ERROR && globalScope?.document.visibilityState === 'visible') {
this.mode = MODE.FULL
// if the error was noticed AFTER the recorder was already imported....
if (recorder && this.initialized) {
this.stopRecording()
this.startRecording()

this.scheduler.startTimer(this.harvestTimeSeconds)

this.syncWithSessionManager({ sessionReplay: this.mode })
Expand Down
44 changes: 43 additions & 1 deletion tests/specs/session-replay/session-pages.e2e.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { testRumRequest } from '../../../tools/testing-server/utils/expect-tests.js'
import { config, testExpectedReplay } from './helpers'
import { config, getSR, testExpectedReplay } from './helpers'
import { supportsMultipleTabs, notIE, notSafari } from '../../../tools/browser-matcher/common-matchers.mjs'

describe.withBrowsersMatching(notIE)('Session Replay Across Pages', () => {
Expand Down Expand Up @@ -102,4 +102,46 @@ describe.withBrowsersMatching(notIE)('Session Replay Across Pages', () => {

await expect(browser.waitForFeatureAggregate('session_replay', 5000)).rejects.toThrow()
})

// As of 06/26/2023 test fails in Safari, though tested behavior works in a live browser (revisit in NR-138940).
it.withBrowsersMatching([supportsMultipleTabs, notSafari])('should kill active tab if killed in backgrounded tab', async () => {
await browser.url(await browser.testHandle.assetURL('rrweb-instrumented.html', config()))
.then(() => browser.waitForAgentLoad())

const { request: page1Contents } = await browser.testHandle.expectBlob(10000)
const { localStorage } = await browser.getAgentSessionInfo()

testExpectedReplay({ data: page1Contents, session: localStorage.value, hasError: false, hasMeta: true, hasSnapshot: true, isFirstChunk: true })

const newTab = await browser.createWindow('tab')
await browser.switchToWindow(newTab.handle)
await browser.enableSessionReplay()
await browser.url(await browser.testHandle.assetURL('rrweb-instrumented.html', config()))
.then(() => browser.waitForAgentLoad())

const { request: page2Contents } = await browser.testHandle.expectBlob(10000)

testExpectedReplay({ data: page2Contents, session: localStorage.value, hasError: false, hasMeta: true, hasSnapshot: true, isFirstChunk: false })

const page2Blocked = await browser.execute(function () {
try {
var agg = Object.values(newrelic.initializedAgents)[0].features.session_replay.featAggregate
agg.abort()
return agg.blocked
} catch (err) {
return false
}
})
await browser.closeWindow()
await browser.switchToWindow((await browser.getWindowHandles())[0])

expect(page2Blocked).toEqual(true)
await expect(getSR()).resolves.toEqual(expect.objectContaining({
events: [],
initialized: true,
recording: false,
mode: 0,
blocked: true
}))
})
})

0 comments on commit 81bedc6

Please sign in to comment.