Skip to content

Commit

Permalink
feat: Isolate observation context per agent (#903)
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickhousley authored Mar 5, 2024
1 parent 7f5cd4b commit 85887c8
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 31 deletions.
File renamed without changes.
55 changes: 55 additions & 0 deletions src/common/context/observation-context-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { gosNREUM } from '../window/nreum'
import { bundleId } from '../ids/bundle-id'
import { EventContext } from './event-context'

export class ObservationContextManager {
// These IDs are provided for backwards compatibility until the agent is fully updated
// use the observation context class.

static contextId = `nr@context:${bundleId}`
static contextOriginalId = `nr@original:${bundleId}`
static contextWrappedId = `nr@wrapped:${ObservationContextManager.contextId}`

static getObservationContextByAgentIdentifier (agentIdentifier) {
const nr = gosNREUM()
return Object.keys(nr?.initializedAgents || {}).indexOf(agentIdentifier) > -1
? nr.initializedAgents[agentIdentifier].observationContext
: undefined
}

/**
* @type {WeakMap<WeakKey, {[key: string]: unknown}>}
*/
#observationContext = new WeakMap()

/**
* Returns the observation context tied to the supplied construct. If there has been
* no observation construct created, an empty object is created and stored as the current
* context.
* @param key {unknown} The construct being observed such as an XHR instance
* @return {EventContext} An object of key:value pairs to track as
* part of the observation
*/
getCreateContext (key) {
if (!this.#observationContext.has(key)) {
this.#observationContext.set(key, new EventContext())
}

return this.#observationContext.get(key)
}

/**
* Set the observation context for an observed construct. If values of the context
* need to be updated, they should be done so directly on the context. This function
* is only for the setting of a whole context.
* @param key {unknown} The construct being observed such as an XHR instance
* @param value {EventContext} An object of key:value pairs to track as
* part of the observation
* @return {EventContext} The updated observation context
*/
setContext (key, value) {
this.#observationContext.set(key, value)

return this.#observationContext.get(key)
}
}
30 changes: 20 additions & 10 deletions src/common/event-emitter/contextual-ee.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@
import { gosNREUM } from '../window/nreum'
import { getOrSet } from '../util/get-or-set'
import { getRuntime } from '../config/config'
import { EventContext } from './event-context'
import { bundleId } from '../ids/bundle-id'
import { EventContext } from '../context/event-context'
import { ObservationContextManager } from '../context/observation-context-manager'

// create a unique id to store event context data for the current agent bundle
const contextId = `nr@context:${bundleId}`
// create global emitter instance that can be shared among bundles
const globalInstance = ee(undefined, 'globalEE')

Expand All @@ -20,7 +18,7 @@ if (!nr.ee) {
nr.ee = globalInstance
}

export { globalInstance as ee, contextId }
export { globalInstance as ee }

function ee (old, debugId) {
var handlers = {}
Expand Down Expand Up @@ -52,8 +50,8 @@ function ee (old, debugId) {
aborted: false,
isBuffering,
debugId,
backlog: isolatedBacklog ? {} : old && typeof old.backlog === 'object' ? old.backlog : {}

backlog: isolatedBacklog ? {} : old && typeof old.backlog === 'object' ? old.backlog : {},
observationContextManager: null
}

return emitter
Expand All @@ -62,9 +60,15 @@ function ee (old, debugId) {
if (contextOrStore && contextOrStore instanceof EventContext) {
return contextOrStore
} else if (contextOrStore) {
return getOrSet(contextOrStore, contextId, () => new EventContext(contextId))
return getOrSet(contextOrStore, ObservationContextManager.contextId, () =>
emitter.observationContextManager
? emitter.observationContextManager.getCreateContext(contextOrStore)
: new EventContext(ObservationContextManager.contextId)
)
} else {
return new EventContext(contextId)
return emitter.observationContextManager
? emitter.observationContextManager.getCreateContext({})
: new EventContext(ObservationContextManager.contextId)
}
}

Expand Down Expand Up @@ -112,7 +116,13 @@ function ee (old, debugId) {
}

function getOrCreate (name) {
return (emitters[name] = emitters[name] || ee(emitter, name))
const newEventEmitter = (emitters[name] = emitters[name] || ee(emitter, name))

if (!newEventEmitter.observationContextManager && emitter.observationContextManager) {
newEventEmitter.observationContextManager = emitter.observationContextManager
}

return newEventEmitter
}

function bufferEventsByGroup (types, group) {
Expand Down
6 changes: 3 additions & 3 deletions src/common/wrap/wrap-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
* This module is used directly by: session_trace.
* It is also called by -> wrapXhr <-, so see "wrap-xhr.js" for features that use this indirectly.
*/
import { ee as baseEE, contextId } from '../event-emitter/contextual-ee'
import { ee as baseEE } from '../event-emitter/contextual-ee'
import { createWrapperWithEmitter as wfn } from './wrap-function'
import { getOrSet } from '../util/get-or-set'
import { globalScope, isBrowserScope } from '../constants/runtime'
import { ObservationContextManager } from '../context/observation-context-manager'

const wrapped = {}
const XHR = globalScope.XMLHttpRequest
const ADD_EVENT_LISTENER = 'addEventListener'
const REMOVE_EVENT_LISTENER = 'removeEventListener'
const flag = `nr@wrapped:${contextId}`

/**
* Wraps `addEventListener` and `removeEventListener` on: global scope; the prototype of `XMLHttpRequest`, and
Expand Down Expand Up @@ -49,7 +49,7 @@ export function wrapEvents (sharedEE) {
return
}

var wrapped = getOrSet(originalListener, flag, function () {
var wrapped = getOrSet(originalListener, ObservationContextManager.contextWrappedId, function () {
var listener = {
object: wrapHandleEvent,
function: originalListener
Expand Down
5 changes: 3 additions & 2 deletions src/common/wrap/wrap-fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
* @file Wraps `fetch` and related methods for instrumentation.
* This module is used by: ajax, spa.
*/
import { ee as baseEE, contextId } from '../event-emitter/contextual-ee'
import { ee as baseEE } from '../event-emitter/contextual-ee'
import { globalScope } from '../constants/runtime'
import { ObservationContextManager } from '../context/observation-context-manager'

var prefix = 'fetch-'
var bodyPrefix = prefix + 'body-'
Expand Down Expand Up @@ -74,7 +75,7 @@ export function wrapFetch (sharedEE) {
// we are wrapping args in an array so we can preserve the reference
ee.emit(prefix + 'before-start', [args], ctx)
var dtPayload
if (ctx[contextId] && ctx[contextId].dt) dtPayload = ctx[contextId].dt
if (ctx[ObservationContextManager.contextId] && ctx[ObservationContextManager.contextId].dt) dtPayload = ctx[ObservationContextManager.contextId].dt

var origPromiseFromFetch = fn.apply(this, args)

Expand Down
10 changes: 4 additions & 6 deletions src/common/wrap/wrap-function.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@
*/

import { ee } from '../event-emitter/contextual-ee'
import { bundleId } from '../ids/bundle-id'

export const flag = `nr@original:${bundleId}`
import { ObservationContextManager } from '../context/observation-context-manager'

/**
* A convenience alias of `hasOwnProperty`.
Expand Down Expand Up @@ -42,7 +40,7 @@ export function createWrapperWithEmitter (emitter, always) {
* As a property on a wrapped function, contains the original function.
* @type {string}
*/
wrapFn.flag = flag
wrapFn.flag = ObservationContextManager.contextOriginalId

return wrapFn

Expand All @@ -61,7 +59,7 @@ export function createWrapperWithEmitter (emitter, always) {

if (!prefix) prefix = ''

nrWrapper[flag] = fn
nrWrapper[ObservationContextManager.contextOriginalId] = fn
copy(fn, nrWrapper, emitter)
return nrWrapper

Expand Down Expand Up @@ -215,5 +213,5 @@ function copy (from, to, emitter) {
* @returns {boolean} Whether the passed function is ineligible to be wrapped.
*/
function notWrappable (fn) {
return !(fn && typeof fn === 'function' && fn.apply && !fn[flag])
return !(fn && typeof fn === 'function' && fn.apply && !fn[ObservationContextManager.contextOriginalId])
}
5 changes: 3 additions & 2 deletions src/common/wrap/wrap-promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
* This module is used by: spa.
*/

import { createWrapperWithEmitter as wrapFn, flag } from './wrap-function'
import { createWrapperWithEmitter as wrapFn } from './wrap-function'
import { ee as baseEE } from '../event-emitter/contextual-ee'
import { globalScope } from '../constants/runtime'
import { ObservationContextManager } from '../context/observation-context-manager'

const wrapped = {}

Expand Down Expand Up @@ -122,7 +123,7 @@ export function wrapPromise (sharedEE) {

return origFnCallWithThis
}
prevPromiseObj.prototype.then[flag] = prevPromiseOrigThen
prevPromiseObj.prototype.then[ObservationContextManager.contextOriginalId] = prevPromiseOrigThen

promiseEE.on('executor-start', function (args) {
args[0] = promiseWrapper(args[0], 'resolve-', this, null, false)
Expand Down
14 changes: 14 additions & 0 deletions src/loaders/agent-base.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
/* eslint-disable n/handle-callback-err */

import { warn } from '../common/util/console'
import { ObservationContextManager } from '../common/context/observation-context-manager'
import { generateRandomHexString } from '../common/ids/unique-id'
import { ee } from '../common/event-emitter/contextual-ee'

/**
* @typedef {import('./api/interaction-types').InteractionInstance} InteractionInstance
*/

export class AgentBase {
agentIdentifier
observationContext = new ObservationContextManager()

constructor (agentIdentifier = generateRandomHexString(16)) {
this.agentIdentifier = agentIdentifier

// Assign the observation context to the event emitter, so it knows how to create observation contexts
const eventEmitter = ee.get(agentIdentifier)
eventEmitter.observationContext = this.observationContext
}

/**
* Tries to execute the api and generates a generic warning message with the api name injected if unsuccessful
* @param {string} methodName
Expand Down
8 changes: 3 additions & 5 deletions src/loaders/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import { Instrument as PageViewEvent } from '../features/page_view_event/instrum
// common files
import { Aggregator } from '../common/aggregate/aggregator'
import { gosNREUM, setNREUMInitializedAgent } from '../common/window/nreum'
import { generateRandomHexString } from '../common/ids/unique-id'
import { getConfiguration, getInfo, getLoaderConfig, getRuntime } from '../common/config/config'
import { warn } from '../common/util/console'
import { stringify } from '../common/util/stringify'
Expand All @@ -23,8 +22,8 @@ import { globalScope } from '../common/constants/runtime'
* sensitive to network load, this may result in smaller builds with slightly lower performance impact.
*/
export class Agent extends AgentBase {
constructor (options, agentIdentifier = generateRandomHexString(16)) {
super()
constructor (options, agentIdentifier) {
super(agentIdentifier)

if (!globalScope) {
// We could not determine the runtime environment. Short-circuite the agent here
Expand All @@ -33,10 +32,9 @@ export class Agent extends AgentBase {
return
}

this.agentIdentifier = agentIdentifier
this.sharedAggregator = new Aggregator({ agentIdentifier: this.agentIdentifier })
this.features = {}
setNREUMInitializedAgent(agentIdentifier, this) // append this agent onto the global NREUM.initializedAgents
setNREUMInitializedAgent(this.agentIdentifier, this) // append this agent onto the global NREUM.initializedAgents

this.desiredFeatures = new Set(options.features || []) // expected to be a list of static Instrument/InstrumentBase classes, see "spa.js" for example
// For Now... ALL agents must make the rum call whether the page_view_event feature was enabled or not.
Expand Down
8 changes: 5 additions & 3 deletions tests/specs/api.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ describe('newrelic api', () => {
})
const agentInstanceApiMethods = await browser.execute(function () {
function getAllPropertyNames (obj) {
let result = new Set()
var result = new Set()
while (obj) {
Object.getOwnPropertyNames(obj).forEach(p => result.add(p))
Object.getOwnPropertyNames(obj).forEach(function (p) {
return result.add(p)
})
obj = Object.getPrototypeOf(obj)
}
return [...result]
return Array.from(result)
}
return getAllPropertyNames(Object.values(newrelic.initializedAgents)[0])
})
Expand Down

0 comments on commit 85887c8

Please sign in to comment.