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

chore: Add wdio test for trace with replay behavior #607

Merged
merged 15 commits into from
Jul 28, 2023
Merged
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions src/features/session_trace/aggregate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@
const stopTracePerm = () => {
if (sessionEntity.state.sessionTraceMode !== MODE.OFF) sessionEntity.write({ sessionTraceMode: MODE.OFF })
operationalGate.permanentlyDecide(false)
this.#scheduler?.stopTimer(true)
if (mostRecentModeKnown === MODE.FULL) this.#scheduler?.runHarvest() // allow queued nodes (past opGate) to final harvest, unless they were buffered in other modes
this.#scheduler?.stopTimer(true) // the 'true' arg here will forcibly block any future call to runHarvest, so the last runHarvest above must be prior
this.#scheduler = null
}

Expand All @@ -122,9 +122,8 @@
this.ee.on(SESSION_EVENTS.PAUSE, () => mostRecentModeKnown = sessionEntity.state.sessionTraceMode)

if (!sessionEntity.isNew) { // inherit the same mode as existing session's Trace
const existingTraceMode = mostRecentModeKnown = sessionEntity.state.sessionTraceMode
if (existingTraceMode === MODE.OFF) this.isStandalone = true
controlTraceOp(existingTraceMode)
if (sessionEntity.state.sessionReplay === MODE.OFF) this.isStandalone = true
controlTraceOp(mostRecentModeKnown = sessionEntity.state.sessionTraceMode)

Check warning on line 126 in src/features/session_trace/aggregate/index.js

View check run for this annotation

Codecov / codecov/patch

src/features/session_trace/aggregate/index.js#L126

Added line #L126 was not covered by tests
} else { // for new sessions, see the truth table associated with NEWRELIC-8662 wrt the new Trace behavior under session management
const replayMode = await getSessionReplayMode(agentIdentifier)
if (replayMode === MODE.OFF) this.isStandalone = true // without SR, Traces are still subject to old harvest limits
Expand Down
3 changes: 2 additions & 1 deletion src/features/utils/instrument-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class InstrumentBase extends FeatureBase {
importAggregator (argsObjFromInstrument = {}) {
if (this.featAggregate || !this.auto) return
const enableSessionTracking = isBrowserScope && getConfigurationValue(this.agentIdentifier, 'privacy.cookies_enabled') === true
let loadedSuccessfully, loadFailed
let loadedSuccessfully
this.onAggregateImported = new Promise(resolve => {
loadedSuccessfully = resolve
})
Expand All @@ -78,6 +78,7 @@ export class InstrumentBase extends FeatureBase {
try {
if (!this.shouldImportAgg(this.featureName, session)) {
drain(this.agentIdentifier, this.featureName)
loadedSuccessfully(false) // aggregate module isn't loaded at all
return
}
const { lazyFeatureLoader } = await import(/* webpackChunkName: "lazy-feature-loader" */ './lazy-feature-loader')
Expand Down
5 changes: 5 additions & 0 deletions tests/specs/session-replay/helpers.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { deepmerge } from 'deepmerge-ts'

export const MODE = {
OFF: 0,
FULL: 1,
ERROR: 2
}
export const RRWEB_EVENT_TYPES = {
DomContentLoaded: 0,
Load: 1,
Expand Down
9 changes: 1 addition & 8 deletions tests/specs/session-replay/session-state.e2e.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import { supportsMultipleTabs, notIE } from '../../../tools/browser-matcher/common-matchers.mjs'
import { RRWEB_EVENT_TYPES, config, getSR } from './helpers.js'

/** The "mode" with which the session replay is recording */
const MODE = {
OFF: 0,
FULL: 1,
ERROR: 2
}
import { RRWEB_EVENT_TYPES, config, getSR, MODE } from './helpers.js'

describe.withBrowsersMatching(notIE)('session manager state behavior', () => {
beforeEach(async () => {
Expand Down
152 changes: 152 additions & 0 deletions tests/specs/stn/with-replay-error-mode.e2e.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* All behavior and mode transition from error mode of Trace in tandem with the replay feature is tested in here.
* Right now, Trace can only be in error mode when its stn flag is 0 but replay runs in error mode.
*/
import { testRumRequest } from '../../../tools/testing-server/utils/expect-tests'
import { config, MODE } from '../session-replay/helpers'
import { notIE, onlyChrome, supportsMultipleTabs } from '../../../tools/browser-matcher/common-matchers.mjs'

const getTraceMode = () => browser.execute(function () {
const agent = Object.values(newrelic.initializedAgents)[0]
return [
agent.runtime.session.state.sessionTraceMode,
agent.features.session_trace.featAggregate.isStandalone
]
})

describe.withBrowsersMatching(notIE)('Trace error mode', () => {
let getReplayOnErrorUrl
beforeEach(async () => {
await browser.destroyAgentSession()
await browser.testHandle.scheduleReply('bamServer', {
test: testRumRequest,
permanent: true,
body: JSON.stringify({ stn: 0, err: 1, ins: 1, spa: 1, sr: 1, loaded: 1 })
})

getReplayOnErrorUrl = browser.testHandle.assetURL('stn/instrumented.html', config({ session_replay: { sampleRate: 0, errorSampleRate: 1 }, session_trace: { harvestTimeSeconds: 3 } }))
})

function simulateErrorInBrowser () { // this is a way to throw error in WdIO / Selenium without killing the test itself
const errorElem = document.createElement('script')
errorElem.textContent = 'throw new Error("triggered! 0__0");'
document.body.append(errorElem)
}

it('switches to full mode when an error happens after page load', async () => {
await Promise.all([
browser.testHandle.expectResources(5001, true),
browser.url(await getReplayOnErrorUrl).then(() => browser.waitForFeatureAggregate('session_trace'))
])
await expect(getTraceMode()).resolves.toEqual([MODE.ERROR, false])

const [resources] = await Promise.all([
browser.testHandle.expectResources(),
browser.execute(simulateErrorInBrowser)
])
await expect(getTraceMode()).resolves.toEqual([MODE.FULL, false])
// The loadEventEnd is part of PT and PNT resource entry and is a node created at start of page life.
expect(resources.request.body.res).toEqual(expect.arrayContaining([expect.objectContaining({
n: 'loadEventEnd', o: 'document', t: 'timing'
})]))

await expect(browser.testHandle.expectResources(5002)).resolves.toBeTruthy() // double check there's nothing wrong with full mode interval harvest
})

it('starts in full mode when an error happens before page load', async () => {
let getFirstSTPayload = browser.testHandle.expectResources(5010)
metal-messiah marked this conversation as resolved.
Show resolved Hide resolved
await browser.url(await browser.testHandle.assetURL('js-error-with-error-before-page-load.html', config({ session_replay: { sampleRate: 0, errorSampleRate: 1 }, session_trace: { harvestTimeSeconds: 3 } })))
await browser.waitForAgentLoad()
await expect(getFirstSTPayload).resolves.toBeTruthy()
await expect(getTraceMode()).resolves.toEqual([MODE.FULL, false])

await expect(browser.testHandle.expectResources(5011)).resolves.toBeTruthy()
})

it.withBrowsersMatching(onlyChrome)('does not capture more than the last 30 seconds when error happens', async () => {
await getReplayOnErrorUrl.then(builtUrl => browser.url(builtUrl)).then(() => browser.waitForAgentLoad()).then(async () => {
await browser.pause(30000)
metal-messiah marked this conversation as resolved.
Show resolved Hide resolved
await Promise.all([
browser.testHandle.expectResources(5020), // the setup expectRes promise would've timed out already -- you will see some console errors but they don't impact validity
browser.execute(simulateErrorInBrowser)
]).then(async ([initSTAfterErr]) => {
expect(initSTAfterErr.request.body.res.find(node => node.n === 'loadEventEnd')).toBeUndefined() // that node should've been tossed out by now
metal-messiah marked this conversation as resolved.
Show resolved Hide resolved
})
})
})

it('does not perform final harvest while in this mode', async () => {
let resources = browser.testHandle.expectResources(5030, true)
await getReplayOnErrorUrl.then(builtUrl => browser.url(builtUrl)).then(() => browser.waitForAgentLoad()).then(async () => {
await expect(getTraceMode()).resolves.toEqual([MODE.ERROR, false]) // sanity check tbh
await expect(resources).resolves.toBeUndefined()

resources = browser.testHandle.expectResources(5031, true)
}).then(() => browser.refresh()).then(() => browser.waitForAgentLoad()).then(async () => {
await expect(getTraceMode()).resolves.toEqual([MODE.ERROR, false])
await expect(resources).resolves.toBeUndefined() // no harvest from either previous unload or from new existing-session load
})
})

it.withBrowsersMatching(supportsMultipleTabs)('catches mode transition from other pages in the session', async () => {
await getReplayOnErrorUrl.then(builtUrl => browser.url(builtUrl)).then(() => browser.waitForAgentLoad()).then(async () => {
await getTraceMode().then(([traceMode]) => expect(traceMode).toEqual(MODE.ERROR))
let firstPageTitle = await browser.getTitle()

await browser.newWindow(await getReplayOnErrorUrl, { windowName: 'Second page' })
await getTraceMode().then(([traceMode]) => expect(traceMode).toEqual(MODE.ERROR))
await browser.execute(simulateErrorInBrowser)
await browser.pause(500)
await getTraceMode().then(([traceMode]) => expect(traceMode).toEqual(MODE.FULL))
// NOTE: replay entitlement must be on (sr = 1) for trace to exhibit this behavior, although this could change in the future to be applicable to standalone trace in a session.

await browser.switchWindow(firstPageTitle)
await getTraceMode().then(([traceMode]) => expect(traceMode).toEqual(MODE.FULL))
})
})

/* The mode transition should also work even when replay entitlement is 0 or it is off and trace is standalone, as long as session tracking is enabled.
However, this particular behavior currently does not need testing because trace can not yet end up in error mode when running by itself. Could change in the future. */
})

describe.withBrowsersMatching(notIE)('Trace when replay runs then is aborted', () => {
beforeEach(async () => {
await browser.destroyAgentSession()
await browser.testHandle.scheduleReply('bamServer', {
test: testRumRequest,
permanent: true,
body: JSON.stringify({ stn: 0, err: 1, ins: 1, spa: 1, sr: 1, loaded: 1 })
})
})

const triggerReplayAbort = () => browser.execute(function () { Object.values(NREUM.initializedAgents)[0].runtime.session.reset() })

;[
['does a last harvest then stops, in full mode', MODE.FULL, { sampleRate: 1, errorSampleRate: 0 }],
['does not harvest anything, in error mode', MODE.ERROR, { sampleRate: 0, errorSampleRate: 1 }]
].forEach(([description, supposedMode, replayConfig]) => {
it(description, async () => {
let url = await browser.testHandle.assetURL('stn/instrumented.html', config({ session_replay: replayConfig, session_trace: { harvestTimeSeconds: 2 } }))
let getFirstSTPayload = browser.testHandle.expectResources(3000)
await browser.url(url)
await browser.waitForAgentLoad()

if (supposedMode === MODE.FULL) await expect(getFirstSTPayload).resolves.toBeTruthy()
else await expect(getFirstSTPayload).rejects.toThrow() // in ERROR mode
expect(await getTraceMode()).toEqual([supposedMode, false])

let lastSTHarvest = browser.testHandle.expectResources(1000) // abort should cause a harvest right away (in FULL), rather than the usual interval
await triggerReplayAbort()
expect(await getTraceMode()).toEqual([MODE.OFF, false])
if (supposedMode === MODE.FULL) await expect(lastSTHarvest).resolves.toBeTruthy()
else await expect(lastSTHarvest).rejects.toThrow()

let anotherTraceHarvest = browser.testHandle.expectResources(3000, true)
await expect(anotherTraceHarvest).resolves.toBeUndefined() // we shouldn't see any more harvest after the previous one on abort

anotherTraceHarvest = browser.testHandle.expectResources(2000, true)
await browser.url(await browser.testHandle.assetURL('/'))
await expect(anotherTraceHarvest).resolves.toBeUndefined() // doubly check that nothing else is sent, i.e. on test page's unload logic
})
})
})
Loading
Loading