From 9141a45cdb3fffd2306fcc5388ed74142d167c53 Mon Sep 17 00:00:00 2001 From: Jordan Porter Date: Fri, 1 Sep 2023 11:28:49 -0600 Subject: [PATCH] feat: Add mode to enable agent to not harvest until user consent (#656) --- src/common/config/state/init.js | 17 +- src/common/config/state/init.test.js | 2 +- src/common/drain/drain.js | 2 +- src/common/util/feature-flags.js | 32 +- src/common/util/feature-flags.test.js | 4 - src/features/ajax/aggregate/index.js | 5 +- src/features/jserrors/aggregate/index.js | 3 +- src/features/metrics/aggregate/index.js | 3 +- src/features/page_action/aggregate/index.js | 21 +- .../page_view_event/aggregate/index.js | 3 +- .../page_view_timing/aggregate/index.js | 3 +- .../session_replay/aggregate/index.js | 3 +- src/features/session_trace/aggregate/index.js | 3 +- src/features/spa/aggregate/index.js | 3 +- src/features/utils/aggregate-base.js | 5 + src/features/utils/aggregate-base.test.js | 3 +- src/features/utils/instrument-base.js | 23 +- src/features/utils/instrument-base.test.js | 2 +- src/loaders/agent-base.js | 9 + src/loaders/agent.js | 4 +- src/loaders/api/api.js | 19 +- src/loaders/api/api.test.js | 4 +- src/loaders/micro-agent.js | 26 +- tests/assets/instrumented-manual.html | 34 ++ tests/assets/rrweb-record.html | 1 - .../session/config-race-condition.test.js | 2 +- tests/functional/spa/jsonp.test.js | 2 +- tests/functional/stn/ajax.test.js | 5 +- tests/specs/manual-loader.e2e.js | 402 ++++++++++++++++++ .../session-replay/rrweb-configuration.e2e.js | 22 + tools/testing-server/constants.js | 18 + .../plugins/agent-injector/init-transform.js | 5 +- .../testing-server/default-asset-query.mjs | 19 +- 33 files changed, 609 insertions(+), 100 deletions(-) create mode 100644 tests/assets/instrumented-manual.html create mode 100644 tests/specs/manual-loader.e2e.js diff --git a/src/common/config/state/init.js b/src/common/config/state/init.js index 198e9ba86..36bdb6178 100644 --- a/src/common/config/state/init.js +++ b/src/common/config/state/init.js @@ -9,7 +9,7 @@ const model = () => { } return { privacy: { cookies_enabled: true }, // *cli - per discussion, default should be true - ajax: { deny_list: undefined, block_internal: true, enabled: true, harvestTimeSeconds: 10 }, + ajax: { deny_list: undefined, block_internal: true, enabled: true, harvestTimeSeconds: 10, autoStart: true }, distributed_tracing: { enabled: undefined, exclude_newrelic_header: undefined, @@ -24,15 +24,16 @@ const model = () => { }, ssl: undefined, obfuscate: undefined, - jserrors: { enabled: true, harvestTimeSeconds: 10 }, - metrics: { enabled: true }, - page_action: { enabled: true, harvestTimeSeconds: 30 }, - page_view_event: { enabled: true }, - page_view_timing: { enabled: true, harvestTimeSeconds: 30, long_task: false }, - session_trace: { enabled: true, harvestTimeSeconds: 10 }, + jserrors: { enabled: true, harvestTimeSeconds: 10, autoStart: true }, + metrics: { enabled: true, autoStart: true }, + page_action: { enabled: true, harvestTimeSeconds: 30, autoStart: true }, + page_view_event: { enabled: true, autoStart: true }, + page_view_timing: { enabled: true, harvestTimeSeconds: 30, long_task: false, autoStart: true }, + session_trace: { enabled: true, harvestTimeSeconds: 10, autoStart: true }, harvest: { tooManyRequestsDelay: 60 }, session_replay: { // feature settings + autoStart: true, enabled: false, harvestTimeSeconds: 60, sampleRate: 0.1, @@ -60,7 +61,7 @@ const model = () => { hiddenState.maskInputOptions = { ...val, password: true } } }, - spa: { enabled: true, harvestTimeSeconds: 10 } + spa: { enabled: true, harvestTimeSeconds: 10, autoStart: true } } } diff --git a/src/common/config/state/init.test.js b/src/common/config/state/init.test.js index 55913628f..c2ce3d2f1 100644 --- a/src/common/config/state/init.test.js +++ b/src/common/config/state/init.test.js @@ -23,6 +23,6 @@ test('set/getConfiguration works correctly', () => { test('getConfigurationValue parses path correctly', () => { setConfiguration('ab', { page_action: { harvestTimeSeconds: 1000 } }) expect(getConfigurationValue('ab', '')).toBeUndefined() - expect(getConfigurationValue('ab', 'page_action')).toEqual({ enabled: true, harvestTimeSeconds: 1000 }) + expect(getConfigurationValue('ab', 'page_action')).toEqual({ enabled: true, harvestTimeSeconds: 1000, autoStart: true }) expect(getConfigurationValue('ab', 'page_action.harvestTimeSeconds')).toEqual(1000) }) diff --git a/src/common/drain/drain.js b/src/common/drain/drain.js index 07f3ffb7c..206dfb51a 100644 --- a/src/common/drain/drain.js +++ b/src/common/drain/drain.js @@ -44,7 +44,6 @@ function curateRegistry (agentIdentifier) { */ export function drain (agentIdentifier = '', featureName = 'feature') { curateRegistry(agentIdentifier) - // If the feature for the specified agent is not in the registry, that means the instrument file was bypassed. // This could happen in tests, or loaders that directly import the aggregator. In these cases it is safe to // drain the feature group immediately rather than waiting to drain all at once. @@ -59,6 +58,7 @@ export function drain (agentIdentifier = '', featureName = 'feature') { if (items.every(([key, values]) => values.staged)) { items.sort((a, b) => a[1].priority - b[1].priority) items.forEach(([group]) => { + registry[agentIdentifier].delete(group) drainGroup(group) }) } diff --git a/src/common/util/feature-flags.js b/src/common/util/feature-flags.js index c89858612..1ca547e67 100644 --- a/src/common/util/feature-flags.js +++ b/src/common/util/feature-flags.js @@ -4,7 +4,6 @@ */ import { ee } from '../event-emitter/contextual-ee' import { handle } from '../event-emitter/handle' -import { drain } from '../drain/drain' import { FEATURE_NAMES } from '../../loaders/features/features' const bucketMap = { @@ -15,26 +14,25 @@ const bucketMap = { sr: [FEATURE_NAMES.sessionReplay, FEATURE_NAMES.sessionTrace] } +const sentIds = new Set() + /** Note that this function only processes each unique flag ONCE, with the first occurrence of each flag and numeric value determining its switch on/off setting. */ export function activateFeatures (flags, agentIdentifier) { const sharedEE = ee.get(agentIdentifier) if (!(flags && typeof flags === 'object')) return - Object.entries(flags).forEach(([flag, num]) => { - if (activatedFeatures[flag] !== undefined) return - - if (bucketMap[flag]) { - bucketMap[flag].forEach(feat => { - if (!num) handle('block-' + flag, [], undefined, feat, sharedEE) - else handle('feat-' + flag, [], undefined, feat, sharedEE) - - handle('rumresp-' + flag, [Boolean(num)], undefined, feat, sharedEE) // this is a duplicate of feat-/block- but makes awaiting for 1 event easier than 2 - }) - } else if (num) handle('feat-' + flag, [], undefined, undefined, sharedEE) // not sure what other flags are overlooked, but there's a test for ones not in the map -- - // ^^^ THIS DOESN'T ACTUALLY DO ANYTHHING AS UNDEFINED/FEATURE GROUP ISN'T DRAINED - - activatedFeatures[flag] = Boolean(num) - }) + if (!sentIds.has(agentIdentifier)) { + Object.entries(flags).forEach(([flag, num]) => { + if (bucketMap[flag]) { + bucketMap[flag].forEach(feat => { + if (!num) handle('block-' + flag, [], undefined, feat, sharedEE) + else handle('feat-' + flag, [], undefined, feat, sharedEE) + handle('rumresp-' + flag, [Boolean(num)], undefined, feat, sharedEE) // this is a duplicate of feat-/block- but makes awaiting for 1 event easier than 2 + }) + } else if (num) handle('feat-' + flag, [], undefined, undefined, sharedEE) // not sure what other flags are overlooked, but there's a test for ones not in the map -- + activatedFeatures[flag] = Boolean(num) + }) + } // Let the features waiting on their respective flags know that RUM response was received and that any missing flags are interpreted as bad entitlement / "off". // Hence, those features will not be hanging forever if their flags aren't included in the response. @@ -44,7 +42,7 @@ export function activateFeatures (flags, agentIdentifier) { activatedFeatures[flag] = false } }) - drain(agentIdentifier, FEATURE_NAMES.pageViewEvent) + sentIds.add(agentIdentifier) } export const activatedFeatures = {} diff --git a/src/common/util/feature-flags.test.js b/src/common/util/feature-flags.test.js index e0e9af011..eb3975d03 100644 --- a/src/common/util/feature-flags.test.js +++ b/src/common/util/feature-flags.test.js @@ -57,7 +57,6 @@ test('emits the right events when feature flag = 1', () => { expect(handleModule.handle).toHaveBeenCalledTimes(14) expect(handleModule.handle).toHaveBeenNthCalledWith(1, 'feat-stn', [], undefined, FEATURE_NAMES.sessionTrace, sharedEE) expect(handleModule.handle).toHaveBeenLastCalledWith('rumresp-sr', [true], undefined, FEATURE_NAMES.sessionTrace, sharedEE) - expect(drainModule.drain).toHaveBeenCalledWith(agentIdentifier, 'page_view_event') Object.keys(flags).forEach(flag => { flags[flag] = true }) expect(activatedFeatures).toEqual(flags) @@ -74,7 +73,6 @@ test('emits the right events when feature flag = 0', () => { expect(handleModule.handle).toHaveBeenCalledTimes(14) expect(handleModule.handle).toHaveBeenNthCalledWith(1, 'block-stn', [], undefined, FEATURE_NAMES.sessionTrace, sharedEE) expect(handleModule.handle).toHaveBeenLastCalledWith('rumresp-sr', [false], undefined, FEATURE_NAMES.sessionTrace, sharedEE) - expect(drainModule.drain).toHaveBeenCalledWith(agentIdentifier, 'page_view_event') Object.keys(flags).forEach(flag => { flags[flag] = false }) expect(activatedFeatures).toEqual(flags) @@ -92,7 +90,5 @@ test('only the first activate of the same feature is respected', () => { expect(handleModule.handle).toHaveBeenNthCalledWith(1, 'feat-stn', [], undefined, 'session_trace', sharedEE1) expect(handleModule.handle).toHaveBeenNthCalledWith(2, 'rumresp-stn', [true], undefined, 'session_trace', sharedEE1) expect(handleModule.handle).not.toHaveBeenNthCalledWith(1, 'feat-stn', [], undefined, 'session_trace', sharedEE2) - expect(drainModule.drain).toHaveBeenCalledWith(agentIdentifier, 'page_view_event') - expect(drainModule.drain).toHaveBeenCalledTimes(2) expect(activatedFeatures.stn).toBeTruthy() }) diff --git a/src/features/ajax/aggregate/index.js b/src/features/ajax/aggregate/index.js index 72ffe14e4..fc087d5f0 100644 --- a/src/features/ajax/aggregate/index.js +++ b/src/features/ajax/aggregate/index.js @@ -10,7 +10,6 @@ import { getConfiguration, getInfo, getRuntime } from '../../../common/config/co import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler' import { setDenyList, shouldCollectEvent } from '../../../common/deny-list/deny-list' import { FEATURE_NAME } from '../constants' -import { drain } from '../../../common/drain/drain' import { FEATURE_NAMES } from '../../../loaders/features/features' import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants' import { AggregateBase } from '../../utils/aggregate-base' @@ -24,7 +23,7 @@ export class Aggregate extends AggregateBase { register('xhr', storeXhr, this.featureName, this.ee) if (!allAjaxIsEnabled) { - drain(this.agentIdentifier, this.featureName) + this.drain() return // feature will only collect timeslice metrics & ajax trace nodes if it's not fully enabled } @@ -66,7 +65,7 @@ export class Aggregate extends AggregateBase { ee.on(`drain-${this.featureName}`, () => { scheduler.startTimer(harvestTimeSeconds) }) - drain(this.agentIdentifier, this.featureName) + this.drain() function storeXhr (params, metrics, startTime, endTime, type) { metrics.time = startTime diff --git a/src/features/jserrors/aggregate/index.js b/src/features/jserrors/aggregate/index.js index c06a79f9a..c4330fe90 100644 --- a/src/features/jserrors/aggregate/index.js +++ b/src/features/jserrors/aggregate/index.js @@ -18,7 +18,6 @@ import { now } from '../../../common/timing/now' import { globalScope } from '../../../common/constants/runtime' import { FEATURE_NAME } from '../constants' -import { drain } from '../../../common/drain/drain' import { FEATURE_NAMES } from '../../../loaders/features/features' import { AggregateBase } from '../../utils/aggregate-base' @@ -63,7 +62,7 @@ export class Aggregate extends AggregateBase { scheduler.stopTimer(true) }, this.featureName, this.ee) - drain(this.agentIdentifier, this.featureName) + this.drain() } onHarvestStarted (options) { diff --git a/src/features/metrics/aggregate/index.js b/src/features/metrics/aggregate/index.js index 8cc5492ea..ddb2169a5 100644 --- a/src/features/metrics/aggregate/index.js +++ b/src/features/metrics/aggregate/index.js @@ -2,7 +2,6 @@ import { getRuntime } from '../../../common/config/config' import { registerHandler } from '../../../common/event-emitter/register-handler' import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler' import { FEATURE_NAME, SUPPORTABILITY_METRIC, CUSTOM_METRIC, SUPPORTABILITY_METRIC_CHANNEL, CUSTOM_METRIC_CHANNEL } from '../constants' -import { drain } from '../../../common/drain/drain' import { getFrameworks } from './framework-detection' import { isFileProtocol } from '../../../common/url/protocol' import { getRules, validateRules } from '../../../common/util/obfuscate' @@ -34,7 +33,7 @@ export class Aggregate extends AggregateBase { scheduler = new HarvestScheduler('jserrors', { onUnload: () => this.unload() }, this) scheduler.harvest.on('jserrors', () => ({ body: this.aggregator.take(['cm', 'sm']) })) - drain(this.agentIdentifier, this.featureName) // regardless if this is blocked or not, drain is needed to unblock other features from harvesting (counteract registerDrain) + this.drain() } storeSupportabilityMetrics (name, value) { diff --git a/src/features/page_action/aggregate/index.js b/src/features/page_action/aggregate/index.js index 5443a8b67..cd13e29ad 100644 --- a/src/features/page_action/aggregate/index.js +++ b/src/features/page_action/aggregate/index.js @@ -10,7 +10,6 @@ import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler' import { cleanURL } from '../../../common/url/clean-url' import { getConfigurationValue, getInfo, getRuntime } from '../../../common/config/config' import { FEATURE_NAME } from '../constants' -import { drain } from '../../../common/drain/drain' import { isBrowserScope } from '../../../common/constants/runtime' import { AggregateBase } from '../../utils/aggregate-base' @@ -32,17 +31,17 @@ export class Aggregate extends AggregateBase { register('api-addPageAction', (...args) => this.addPageAction(...args), this.featureName, this.ee) - var scheduler = new HarvestScheduler('ins', { onFinished: (...args) => this.onHarvestFinished(...args) }, this) - scheduler.harvest.on('ins', (...args) => this.onHarvestStarted(...args)) - this.ee.on(`drain-${this.featureName}`, () => { if (!this.blocked) scheduler.startTimer(this.harvestTimeSeconds, 0) }) - - // if rum response determines that customer lacks entitlements for ins endpoint, block it - register('block-ins', () => { - this.blocked = true - scheduler.stopTimer(true) - }, this.featureName, this.ee) + this.waitForFlags(['ins']).then(([enabled]) => { + if (enabled) { + const scheduler = new HarvestScheduler('ins', { onFinished: (...args) => this.onHarvestFinished(...args) }, this) + scheduler.harvest.on('ins', (...args) => this.onHarvestStarted(...args)) + scheduler.startTimer(this.harvestTimeSeconds, 0) + } else { + this.blocked = true + } + }) - drain(this.agentIdentifier, this.featureName) + this.drain() } onHarvestStarted (options) { diff --git a/src/features/page_view_event/aggregate/index.js b/src/features/page_view_event/aggregate/index.js index fa479ed0f..0cc5abbaf 100644 --- a/src/features/page_view_event/aggregate/index.js +++ b/src/features/page_view_event/aggregate/index.js @@ -9,7 +9,6 @@ import { getConfigurationValue, getInfo, getRuntime } from '../../../common/conf import { Harvest } from '../../../common/harvest/harvest' import * as CONSTANTS from '../constants' import { getActivatedFeaturesFlags } from './initialized-features' -import { drain } from '../../../common/drain/drain' import { activateFeatures } from '../../../common/util/feature-flags' import { warn } from '../../../common/util/console' import { AggregateBase } from '../../utils/aggregate-base' @@ -131,7 +130,7 @@ export class Aggregate extends AggregateBase { try { activateFeatures(JSON.parse(responseText), this.agentIdentifier) - drain(this.agentIdentifier, this.featureName) + this.drain() } catch (err) { this.ee.abort() warn('RUM call failed. Agent shutting down.') diff --git a/src/features/page_view_timing/aggregate/index.js b/src/features/page_view_timing/aggregate/index.js index 89f25f721..9944ee9db 100644 --- a/src/features/page_view_timing/aggregate/index.js +++ b/src/features/page_view_timing/aggregate/index.js @@ -15,7 +15,6 @@ import { cleanURL } from '../../../common/url/clean-url' import { handle } from '../../../common/event-emitter/handle' import { getInfo, getConfigurationValue, getRuntime } from '../../../common/config/config' import { FEATURE_NAME } from '../constants' -import { drain } from '../../../common/drain/drain' import { FEATURE_NAMES } from '../../../loaders/features/features' import { AggregateBase } from '../../utils/aggregate-base' @@ -128,7 +127,7 @@ export class Aggregate extends AggregateBase { // send initial data sooner, then start regular this.ee.on(`drain-${this.featureName}`, () => { this.scheduler.startTimer(harvestTimeSeconds, initialHarvestSeconds) }) - drain(this.agentIdentifier, this.featureName) + this.drain() } // takes an attributes object and appends connection attributes if available diff --git a/src/features/session_replay/aggregate/index.js b/src/features/session_replay/aggregate/index.js index d6db8a040..294f6c626 100644 --- a/src/features/session_replay/aggregate/index.js +++ b/src/features/session_replay/aggregate/index.js @@ -10,7 +10,6 @@ * functionality is validated and a full user experience is curated. */ -import { drain } from '../../../common/drain/drain' import { registerHandler } from '../../../common/event-emitter/register-handler' import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler' import { FEATURE_NAME } from '../constants' @@ -128,7 +127,7 @@ export class Aggregate extends AggregateBase { Math.random() < getConfigurationValue(this.agentIdentifier, 'session_replay.sampleRate') )).then(() => sharedChannel.onReplayReady(this.mode)) // notify watchers that replay started with the mode - drain(this.agentIdentifier, this.featureName) + this.drain() } } diff --git a/src/features/session_trace/aggregate/index.js b/src/features/session_trace/aggregate/index.js index 9ac5056de..225ca5241 100644 --- a/src/features/session_trace/aggregate/index.js +++ b/src/features/session_trace/aggregate/index.js @@ -8,7 +8,6 @@ import { parseUrl } from '../../../common/url/parse-url' import { getConfigurationValue, getRuntime } from '../../../common/config/config' import { now } from '../../../common/timing/now' import { FEATURE_NAME } from '../constants' -import { drain } from '../../../common/drain/drain' import { HandlerCache } from '../../utils/handler-cache' import { MODE, SESSION_EVENTS } from '../../../common/session/session-entity' import { getSessionReplayMode } from '../../session_replay/replay-mode' @@ -156,7 +155,7 @@ export class Aggregate extends AggregateBase { registerHandler('bstApi', (...args) => operationalGate.settle(() => this.storeSTN(...args)), this.featureName, this.ee) registerHandler('errorAgg', (...args) => operationalGate.settle(() => this.storeErrorAgg(...args)), this.featureName, this.ee) registerHandler('pvtAdded', (...args) => operationalGate.settle(() => this.processPVT(...args)), this.featureName, this.ee) - drain(this.agentIdentifier, this.featureName) + this.drain() } startTracing (startupBuffer, dontStartHarvestYet = false) { diff --git a/src/features/spa/aggregate/index.js b/src/features/spa/aggregate/index.js index 4d926736f..4268c9e99 100644 --- a/src/features/spa/aggregate/index.js +++ b/src/features/spa/aggregate/index.js @@ -17,7 +17,6 @@ import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler' import { Serializer } from './serializer' import { ee } from '../../../common/event-emitter/contextual-ee' import * as CONSTANTS from '../constants' -import { drain } from '../../../common/drain/drain' import { FEATURE_NAMES } from '../../../loaders/features/features' import { AggregateBase } from '../../utils/aggregate-base' @@ -728,6 +727,6 @@ export class Aggregate extends AggregateBase { return enabled !== false } - drain(this.agentIdentifier, this.featureName) + this.drain() } } diff --git a/src/features/utils/aggregate-base.js b/src/features/utils/aggregate-base.js index 0b1eb3f2b..c978b2c21 100644 --- a/src/features/utils/aggregate-base.js +++ b/src/features/utils/aggregate-base.js @@ -3,6 +3,7 @@ import { FeatureBase } from './feature-base' import { getInfo, isConfigured, getRuntime } from '../../common/config/config' import { configure } from '../../loaders/configure/configure' import { gosCDN } from '../../common/window/nreum' +import { drain } from '../../common/drain/drain' export class AggregateBase extends FeatureBase { constructor (...args) { @@ -25,6 +26,10 @@ export class AggregateBase extends FeatureBase { ) } + drain () { + drain(this.agentIdentifier, this.featureName) + } + /** * Checks for additional `jsAttributes` items to support backward compatibility with implementations of the agent where * loader configurations may appear after the loader code is executed. diff --git a/src/features/utils/aggregate-base.test.js b/src/features/utils/aggregate-base.test.js index 732f3de38..88f127924 100644 --- a/src/features/utils/aggregate-base.test.js +++ b/src/features/utils/aggregate-base.test.js @@ -31,7 +31,8 @@ jest.mock('../../loaders/configure/configure', () => ({ })) jest.mock('../../common/window/nreum', () => ({ __esModule: true, - gosCDN: jest.fn().mockReturnValue({}) + gosCDN: jest.fn().mockReturnValue({}), + gosNREUM: jest.fn().mockReturnValue({}) })) let agentIdentifier diff --git a/src/features/utils/instrument-base.js b/src/features/utils/instrument-base.js index f58fbb526..d5df78388 100644 --- a/src/features/utils/instrument-base.js +++ b/src/features/utils/instrument-base.js @@ -45,7 +45,10 @@ export class InstrumentBase extends FeatureBase { */ this.onAggregateImported = undefined - if (auto) registerDrain(agentIdentifier, featureName) + /** used in conjunction with newrelic.start() to defer harvesting in features */ + if (getConfigurationValue(this.agentIdentifier, `${this.featureName}.autoStart`) === false) this.auto = false + /** if the feature requires opt-in (!auto-start), it will get registered once the api has been called */ + if (this.auto) registerDrain(agentIdentifier, featureName) } /** @@ -55,7 +58,21 @@ export class InstrumentBase extends FeatureBase { * @returns void */ importAggregator (argsObjFromInstrument = {}) { - if (this.featAggregate || !this.auto) return + if (this.featAggregate) return + + if (!this.auto) { + // this feature requires an opt in... + // wait for API to be called + this.ee.on(`${this.featureName}-opt-in`, () => { + // register the feature to drain only once the API has been called, it will drain when importAggregator finishes for all the features + // called by the api in that cycle + registerDrain(this.agentIdentifier, this.featureName) + this.auto = true + this.importAggregator() + }) + return + } + const enableSessionTracking = isBrowserScope && getConfigurationValue(this.agentIdentifier, 'privacy.cookies_enabled') === true let loadedSuccessfully this.onAggregateImported = new Promise(resolve => { @@ -74,7 +91,7 @@ export class InstrumentBase extends FeatureBase { } /** - * Note this try-catch differs from the one in Agent.start() in that it's placed later in a page's lifecycle and + * Note this try-catch differs from the one in Agent.run() in that it's placed later in a page's lifecycle and * it's only responsible for aborting its one specific feature, rather than all. */ try { diff --git a/src/features/utils/instrument-base.test.js b/src/features/utils/instrument-base.test.js index b598d047f..4267cf12f 100644 --- a/src/features/utils/instrument-base.test.js +++ b/src/features/utils/instrument-base.test.js @@ -33,7 +33,7 @@ jest.mock('../../common/config/config', () => ({ })) jest.mock('../../common/config/config', () => ({ __esModule: true, - getConfigurationValue: jest.fn(), + getConfigurationValue: jest.fn().mockReturnValue({}), originals: { MO: jest.fn() } diff --git a/src/loaders/agent-base.js b/src/loaders/agent-base.js index 5dabe9095..69435a349 100644 --- a/src/loaders/agent-base.js +++ b/src/loaders/agent-base.js @@ -92,4 +92,13 @@ export class AgentBase { addRelease (name, id) { warn('Call to agent api addRelease failed. The agent is not currently initialized.') } + + /** + * Starts a set of agent features if not running in "autoStart" mode + * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/start/} + * @param {string|string[]|undefined} name The feature name(s) to start. If no name(s) are passed, all features will be started + */ + start (featureNames) { + warn('Call to agent api addRelease failed. The agent is not currently initialized.') + } } diff --git a/src/loaders/agent.js b/src/loaders/agent.js index fcbe9273f..6ebf243e9 100644 --- a/src/loaders/agent.js +++ b/src/loaders/agent.js @@ -46,7 +46,7 @@ export class Agent extends AgentBase { Object.assign(this, configure(this.agentIdentifier, options, options.loaderType || 'agent')) - this.start() + this.run() } get config () { @@ -58,7 +58,7 @@ export class Agent extends AgentBase { } } - start () { + run () { const NR_FEATURES_REF_NAME = 'features' // Attempt to initialize all the requested features (sequentially in prio order & synchronously), with any failure aborting the whole process. try { diff --git a/src/loaders/api/api.js b/src/loaders/api/api.js index 12a717305..ab59ac289 100644 --- a/src/loaders/api/api.js +++ b/src/loaders/api/api.js @@ -21,7 +21,7 @@ export function setTopLevelCallers () { const funcs = [ 'setErrorHandler', 'finished', 'addToTrace', 'inlineHit', 'addRelease', 'addPageAction', 'setCurrentRouteName', 'setPageViewName', 'setCustomAttribute', - 'interaction', 'noticeError', 'setUserId', 'setApplicationVersion' + 'interaction', 'noticeError', 'setUserId', 'setApplicationVersion', 'start' ] funcs.forEach(f => { nr[f] = (...args) => caller(f, ...args) @@ -122,6 +122,23 @@ export function setAPI (agentIdentifier, forceDrain) { return appendJsAttribute('application.version', value, 'setApplicationVersion', false) } + apiInterface.start = (features) => { + try { + const featNames = Object.values(FEATURE_NAMES) + if (features === undefined) features = featNames + else { + features = Array.isArray(features) && features.length ? features : [features] + if (features.some(f => !featNames.includes(f))) return warn(`Invalid feature name supplied. Acceptable feature names are: ${featNames}`) + if (!features.includes(FEATURE_NAMES.pageViewEvent)) features.push(FEATURE_NAMES.pageViewEvent) + } + features.forEach(feature => { + instanceEE.emit(`${feature}-opt-in`) + }) + } catch (err) { + warn('An unexpected issue occurred', err) + } + } + apiInterface.interaction = function () { return new InteractionHandle().get() } diff --git a/src/loaders/api/api.test.js b/src/loaders/api/api.test.js index 3daf832ed..7533d743a 100644 --- a/src/loaders/api/api.test.js +++ b/src/loaders/api/api.test.js @@ -12,7 +12,7 @@ describe('setTopLevelCallers', () => { test('adds all api methods', () => { setTopLevelCallers() - expect(Object.keys(gosCDN()).length).toEqual(13) + expect(Object.keys(gosCDN()).length).toEqual(14) }) test('and runs the corresponding fn under every exposed agent', () => { @@ -56,7 +56,7 @@ describe('setAPI', () => { test('also adds all api methods', () => { let apiI = setAPI('abcd', true) - expect(Object.keys(apiI).length).toEqual(13) + expect(Object.keys(apiI).length).toEqual(14) for (const k of Object.keys(apiI)) { expect(apiI[k]).toBeInstanceOf(Function) } }) diff --git a/src/loaders/micro-agent.js b/src/loaders/micro-agent.js index a45032bd4..c036b36bb 100644 --- a/src/loaders/micro-agent.js +++ b/src/loaders/micro-agent.js @@ -6,7 +6,7 @@ import { configure } from './configure/configure' import { Aggregator } from '../common/aggregate/aggregator' import { gosNREUMInitializedAgents } from '../common/window/nreum' import { generateRandomHexString } from '../common/ids/unique-id' -import { getConfiguration, getInfo, getLoaderConfig, getRuntime } from '../common/config/config' +import { getConfiguration, getConfigurationValue, getInfo, getLoaderConfig, getRuntime } from '../common/config/config' import { FEATURE_NAMES } from './features/features' import { warn } from '../common/util/console' import { onWindowLoad } from '../common/window/load' @@ -37,7 +37,13 @@ export class MicroAgent extends AgentBase { Object.assign(this, configure(this.agentIdentifier, { ...options, runtime: { isolatedBacklog: true } }, options.loaderType || 'micro-agent')) - this.start() + /** + * Starts a set of agent features if not running in "autoStart" mode + * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/start/} + * @param {string|string[]|undefined} name The feature name(s) to start. If no name(s) are passed, all features will be started + */ + this.start = features => this.run(features) + this.run(nonAutoFeatures.filter(featureName => getConfigurationValue(agentIdentifier, `${featureName}.autoStart`))) } get config () { @@ -49,7 +55,19 @@ export class MicroAgent extends AgentBase { } } - start () { + run (features) { + try { + const featNames = nonAutoFeatures + if (features === undefined) features = featNames + else { + features = Array.isArray(features) && features.length ? features : [features] + if (features.some(f => !featNames.includes(f))) return warn(`Invalid feature name supplied. Acceptable feature names are: ${featNames}`) + if (!features.includes(FEATURE_NAMES.pageViewEvent)) features.push(FEATURE_NAMES.pageViewEvent) + } + } catch (err) { + warn('An unexpected issue occurred', err) + } + try { const enabledFeatures = getEnabledFeatures(this.agentIdentifier) @@ -63,7 +81,7 @@ export class MicroAgent extends AgentBase { onWindowLoad(() => { // these features do not import an "instrument" file, meaning they are only hooked up to the API. nonAutoFeatures.forEach(f => { - if (enabledFeatures[f]) { + if (enabledFeatures[f] && features.includes(f)) { import(/* webpackChunkName: "lazy-feature-loader" */ '../features/utils/lazy-feature-loader').then(({ lazyFeatureLoader }) => { return lazyFeatureLoader(f, 'aggregate') }).then(({ Aggregate }) => { diff --git a/tests/assets/instrumented-manual.html b/tests/assets/instrumented-manual.html new file mode 100644 index 000000000..54f165391 --- /dev/null +++ b/tests/assets/instrumented-manual.html @@ -0,0 +1,34 @@ + + + + + RUM Unit Test + {init} {config} + + {loader} {script-injection} + + + Instrumented + diff --git a/tests/assets/rrweb-record.html b/tests/assets/rrweb-record.html index ac9093eb6..6538e7579 100644 --- a/tests/assets/rrweb-record.html +++ b/tests/assets/rrweb-record.html @@ -18,7 +18,6 @@ {config} {loader} diff --git a/tests/functional/session/config-race-condition.test.js b/tests/functional/session/config-race-condition.test.js index af48b00af..c082a0e64 100644 --- a/tests/functional/session/config-race-condition.test.js +++ b/tests/functional/session/config-race-condition.test.js @@ -3,7 +3,7 @@ const testDriver = require('../../../tools/jil/index') let notSafariWithSeleniumBug = testDriver.Matcher.withFeature('notSafariWithSeleniumBug') testDriver.test(`Session object exists when config is set after loader`, notSafariWithSeleniumBug, function (t, browser, router) { - let url = router.assetURL('custom-attribute-race-condition.html', {init: {jserrors: {enabled: true, harvestTimeSeconds: 5}}}) + let url = router.assetURL('custom-attribute-race-condition.html', {init: {jserrors: {enabled: true, harvestTimeSeconds: 5}, privacy: {cookies_enabled: true}}}) let loadPromise = browser.get(url) let rumPromise = router.expectRum() diff --git a/tests/functional/spa/jsonp.test.js b/tests/functional/spa/jsonp.test.js index a5bc32ffa..32479e518 100644 --- a/tests/functional/spa/jsonp.test.js +++ b/tests/functional/spa/jsonp.test.js @@ -236,7 +236,7 @@ function waitForPageLoad (browser, router, urlPath) { function clickPageAndWaitForEvents (browser, router) { return Promise.all([ - router.expectEvents(), + router.expectInteractionEvents(), browser.elementByCssSelector('body').click() ]).then(([eventData, domData]) => { return eventData diff --git a/tests/functional/stn/ajax.test.js b/tests/functional/stn/ajax.test.js index 40a2b1030..c9c44801d 100644 --- a/tests/functional/stn/ajax.test.js +++ b/tests/functional/stn/ajax.test.js @@ -77,11 +77,8 @@ testDriver.test('session trace ajax deny list', supported, function (t, browser, let loadPromise = browser.safeGet(assetURL).waitForFeature('loaded') let rumPromise = router.expectRum() let resourcePromise = router.expectResources() - const ajaxPromise = router.expectAjaxTimeSlices(8000).then(() => { - t.fail('Should not have seen the ajax event') - }).catch(() => {}) - Promise.all([resourcePromise, ajaxPromise, loadPromise, rumPromise]).then(([result]) => { + Promise.all([resourcePromise, loadPromise, rumPromise]).then(([result]) => { t.equal(result.reply.statusCode, 200, 'server responded with 200') // trigger an XHR call after diff --git a/tests/specs/manual-loader.e2e.js b/tests/specs/manual-loader.e2e.js new file mode 100644 index 000000000..d03de5055 --- /dev/null +++ b/tests/specs/manual-loader.e2e.js @@ -0,0 +1,402 @@ +describe('Manual Loader', () => { + describe('invalid params do not initialize data', () => { + it('wrong string', async () => { + await browser.url(await browser.testHandle.assetURL('instrumented-manual.html')) // Setup expects before loading the page + + const rum = await browser.testHandle.expectRum(5000, true) + expect(rum).toEqual(undefined) + + Promise.all([ + browser.testHandle.expectRum(10000, true), + browser.testHandle.expectTimings(10000, true), + browser.testHandle.expectAjaxEvents(10000, true), + browser.testHandle.expectErrors(10000, true), + browser.testHandle.expectMetrics(10000, true), + browser.testHandle.expectIns(10000, true), + browser.testHandle.expectResources(10000, true), + browser.testHandle.expectInteractionEvents(10000, true), + browser.execute(function () { + newrelic.start('INVALID') + setTimeout(function () { + window.location.reload() + }, 1000) + }) + ]).then(expect(1).toEqual(1)).catch(() => expect(1).toEqual(2)) + }) + + it('wrong type', async () => { + await browser.url(await browser.testHandle.assetURL('instrumented-manual.html')) // Setup expects before loading the page + + const rum = await browser.testHandle.expectRum(5000, true) + expect(rum).toEqual(undefined) + + Promise.all([ + browser.testHandle.expectRum(10000, true), + browser.testHandle.expectTimings(10000, true), + browser.testHandle.expectAjaxEvents(10000, true), + browser.testHandle.expectErrors(10000, true), + browser.testHandle.expectMetrics(10000, true), + browser.testHandle.expectIns(10000, true), + browser.testHandle.expectResources(10000, true), + browser.testHandle.expectInteractionEvents(10000, true), + browser.execute(function () { + newrelic.start(1) + setTimeout(function () { + window.location.reload() + }, 1000) + }) + ]).then(expect(1).toEqual(1)).catch(() => expect(1).toEqual(2)) + }) + }) + + describe('all at once', () => { + it('empty params initializes all features', async () => { + await browser.url(await browser.testHandle.assetURL('instrumented-manual.html')) // Setup expects before loading the page + + const rum = await browser.testHandle.expectRum(5000, true) + expect(rum).toEqual(undefined) + + const [rum2, pvt, ajax, jserrors, metrics, pa, st, spa] = await Promise.all([ + browser.testHandle.expectRum(), + browser.testHandle.expectTimings(), + browser.testHandle.expectAjaxEvents(), + browser.testHandle.expectErrors(), + browser.testHandle.expectMetrics(), + browser.testHandle.expectIns(), + browser.testHandle.expectResources(), + browser.testHandle.expectInteractionEvents(), + browser.execute(function () { + newrelic.start() + setTimeout(function () { + window.location.reload() + }, 1000) + }) + ]) + + checkRum(rum2.request) + checkPVT(pvt.request) + checkAjax(ajax.request) + checkJsErrors(jserrors.request) + checkMetrics(metrics.request) + checkPageAction(pa.request) + checkSessionTrace(st.request) + checkSpa(spa.request) + }) + }) + + describe('partial implementations', () => { + it('works if config supplied is incomplete', async () => { + const [rum, pvt, ajax, jserrors, st, spa] = await Promise.all([ + browser.testHandle.expectRum(), + browser.testHandle.expectTimings(), + browser.testHandle.expectAjaxEvents(10000, true), + browser.testHandle.expectErrors(10000, true), + browser.testHandle.expectResources(), + browser.testHandle.expectInteractionEvents(), + browser.url(await browser.testHandle.assetURL('instrumented.html', { + init: { + ajax: { + block_internal: false, + autoStart: false + }, + jserrors: { + autoStart: false + } + } + })).then(() => browser.execute(function () { + setTimeout(function () { + var xhr = new XMLHttpRequest() + xhr.open('GET', '/json') + xhr.send() + newrelic.noticeError('test') + }, 1000) + })) + ]) + + await browser.pause(2000) + checkRum(rum.request) + checkPVT(pvt.request) + checkSessionTrace(st.request) + checkSpa(spa.request) + + expect(ajax).toEqual(undefined) + expect(jserrors).toEqual(undefined) + + await browser.pause(1000) + const [ajax2, jserrors2] = await Promise.all([ + browser.testHandle.expectAjaxEvents(), + browser.testHandle.expectErrors(), + browser.execute(function () { + newrelic.start() + }) + ]) + + checkAjax(ajax2.request) + checkJsErrors(jserrors2.request) + }) + + it('still initializes manual features later when split', async () => { + const [rum, pvt, ajax, jserrors, st, spa] = await Promise.all([ + browser.testHandle.expectRum(), + browser.testHandle.expectTimings(), + browser.testHandle.expectAjaxEvents(10000, true), + browser.testHandle.expectErrors(10000, true), + browser.testHandle.expectResources(), + browser.testHandle.expectInteractionEvents(), + browser.url(await browser.testHandle.assetURL('instrumented.html', { + init: { + ajax: { + block_internal: false, + autoStart: false + }, + jserrors: { + autoStart: false + } + } + })).then(() => browser.execute(function () { + setTimeout(function () { + var xhr = new XMLHttpRequest() + xhr.open('GET', '/json') + xhr.send() + newrelic.noticeError('test') + }, 1000) + })) + ]) + + await browser.pause(2000) + checkRum(rum.request) + checkPVT(pvt.request) + checkSessionTrace(st.request) + checkSpa(spa.request) + + expect(ajax).toEqual(undefined) + expect(jserrors).toEqual(undefined) + + await browser.pause(1000) + const [ajax2, jserrors2] = await Promise.all([ + browser.testHandle.expectAjaxEvents(), + browser.testHandle.expectErrors(), + browser.execute(function () { + newrelic.start() + }) + ]) + + checkAjax(ajax2.request) + checkJsErrors(jserrors2.request) + }) + }) + + describe('individual features', () => { + it('page_view_timings', async () => { + await browser.url(await browser.testHandle.assetURL('instrumented-manual.html')) // Setup expects before loading the page + + const rum = await browser.testHandle.expectRum(5000, true) + expect(rum).toEqual(undefined) + + const [rum2, timings] = await Promise.all([ + browser.testHandle.expectRum(), + browser.testHandle.expectTimings(), + browser.execute(function () { + newrelic.start('page_view_timing') + }) + ]) + checkRum(rum2.request) + checkPVT(timings.request) + }) + + it('ajax', async () => { + await browser.url(await browser.testHandle.assetURL('instrumented-manual.html')) // Setup expects before loading the page + + const rum = await browser.testHandle.expectRum(5000, true) + expect(rum).toEqual(undefined) + + const [rum2, ajax] = await Promise.all([ + browser.testHandle.expectRum(), + browser.testHandle.expectAjaxEvents(), + browser.execute(function () { + newrelic.start('ajax') + }) + ]) + checkRum(rum2.request) + checkAjax(ajax.request) + }) + + it('jserrors', async () => { + await browser.url(await browser.testHandle.assetURL('instrumented-manual.html')) // Setup expects before loading the page + + const rum = await browser.testHandle.expectRum(5000, true) + expect(rum).toEqual(undefined) + + const [rum2, jserrors] = await Promise.all([ + browser.testHandle.expectRum(), + browser.testHandle.expectErrors(), + browser.execute(function () { + newrelic.start('jserrors') + }) + ]) + checkRum(rum2.request) + checkJsErrors(jserrors.request) + }) + + it('metrics', async () => { + await browser.url(await browser.testHandle.assetURL('instrumented-manual.html')) // Setup expects before loading the page + + const rum = await browser.testHandle.expectRum(5000, true) + expect(rum).toEqual(undefined) + + const [rum2, metrics] = await Promise.all([ + browser.testHandle.expectRum(), + browser.testHandle.expectMetrics(), + browser.execute(function () { + newrelic.start('metrics') + setTimeout(function () { + window.location.reload() + }, 1000) + }) + ]) + await browser.pause(2000) + checkRum(rum2.request) + checkMetrics(metrics.request) + }) + + it('page_action', async () => { + await browser.url(await browser.testHandle.assetURL('instrumented-manual.html')) // Setup expects before loading the page + + const rum = await browser.testHandle.expectRum(5000, true) + expect(rum).toEqual(undefined) + + const [rum2, pageAction] = await Promise.all([ + browser.testHandle.expectRum(), + browser.testHandle.expectIns(), + browser.execute(function () { + newrelic.start('page_action') + }) + ]) + checkRum(rum2.request) + checkPageAction(pageAction.request) + }) + + it('session_trace', async () => { + await browser.url(await browser.testHandle.assetURL('instrumented-manual.html')) // Setup expects before loading the page + + const rum = await browser.testHandle.expectRum(5000, true) + expect(rum).toEqual(undefined) + + const [rum2, sessionTrace] = await Promise.all([ + browser.testHandle.expectRum(), + browser.testHandle.expectResources(), + browser.execute(function () { + newrelic.start('session_trace') + }) + ]) + checkRum(rum2.request) + checkSessionTrace(sessionTrace.request) + }) + + it('spa', async () => { + await browser.url(await browser.testHandle.assetURL('instrumented-manual.html')) // Setup expects before loading the page + + const rum = await browser.testHandle.expectRum(5000, true) + expect(rum).toEqual(undefined) + + const [rum2, spa] = await Promise.all([ + browser.testHandle.expectRum(), + browser.testHandle.expectInteractionEvents(), + browser.execute(function () { + newrelic.start('spa') + }) + ]) + checkRum(rum2.request) + checkSpa(spa.request) + }) + }) +}) + +const baseQuery = expect.objectContaining({ + a: expect.any(String), + ck: expect.any(String), + ref: expect.any(String), + rst: expect.any(String), + s: expect.any(String), + t: expect.any(String), + v: expect.any(String) +}) + +function checkRum ({ query, body }) { + expect(query).toMatchObject({ + a: expect.any(String), + af: expect.any(String), + be: expect.any(String), + ck: expect.any(String), + dc: expect.any(String), + fe: expect.any(String), + perf: expect.any(String), + ref: expect.any(String), + rst: expect.any(String), + s: expect.any(String), + t: expect.any(String), + v: expect.any(String) + }) + expect(body).toEqual('') +} + +function checkPVT ({ query, body }) { + const pvtItem = expect.objectContaining({ + attributes: expect.any(Array), + name: expect.any(String), + type: expect.any(String), + value: expect.any(Number) + }) + expect(query).toEqual(baseQuery) + expect(body[0]).toEqual(pvtItem) +} + +function checkAjax ({ query, body }) { + expect(query).toEqual(baseQuery) + expect(body.find(x => x.path === '/json')).toMatchObject({ + callbackDuration: expect.any(Number), + callbackEnd: expect.any(Number), + children: expect.any(Array), + domain: expect.any(String), + end: expect.any(Number), + guid: null, + method: expect.any(String), + nodeId: expect.any(String), + path: expect.any(String), + requestBodySize: expect.any(Number), + requestedWith: expect.any(String), + responseBodySize: expect.any(Number), + start: expect.any(Number), + status: expect.any(Number), + timestamp: null, + traceId: null, + type: expect.any(String) + }) +} + +function checkJsErrors ({ query, body }) { + expect(query).toEqual(baseQuery) + + expect(body.err[0]).toBeTruthy() + expect(body.err[0].params.message).toEqual('test') +} + +function checkMetrics ({ query, body }) { + expect(query).toEqual(baseQuery) + expect(body.sm?.length).toBeTruthy() +} + +function checkPageAction ({ query, body }) { + expect(query).toEqual(baseQuery) + expect(body.ins?.[0]?.test).toEqual(1) +} + +function checkSessionTrace ({ query, body }) { + expect(query).toEqual(baseQuery) + expect(body.res.length).toBeGreaterThanOrEqual(1) +} + +function checkSpa ({ query, body }) { + expect(query).toEqual(baseQuery) + expect(body.length).toBeGreaterThanOrEqual(1) +} diff --git a/tests/specs/session-replay/rrweb-configuration.e2e.js b/tests/specs/session-replay/rrweb-configuration.e2e.js index 8934ca495..ab2145428 100644 --- a/tests/specs/session-replay/rrweb-configuration.e2e.js +++ b/tests/specs/session-replay/rrweb-configuration.e2e.js @@ -30,6 +30,28 @@ describe.withBrowsersMatching(notIE)('RRWeb Configuration', () => { }) }) + describe('optIn', async () => { + it('when enabled: should only import feature after opt in', async () => { + await browser.url(await browser.testHandle.assetURL('instrumented.html', config({ session_replay: { autoStart: false } }))) + .then(() => browser.waitForAgentLoad()) + + let wasInitialized = await browser.execute(function () { + return !!Object.values(newrelic.initializedAgents)[0].features.session_replay?.featAggregate?.initialized + }) + + expect(wasInitialized).toEqual(false) + + await browser.execute(function () { + newrelic.start('session_replay') + }) + await browser.pause(1000) + wasInitialized = await browser.execute(function () { + return Object.values(newrelic.initializedAgents)[0].features.session_replay.featAggregate.initialized + }) + expect(wasInitialized).toEqual(true) + }) + }) + describe('maskAllInputs', () => { it('maskAllInputs: true should convert inputs to *', async () => { await browser.url(await browser.testHandle.assetURL('rrweb-instrumented.html', config())) diff --git a/tools/testing-server/constants.js b/tools/testing-server/constants.js index 09c9c06ec..a8328bfda 100644 --- a/tools/testing-server/constants.js +++ b/tools/testing-server/constants.js @@ -37,3 +37,21 @@ module.exports.rumFlags = { loaded: 1, sr: 0 // this should be off, for now, if privacy.cookie_enabled is on (default) or Traces tests will fail } + +module.exports.defaultInitBlock = { + privacy: { cookies_enabled: false }, + ajax: { deny_list: [], block_internal: false, enabled: true, harvestTimeSeconds: 5, autoStart: true }, + distributed_tracing: {}, + session: { domain: undefined, expiresMs: 14400000, inactiveMs: 1800000 }, + ssl: false, + obfuscate: undefined, + jserrors: { enabled: true, harvestTimeSeconds: 5, autoStart: true }, + metrics: { enabled: true, autoStart: true }, + page_action: { enabled: true, harvestTimeSeconds: 5, autoStart: true }, + page_view_event: { enabled: true, autoStart: true }, + page_view_timing: { enabled: true, harvestTimeSeconds: 5, long_task: false, autoStart: true }, + session_trace: { enabled: true, harvestTimeSeconds: 5, autoStart: true }, + spa: { enabled: true, harvestTimeSeconds: 5, autoStart: true }, + harvest: { tooManyRequestsDelay: 5 }, + session_replay: { enabled: false, harvestTimeSeconds: 5, sampleRate: 0, errorSampleRate: 0, autoStart: true } +} diff --git a/tools/testing-server/plugins/agent-injector/init-transform.js b/tools/testing-server/plugins/agent-injector/init-transform.js index 980c7ff1f..4329e4da1 100644 --- a/tools/testing-server/plugins/agent-injector/init-transform.js +++ b/tools/testing-server/plugins/agent-injector/init-transform.js @@ -1,5 +1,6 @@ const { Transform } = require('stream') -const { regexReplacementRegex } = require('../../constants') +const { regexReplacementRegex, defaultInitBlock } = require('../../constants') +const { deepmerge } = require('deepmerge-ts') /** * Constructs the agent init script block based on the init query. @@ -23,7 +24,7 @@ function getInitContent (request, reply, testServer) { } })() - let initJSON = JSON.stringify(queryInit) + let initJSON = JSON.stringify(deepmerge(defaultInitBlock, queryInit)) if (initJSON.includes('new RegExp')) { // de-serialize RegExp obj from router initJSON = initJSON.replace(regexReplacementRegex, '/$1/$2') diff --git a/tools/wdio/plugins/testing-server/default-asset-query.mjs b/tools/wdio/plugins/testing-server/default-asset-query.mjs index 82a3135df..ce4bd9904 100644 --- a/tools/wdio/plugins/testing-server/default-asset-query.mjs +++ b/tools/wdio/plugins/testing-server/default-asset-query.mjs @@ -4,24 +4,7 @@ * is not defined in this object, it cannot be overridden in a test. */ const query = { - loader: 'spa', - init: { - privacy: { cookies_enabled: false }, - ajax: { deny_list: [], block_internal: false, enabled: true, harvestTimeSeconds: 5 }, - distributed_tracing: {}, - session: { domain: undefined, expiresMs: 14400000, inactiveMs: 1800000 }, - ssl: false, - obfuscate: undefined, - jserrors: { enabled: true, harvestTimeSeconds: 5 }, - metrics: { enabled: true }, - page_action: { enabled: true, harvestTimeSeconds: 5 }, - page_view_event: { enabled: true }, - page_view_timing: { enabled: true, harvestTimeSeconds: 5, long_task: false }, - session_trace: { enabled: true, harvestTimeSeconds: 5 }, - spa: { enabled: true, harvestTimeSeconds: 5 }, - harvest: { tooManyRequestsDelay: 5 }, - session_replay: { enabled: false, harvestTimeSeconds: 5, sampleRate: 0, errorSampleRate: 0 } - } + loader: 'spa' } export default query