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: Persist logging flag in session #1264

Merged
merged 13 commits into from
Dec 3, 2024
2 changes: 2 additions & 0 deletions src/common/session/session-entity.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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'
import { LOGGING_MODE } from '../../features/logging/constants'

// this is what can be stored in local storage (not enforced but probably should be)
// these values should sync between local storage and the parent class props
Expand All @@ -24,6 +25,7 @@ const model = {
sessionReplaySentFirstChunk: false,
sessionTraceMode: MODE.OFF,
traceHarvestStarted: false,
loggingMode: LOGGING_MODE.OFF,
serverTimeDiff: null, // set by TimeKeeper; "undefined" value will not be stringified and stored but "null" will
custom: {}
}
Expand Down
40 changes: 37 additions & 3 deletions src/features/logging/aggregate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,45 @@
import { isValidLogLevel } from '../shared/utils'
import { applyFnToProps } from '../../../common/util/traverse'
import { MAX_PAYLOAD_SIZE } from '../../../common/constants/agent-constants'
import { FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
import { FEATURE_NAMES, FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
import { SESSION_EVENT_TYPES, SESSION_EVENTS } from '../../../common/session/constants'
import { ABORT_REASONS } from '../../session_replay/constants'

export class Aggregate extends AggregateBase {
static featureName = FEATURE_NAME
constructor (agentRef) {
super(agentRef, FEATURE_NAME)
this.harvestTimeSeconds = agentRef.init.logging.harvestTimeSeconds

// The SessionEntity class can emit a message indicating the session was cleared and reset (expiry, inactivity). This feature must abort and never resume if that occurs.
this.ee.on(SESSION_EVENTS.RESET, () => {
this.abort(ABORT_REASONS.RESET)

Check warning on line 25 in src/features/logging/aggregate/index.js

View check run for this annotation

Codecov / codecov/patch

src/features/logging/aggregate/index.js#L25

Added line #L25 was not covered by tests
})

this.ee.on(SESSION_EVENTS.UPDATE, (type, data) => {
if (this.blocked || type !== SESSION_EVENT_TYPES.CROSS_TAB) return
if (this.mode !== LOGGING_MODE.OFF && data.loggingMode === LOGGING_MODE.OFF) this.abort(ABORT_REASONS.CROSS_TAB)
else this.mode = data.loggingMode
})

this.waitForFlags(['log']).then(([loggingMode]) => {
if (!loggingMode) {
const session = this.agentRef.runtime.session ?? {}
if (this.loggingMode === LOGGING_MODE.OFF || (session.isNew && loggingMode === LOGGING_MODE.OFF)) {
this.blocked = true
this.deregisterDrain()
return
}
this.loggingMode = loggingMode
if (session.isNew || !this.isSessionTrackingEnabled) {
this.loggingMode = loggingMode

if (this.isSessionTrackingEnabled) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this if statement is redundant now (since syncWith.. does the same check), but nbd

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, i wasn't sure if it's worth blocking the extra call so added it. Sounds like probably not worth it (?) I can avoid doing that in the future.

this.syncWithSessionManager({
loggingMode: this.loggingMode
})
}
} else {

Check warning on line 49 in src/features/logging/aggregate/index.js

View check run for this annotation

Codecov / codecov/patch

src/features/logging/aggregate/index.js#L49

Added line #L49 was not covered by tests
this.loggingMode = session.state.loggingMode
}
this.scheduler = new HarvestScheduler(FEATURE_TO_ENDPOINT[this.featureName], {
onFinished: (result) => this.postHarvestCleanup(result.sent && result.retry),
retryDelay: this.harvestTimeSeconds,
Expand Down Expand Up @@ -123,4 +147,14 @@
queryStringsBuilder () {
return { browser_monitoring_key: this.agentRef.info.licenseKey }
}

/** Abort the feature, once aborted it will not resume */
abort (reason = {}) {
handle(SUPPORTABILITY_METRIC_CHANNEL, [`Logging/Abort/${reason.sm}`], undefined, FEATURE_NAMES.logging, this.ee)
this.blocked = true
this.loggingMode = LOGGING_MODE.OFF
this.syncWithSessionManager({ loggingMode: this.loggingMode })
metal-messiah marked this conversation as resolved.
Show resolved Hide resolved
this.events.clear()
this.deregisterDrain()

Check warning on line 158 in src/features/logging/aggregate/index.js

View check run for this annotation

Codecov / codecov/patch

src/features/logging/aggregate/index.js#L152-L158

Added lines #L152 - L158 were not covered by tests
}
}
16 changes: 5 additions & 11 deletions src/features/session_replay/aggregate/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
this.ee.on(SESSION_EVENTS.UPDATE, (type, data) => {
if (!this.recorder || !this.initialized || this.blocked || type !== SESSION_EVENT_TYPES.CROSS_TAB) return
if (this.mode !== MODE.OFF && data.sessionReplayMode === MODE.OFF) this.abort(ABORT_REASONS.CROSS_TAB)
this.mode = data.sessionReplay
this.mode = data.sessionReplayMode

Check warning on line 76 in src/features/session_replay/aggregate/index.js

View check run for this annotation

Codecov / codecov/patch

src/features/session_replay/aggregate/index.js#L76

Added line #L76 was not covered by tests
})

// Bespoke logic for blobs endpoint.
Expand Down Expand Up @@ -147,16 +147,14 @@
this.scheduler.startTimer(this.harvestTimeSeconds)
this.syncWithSessionManager({ sessionReplayMode: this.mode })
} else {
this.initializeRecording(false, true, true)
this.initializeRecording(false, true)
}
}

/**
* Evaluate entitlements and sampling before starting feature mechanics, importing and configuring recording library, and setting storage state
* @param {boolean} entitlements - the true/false state of the "sr" flag from RUM response
* @param {boolean} errorSample - the true/false state of the error sampling decision
* @param {boolean} fullSample - the true/false state of the full sampling decision
* @param {boolean} ignoreSession - whether to force the method to ignore the session state and use just the sample flags
* Evaluate entitlements (which already accounts for sampling) before starting feature mechanics, importing and configuring recording library, and setting storage state
* @param {boolean} srMode - the true/false state of the "sr" flag from RUM response
* @param {boolean} ignoreSession - whether to force the method to ignore the session state and use just the "sr" flag
* @returns {void}
*/
async initializeRecording (srMode, ignoreSession) {
Expand Down Expand Up @@ -394,8 +392,4 @@
this.ee.emit('REPLAY_ABORTED')
while (this.recorder?.getEvents().events.length) this.recorder?.clearBuffer?.()
}

syncWithSessionManager (state = {}) {
this.agentRef.runtime.session.write(state)
}
}
8 changes: 8 additions & 0 deletions src/features/utils/aggregate-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { activatedFeatures } from '../../common/util/feature-flags'
import { Obfuscator } from '../../common/util/obfuscate'
import { EventBuffer } from './event-buffer'
import { FEATURE_NAMES } from '../../loaders/features/features'
import { canEnableSessionTracking } from './feature-gates'

export class AggregateBase extends FeatureBase {
constructor (agentRef, featureName) {
Expand All @@ -18,6 +19,7 @@ export class AggregateBase extends FeatureBase {
else if (![FEATURE_NAMES.pageViewEvent, FEATURE_NAMES.sessionTrace].includes(this.featureName)) this.events = new EventBuffer()
this.checkConfiguration(agentRef)
this.obfuscator = agentRef.runtime.obfuscator
this.isSessionTrackingEnabled = canEnableSessionTracking(this.agentIdentifier) && this.agentRef.runtime.session
}

/**
Expand Down Expand Up @@ -117,4 +119,10 @@ export class AggregateBase extends FeatureBase {
existingAgent.runtime.obfuscator = new Obfuscator(this.agentIdentifier)
}
}

syncWithSessionManager (state = {}) {
if (this.isSessionTrackingEnabled) {
this.agentRef.runtime.session.write(state)
}
}
}
59 changes: 7 additions & 52 deletions tests/components/session-entity.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { PREFIX } from '../../src/common/session/constants'
import { SessionEntity } from '../../src/common/session/session-entity'
import { LocalMemory, model } from './session-helpers'
import * as runtimeModule from '../../src/common/constants/runtime'
import { buildExpectedSessionState } from '../specs/util/helpers'

jest.useFakeTimers()

Expand Down Expand Up @@ -33,28 +34,13 @@ describe('constructor', () => {
inactiveTimer: expect.any(Object),
isNew: expect.any(Boolean),
storage: expect.any(Object),
state: expect.objectContaining({
value: expect.any(String),
expiresAt: expect.any(Number),
inactiveAt: expect.any(Number),
sessionReplayMode: expect.any(Number),
sessionReplaySentFirstChunk: expect.any(Boolean),
sessionTraceMode: expect.any(Number)
})
state: expect.objectContaining(buildExpectedSessionState())
})
})

test('can use sane defaults', () => {
const session = new SessionEntity({ agentIdentifier, key, storage })
expect(session.state).toEqual(expect.objectContaining({
value: expect.any(String),
expiresAt: expect.any(Number),
inactiveAt: expect.any(Number),
updatedAt: expect.any(Number),
sessionReplayMode: expect.any(Number),
sessionReplaySentFirstChunk: expect.any(Boolean),
sessionTraceMode: expect.any(Number)
}))
expect(session.state).toEqual(expect.objectContaining(buildExpectedSessionState()))
})

test('expiresAt is the correct future timestamp - new session', () => {
Expand Down Expand Up @@ -110,47 +96,23 @@ describe('constructor', () => {
// missing required fields
const storage = new LocalMemory({ [`${PREFIX}_${key}`]: { invalid_fields: true } })
const session = new SessionEntity({ agentIdentifier, key, storage })
expect(session.state).toEqual(expect.objectContaining({
value: expect.any(String),
expiresAt: expect.any(Number),
inactiveAt: expect.any(Number),
updatedAt: expect.any(Number),
sessionReplayMode: expect.any(Number),
sessionReplaySentFirstChunk: expect.any(Boolean),
sessionTraceMode: expect.any(Number)
}))
expect(session.state).toEqual(expect.objectContaining(buildExpectedSessionState()))
})

test('expired expiresAt value in storage sets new defaults', () => {
const now = Date.now()
jest.setSystemTime(now)
const storage = new LocalMemory({ [`${PREFIX}_${key}`]: { value, expiresAt: now - 100, inactiveAt: Infinity } })
const session = new SessionEntity({ agentIdentifier, key, storage })
expect(session.state).toEqual(expect.objectContaining({
value: expect.any(String),
expiresAt: expect.any(Number),
inactiveAt: expect.any(Number),
updatedAt: expect.any(Number),
sessionReplayMode: expect.any(Number),
sessionReplaySentFirstChunk: expect.any(Boolean),
sessionTraceMode: expect.any(Number)
}))
expect(session.state).toEqual(expect.objectContaining(buildExpectedSessionState()))
})

test('expired inactiveAt value in storage sets new defaults', () => {
const now = Date.now()
jest.setSystemTime(now)
const storage = new LocalMemory({ [`${PREFIX}_${key}`]: { value, inactiveAt: now - 100, expiresAt: Infinity } })
const session = new SessionEntity({ agentIdentifier, key, storage })
expect(session.state).toEqual(expect.objectContaining({
value: expect.any(String),
expiresAt: expect.any(Number),
inactiveAt: expect.any(Number),
updatedAt: expect.any(Number),
sessionReplayMode: expect.any(Number),
sessionReplaySentFirstChunk: expect.any(Boolean),
sessionTraceMode: expect.any(Number)
}))
expect(session.state).toEqual(expect.objectContaining(buildExpectedSessionState()))
})
})

Expand Down Expand Up @@ -212,14 +174,7 @@ describe('read()', () => {
const newSession = new SessionEntity({ agentIdentifier, key, storage, expiresMs: 10 })
expect(newSession.isNew).toBeTruthy()

expect(newSession.read()).toEqual(expect.objectContaining({
value: expect.any(String),
expiresAt: expect.any(Number),
inactiveAt: expect.any(Number),
sessionReplayMode: expect.any(Number),
sessionReplaySentFirstChunk: expect.any(Boolean),
sessionTraceMode: expect.any(Number)
}))
expect(newSession.read()).toEqual(expect.objectContaining(buildExpectedSessionState()))
})

test('"pre-existing" sessions get data from read()', () => {
Expand Down
1 change: 1 addition & 0 deletions tests/components/session-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const model = {
sessionReplaySentFirstChunk: false,
sessionTraceMode: 0,
traceHarvestStarted: false,
loggingMode: 0,
serverTimeDiff: null,
custom: {}
}
6 changes: 5 additions & 1 deletion tests/specs/harvesting/disable-harvesting.e2e.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { testBlobTraceRequest, testErrorsRequest, testEventsRequest, testInsRequest, testLogsRequest, testMetricsRequest, testRumRequest } from '../../../tools/testing-server/utils/expect-tests'
import { rumFlags } from '../../../tools/testing-server/constants'
import { LOGGING_MODE } from '../../../src/features/logging/constants'

describe('disable harvesting', () => {
it('should disable harvesting metrics and errors when err entitlement is 0', async () => {
Expand Down Expand Up @@ -61,10 +62,13 @@ describe('disable harvesting', () => {
})

it('should disable harvesting console logs when log entitlement is 0', async () => {
// logging mode will only take effect on a fresh session
await browser.destroyAgentSession()

const logsCapture = await browser.testHandle.createNetworkCaptures('bamServer', { test: testLogsRequest })
await browser.testHandle.scheduleReply('bamServer', {
test: testRumRequest,
body: JSON.stringify(rumFlags({ log: 0 }))
body: JSON.stringify(rumFlags({ log: LOGGING_MODE.OFF }))
})

const [logsHarvests] = await Promise.all([
Expand Down
30 changes: 27 additions & 3 deletions tests/specs/harvesting/index.e2e.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { faker } from '@faker-js/faker'
import { testExpectedTrace } from '../util/helpers'
import { testAjaxEventsRequest, testAjaxTimeSlicesRequest, testBlobTraceRequest, testErrorsRequest, testInsRequest, testInteractionEventsRequest, testRumRequest, testTimingEventsRequest } from '../../../tools/testing-server/utils/expect-tests'
import { testAjaxEventsRequest, testAjaxTimeSlicesRequest, testBlobTraceRequest, testErrorsRequest, testInsRequest, testInteractionEventsRequest, testLogsRequest, testRumRequest, testTimingEventsRequest } from '../../../tools/testing-server/utils/expect-tests'

describe('harvesting', () => {
let rumCapture
Expand All @@ -11,17 +11,19 @@ describe('harvesting', () => {
let insightsCapture
let interactionEventsCapture
let errorMetricsCapture
let loggingEventsCapture

beforeEach(async () => {
[rumCapture, timingEventsCapture, ajaxEventsCapture, ajaxMetricsCapture, traceCapture, insightsCapture, interactionEventsCapture, errorMetricsCapture] = await browser.testHandle.createNetworkCaptures('bamServer', [
[rumCapture, timingEventsCapture, ajaxEventsCapture, ajaxMetricsCapture, traceCapture, insightsCapture, interactionEventsCapture, errorMetricsCapture, loggingEventsCapture] = await browser.testHandle.createNetworkCaptures('bamServer', [
{ test: testRumRequest },
{ test: testTimingEventsRequest },
{ test: testAjaxEventsRequest },
{ test: testAjaxTimeSlicesRequest },
{ test: testBlobTraceRequest },
{ test: testInsRequest },
{ test: testInteractionEventsRequest },
{ test: testErrorsRequest }
{ test: testErrorsRequest },
{ test: testLogsRequest }
])
})

Expand Down Expand Up @@ -315,6 +317,28 @@ describe('harvesting', () => {
interactionEventsHarvests.forEach(harvest => expect(harvest.request.query.s).toEqual('0'))
})

it('should not contain session data in the logs request when cookies_enabled is false', async () => {
const testURL = await browser.testHandle.assetURL('obfuscate-pii.html', {
init: {
privacy: {
cookies_enabled: false
}
}
})

const [
loggingEventsHarvests
] = await Promise.all([
loggingEventsCapture.waitForResult({ totalCount: 1 }),
browser.url(testURL)
.then(() => browser.waitForAgentLoad())
])

loggingEventsHarvests.forEach(harvest => {
expect(harvest.request.query.session).toBeUndefined()
})
})

it('should not harvest features when there is no data', async () => {
const [
insightsHarvests,
Expand Down
12 changes: 9 additions & 3 deletions tests/specs/logging/harvesting.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ describe('logging harvesting', () => {
})
})

afterEach(async () => {
// logging mode is sticky to the session, so we need to reset before the next test
await browser.destroyAgentSession()
})

const mockRumResponse = async (logLevel) => {
await browser.testHandle.scheduleReply('bamServer', {
test: testRumRequest,
body: JSON.stringify(rumFlags({ log: logLevel }))
})
}

const checkPayload = (actualPayload, expectedPayload) => {
expect(actualPayload).toContainAllKeys(['common', 'logs'])
expect(actualPayload.common).toEqual(expectedPayload.common)
Expand Down Expand Up @@ -70,8 +76,8 @@ describe('logging harvesting', () => {
it(`should harvest expected logs - ${type} pre load - logging mode: ${mode}`, async () => {
await mockRumResponse(logLevel)
const [[{ request: { body } }]] = await Promise.all([
logsCapture.waitForResult({ totalCount: 1 }),
browser.url(await browser.testHandle.assetURL(`logs-${type}-pre-load.html`))
logsCapture.waitForResult({ totalCount: 1, timeout: 15000 }),
await browser.url(await browser.testHandle.assetURL(`logs-${type}-pre-load.html`))
])

const actualPayload = JSON.parse(body)
Expand All @@ -81,7 +87,7 @@ describe('logging harvesting', () => {
it(`should harvest expected logs - ${type} post load - logging mode: ${mode}`, async () => {
await mockRumResponse(logLevel)
const [[{ request: { body } }]] = await Promise.all([
logsCapture.waitForResult({ totalCount: 1 }),
logsCapture.waitForResult({ totalCount: 1, timeout: 15000 }),
ptang-nr marked this conversation as resolved.
Show resolved Hide resolved
browser.url(await browser.testHandle.assetURL(`logs-${type}-post-load.html`))
])
const actualPayload = JSON.parse(body)
Expand Down
Loading
Loading