Skip to content

Commit 1832562

Browse files
authored
feat: Inspection events (#1413)
1 parent b3fe4f4 commit 1832562

File tree

12 files changed

+211
-19
lines changed

12 files changed

+211
-19
lines changed

src/common/harvest/harvester.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { obj, param } from '../url/encode'
1616
import { warn } from '../util/console'
1717
import { stringify } from '../util/stringify'
1818
import { getSubmitMethod, xhr as xhrMethod, xhrFetch as fetchMethod } from '../util/submit-data'
19+
import { activatedFeatures } from '../util/feature-flags'
20+
import { dispatchGlobalEvent } from '../dispatch/global-event'
1921

2022
const RETRY_FAILED = 'Harvester/Retry/Failed/'
2123
const RETRY_SUCCEEDED = 'Harvester/Retry/Succeeded/'
@@ -80,7 +82,8 @@ export class Harvester {
8082
localOpts,
8183
submitMethod,
8284
cbFinished,
83-
raw: aggregateInst.harvestOpts.raw
85+
raw: aggregateInst.harvestOpts.raw,
86+
featureName: aggregateInst.featureName
8487
})
8588
ranSend = true
8689
})
@@ -114,7 +117,7 @@ const warnings = {}
114117
* @param {NetworkSendSpec} param0 Specification for sending data
115118
* @returns {boolean} True if a network call was made. Note that this does not mean or guarantee that it was successful.
116119
*/
117-
function send (agentRef, { endpoint, targetApp, payload, localOpts = {}, submitMethod, cbFinished, raw }) {
120+
function send (agentRef, { endpoint, targetApp, payload, localOpts = {}, submitMethod, cbFinished, raw, featureName }) {
118121
if (!agentRef.info.errorBeacon) return false
119122

120123
let { body, qs } = cleanPayload(payload)
@@ -172,6 +175,24 @@ function send (agentRef, { endpoint, targetApp, payload, localOpts = {}, submitM
172175
})
173176
}
174177
}
178+
179+
dispatchGlobalEvent({
180+
agentIdentifier: agentRef.agentIdentifier,
181+
loaded: !!activatedFeatures?.[agentRef.agentIdentifier],
182+
type: 'data',
183+
name: 'harvest',
184+
feature: featureName,
185+
data: {
186+
endpoint,
187+
headers,
188+
targetApp,
189+
payload,
190+
submitMethod: getSubmitMethodName(),
191+
raw,
192+
synchronousXhr: !!(localOpts.isFinalHarvest && isWorkerScope)
193+
}
194+
})
195+
175196
return true
176197

177198
function shouldRetry (status) {
@@ -183,6 +204,12 @@ function send (agentRef, { endpoint, targetApp, payload, localOpts = {}, submitM
183204
}
184205
return (status >= 502 && status <= 504) || (status >= 512 && status <= 530)
185206
}
207+
208+
function getSubmitMethodName () {
209+
if (submitMethod === xhrMethod) return 'xhr'
210+
if (submitMethod === fetchMethod) return 'fetch'
211+
return 'beacon'
212+
}
186213
}
187214

188215
/**

src/common/util/feature-flags.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,12 @@ export function activateFeatures (flags, agentIdentifier) {
2929
sentIds.add(agentIdentifier)
3030

3131
// let any window level subscribers know that the agent is running
32-
dispatchGlobalEvent({ loaded: true })
32+
dispatchGlobalEvent({
33+
agentIdentifier,
34+
loaded: true,
35+
type: 'lifecycle',
36+
name: 'load',
37+
feature: undefined,
38+
data: flags
39+
})
3340
}

src/features/utils/aggregate-base.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class AggregateBase extends FeatureBase {
3737
This was necessary to prevent race cond. issues where the event buffer was checked before the feature could "block" itself.
3838
Its easier to just keep an empty event buffer in place. */
3939
default:
40-
this.events = new EventStoreManager(agentRef.mainAppKey, 1)
40+
this.events = new EventStoreManager(agentRef.mainAppKey, 1, agentRef.agentIdentifier, this.featureName)
4141
break
4242
}
4343
this.harvestOpts = {} // features aggregate classes can define custom opts for when their harvest is called
@@ -157,7 +157,7 @@ export class AggregateBase extends FeatureBase {
157157

158158
if (!agentRef.mainAppKey) agentRef.mainAppKey = { licenseKey: agentRef.info.licenseKey, appId: agentRef.info.applicationID }
159159
// Create a single Aggregator for this agent if DNE yet; to be used by jserror endpoint features.
160-
if (!agentRef.sharedAggregator) agentRef.sharedAggregator = new EventStoreManager(agentRef.mainAppKey, 2)
160+
if (!agentRef.sharedAggregator) agentRef.sharedAggregator = new EventStoreManager(agentRef.mainAppKey, 2, agentRef.agentIdentifier, 'shared_aggregator')
161161

162162
if (!agentRef.runtime.harvester) agentRef.runtime.harvester = new Harvester(agentRef)
163163
}

src/features/utils/event-store-manager.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55
import { EventAggregator } from '../../common/aggregate/event-aggregator'
6+
import { dispatchGlobalEvent } from '../../common/dispatch/global-event'
7+
import { activatedFeatures } from '../../common/util/feature-flags'
68
import { EventBuffer } from './event-buffer'
79

810
/**
@@ -13,12 +15,16 @@ export class EventStoreManager {
1315
/**
1416
* @param {object} defaultTarget - should contain licenseKey and appId of the main app from NREUM.info at startup
1517
* @param {1|2} storageChoice - the type of storage to use in this manager; 'EventBuffer' (1), 'EventAggregator' (2)
18+
* @param {string} agentIdentifier - agent identifier used in inspection events
19+
* @param {string} featureName - feature name used in inspection events for non-shared aggregators
1620
*/
17-
constructor (defaultTarget, storageChoice) {
21+
constructor (defaultTarget, storageChoice, agentIdentifier, featureName) {
1822
this.mainApp = defaultTarget
1923
this.StorageClass = storageChoice === 1 ? EventBuffer : EventAggregator
2024
this.appStorageMap = new Map()
2125
this.appStorageMap.set(defaultTarget, new this.StorageClass())
26+
this.agentIdentifier = agentIdentifier
27+
this.featureName = featureName
2228
}
2329

2430
// This class must contain an union of all methods from all supported storage classes and conceptualize away the target app argument.
@@ -45,6 +51,14 @@ export class EventStoreManager {
4551
* @returns {boolean} True if the event was successfully added
4652
*/
4753
add (event, target) {
54+
dispatchGlobalEvent({
55+
agentIdentifier: this.agentIdentifier,
56+
loaded: !!activatedFeatures?.[this.agentIdentifier],
57+
type: 'data',
58+
name: 'buffer',
59+
feature: this.featureName,
60+
data: event
61+
})
4862
if (target && !this.appStorageMap.has(target)) this.appStorageMap.set(target, new this.StorageClass())
4963
return this.appStorageMap.get(target || this.mainApp).add(event)
5064
}

src/loaders/api/api.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import { MODE } from '../../common/session/constants'
2020
import { LOG_LEVELS } from '../../features/logging/constants'
2121
import { bufferLog } from '../../features/logging/shared/utils'
2222
import { wrapLogger } from '../../common/wrap/wrap-logger'
23+
import { dispatchGlobalEvent } from '../../common/dispatch/global-event'
24+
import { activatedFeatures } from '../../common/util/feature-flags'
2325

2426
export function setTopLevelCallers () {
2527
const nr = gosCDN()
@@ -194,6 +196,14 @@ export function setAPI (agentIdentifier, forceDrain, runSoftNavOverSpa = false)
194196
function apiCall (prefix, name, notSpa, bufferGroup) {
195197
return function () {
196198
handle(SUPPORTABILITY_METRIC_CHANNEL, ['API/' + name + '/called'], undefined, FEATURE_NAMES.metrics, instanceEE)
199+
dispatchGlobalEvent({
200+
agentIdentifier,
201+
loaded: !!activatedFeatures?.[agentIdentifier],
202+
type: 'data',
203+
name: 'api',
204+
feature: prefix + name,
205+
data: { notSpa, bufferGroup }
206+
})
197207
if (bufferGroup) handle(prefix + name, [notSpa ? now() : performance.now(), ...arguments], notSpa ? null : this, bufferGroup, instanceEE) // no bufferGroup means only the SM is emitted
198208
return notSpa ? undefined : this // returns the InteractionHandle which allows these methods to be chained
199209
}

src/loaders/configure/configure.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { activatedFeatures } from '../../common/util/feature-flags'
1212
import { isWorkerScope } from '../../common/constants/runtime'
1313
import { redefinePublicPath } from './public-path'
1414
import { ee } from '../../common/event-emitter/contextual-ee'
15+
import { dispatchGlobalEvent } from '../../common/dispatch/global-event'
1516

1617
let alreadySetOnce = false // the configure() function can run multiple times in agent lifecycle
1718

@@ -68,5 +69,17 @@ export function configure (agent, opts = {}, loaderType, forceDrain) {
6869

6970
if (agent.api === undefined) agent.api = setAPI(agent.agentIdentifier, forceDrain, agent.runSoftNavOverSpa)
7071
if (agent.exposed === undefined) agent.exposed = exposed
72+
73+
if (!alreadySetOnce) {
74+
dispatchGlobalEvent({
75+
agentIdentifier: agent.agentIdentifier,
76+
loaded: !!activatedFeatures?.[agent.agentIdentifier],
77+
type: 'lifecycle',
78+
name: 'initialize',
79+
feature: undefined,
80+
data: { init: updatedInit, info, loader_config, runtime }
81+
})
82+
}
83+
7184
alreadySetOnce = true
7285
}

tests/assets/all-events.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
xhr.send()
1414

1515
window.addEventListener('newrelic', function(event) {
16-
if (event.detail.loaded) {
16+
if (event?.detail?.type === 'lifecycle' && event.detail.name === 'load') {
1717
var xhr = new XMLHttpRequest()
1818
xhr.open('GET', '/json')
1919
xhr.send()
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<!DOCTYPE html>
2+
<!--
3+
Copyright 2020 New Relic Corporation.
4+
PDX-License-Identifier: Apache-2.0
5+
-->
6+
<html>
7+
8+
<head>
9+
<title>Inspection events</title>
10+
<script>
11+
window.inspectionEvents = {
12+
initialize: false,
13+
load: false,
14+
buffer: false,
15+
harvest: false,
16+
api: false,
17+
}
18+
window.addEventListener('newrelic', ({ detail }) => {
19+
if (!detail || !detail.agentIdentifier) return
20+
const { name, loaded, type, feature, data } = detail
21+
22+
if (
23+
name == 'initialize' &&
24+
loaded == false &&
25+
type == 'lifecycle' &&
26+
feature == undefined &&
27+
data
28+
) {
29+
window.inspectionEvents.initialize = true
30+
}
31+
32+
if (
33+
name === 'load' &&
34+
loaded === true &&
35+
type === 'lifecycle' &&
36+
feature === undefined &&
37+
data
38+
) {
39+
window.inspectionEvents.load = true
40+
}
41+
42+
if (
43+
name === 'buffer' &&
44+
loaded === true &&
45+
type === 'data' &&
46+
feature &&
47+
data
48+
) {
49+
window.inspectionEvents.buffer = true
50+
}
51+
52+
if (
53+
name === 'harvest' &&
54+
loaded === true &&
55+
type === 'data' &&
56+
feature &&
57+
data
58+
) {
59+
window.inspectionEvents.harvest = true
60+
}
61+
62+
// The API call in this test is called before the agent is loaded, so loaded should be false
63+
if (
64+
name === 'api' &&
65+
loaded === false &&
66+
type === 'data' &&
67+
feature &&
68+
data
69+
) {
70+
window.inspectionEvents.api = true
71+
}
72+
})
73+
</script>
74+
{init} {config} {loader}
75+
</head>
76+
77+
<body>
78+
This page is used to monitor and record events from agent-specific event listeners.
79+
<script>
80+
newrelic.finished()
81+
</script>
82+
</body>
83+
84+
</html>

tests/components/setup-agent.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function setupAgent ({ agentOverrides = {}, info = {}, init = {}, loaderC
3939
const fakeAgent = {
4040
agentIdentifier,
4141
ee: eventEmitter,
42-
sharedAggregator: new EventStoreManager({ licenseKey: info.licenseKey, appId: info.applicationID }, 2),
42+
sharedAggregator: new EventStoreManager({ licenseKey: info.licenseKey, appId: info.applicationID }, 2, agentIdentifier),
4343
...agentOverrides
4444
}
4545
setNREUMInitializedAgent(agentIdentifier, fakeAgent)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { getDebugLogs } from './util/helpers'
2+
3+
describe('inspection events', () => {
4+
it('emits all types of inspection events', async () => {
5+
await browser.url(await browser.testHandle.assetURL('inspection-events.html'))
6+
.then(() => browser.waitForAgentLoad())
7+
.then(() => browser.waitUntil(
8+
() => browser.execute(function () {
9+
return window?.inspectionEvents?.buffer &&
10+
window.inspectionEvents.harvest &&
11+
window.inspectionEvents.api
12+
}),
13+
{
14+
timeout: 30000,
15+
timeoutMsg: 'Timeout on inspection events'
16+
}))
17+
18+
const inspectionEvents = await browser.execute(function () {
19+
return window.inspectionEvents
20+
})
21+
22+
console.log(await getDebugLogs())
23+
24+
expect(inspectionEvents.initialize).toBe(true)
25+
expect(inspectionEvents.load).toBe(true)
26+
expect(inspectionEvents.buffer).toBe(true)
27+
expect(inspectionEvents.harvest).toBe(true)
28+
expect(inspectionEvents.api).toBe(true)
29+
})
30+
})

0 commit comments

Comments
 (0)