@newrelic/browser-agent 1.298.0 → 1.299.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +12 -2
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/dom/selector-path.js +60 -44
- package/dist/cjs/common/harvest/harvester.js +37 -7
- package/dist/cjs/common/url/extract-url.js +21 -0
- package/dist/cjs/features/ajax/instrument/index.js +2 -9
- package/dist/cjs/features/generic_events/aggregate/index.js +29 -8
- package/dist/cjs/features/generic_events/aggregate/user-actions/aggregated-user-action.js +5 -3
- package/dist/cjs/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +71 -33
- package/dist/cjs/features/generic_events/constants.js +2 -1
- package/dist/cjs/features/generic_events/instrument/index.js +45 -0
- package/dist/cjs/features/metrics/aggregate/framework-detection.js +2 -0
- package/dist/cjs/features/metrics/aggregate/harvest-metadata.js +45 -0
- package/dist/cjs/features/metrics/aggregate/index.js +21 -0
- package/dist/cjs/features/session_replay/aggregate/index.js +1 -1
- package/dist/cjs/features/session_replay/shared/recorder.js +6 -2
- package/dist/cjs/loaders/api/noticeError.js +1 -0
- package/dist/cjs/loaders/configure/configure.js +1 -0
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/dom/selector-path.js +58 -42
- package/dist/esm/common/harvest/harvester.js +37 -7
- package/dist/esm/common/url/extract-url.js +15 -0
- package/dist/esm/features/ajax/instrument/index.js +2 -9
- package/dist/esm/features/generic_events/aggregate/index.js +29 -8
- package/dist/esm/features/generic_events/aggregate/user-actions/aggregated-user-action.js +5 -3
- package/dist/esm/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +72 -34
- package/dist/esm/features/generic_events/constants.js +1 -0
- package/dist/esm/features/generic_events/instrument/index.js +46 -1
- package/dist/esm/features/metrics/aggregate/framework-detection.js +2 -0
- package/dist/esm/features/metrics/aggregate/harvest-metadata.js +39 -0
- package/dist/esm/features/metrics/aggregate/index.js +21 -0
- package/dist/esm/features/session_replay/aggregate/index.js +1 -1
- package/dist/esm/features/session_replay/shared/recorder.js +6 -2
- package/dist/esm/loaders/api/noticeError.js +1 -0
- package/dist/esm/loaders/configure/configure.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/common/dom/selector-path.d.ts +6 -1
- package/dist/types/common/dom/selector-path.d.ts.map +1 -1
- package/dist/types/common/harvest/harvester.d.ts.map +1 -1
- package/dist/types/common/url/extract-url.d.ts +7 -0
- package/dist/types/common/url/extract-url.d.ts.map +1 -0
- package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
- package/dist/types/features/generic_events/aggregate/index.d.ts +1 -3
- package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts +3 -1
- package/dist/types/features/generic_events/aggregate/user-actions/aggregated-user-action.d.ts.map +1 -1
- package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts +3 -0
- package/dist/types/features/generic_events/aggregate/user-actions/user-actions-aggregator.d.ts.map +1 -1
- package/dist/types/features/generic_events/constants.d.ts +1 -0
- package/dist/types/features/generic_events/constants.d.ts.map +1 -1
- package/dist/types/features/generic_events/instrument/index.d.ts +2 -0
- package/dist/types/features/generic_events/instrument/index.d.ts.map +1 -1
- package/dist/types/features/metrics/aggregate/framework-detection.d.ts.map +1 -1
- package/dist/types/features/metrics/aggregate/harvest-metadata.d.ts +6 -0
- package/dist/types/features/metrics/aggregate/harvest-metadata.d.ts.map +1 -0
- package/dist/types/features/metrics/aggregate/index.d.ts +2 -0
- package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
- package/dist/types/loaders/api/noticeError.d.ts.map +1 -1
- package/dist/types/loaders/configure/configure.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/dom/selector-path.js +51 -39
- package/src/common/harvest/harvester.js +34 -7
- package/src/common/url/extract-url.js +17 -0
- package/src/features/ajax/instrument/index.js +2 -10
- package/src/features/generic_events/aggregate/index.js +23 -8
- package/src/features/generic_events/aggregate/user-actions/aggregated-user-action.js +5 -3
- package/src/features/generic_events/aggregate/user-actions/user-actions-aggregator.js +80 -24
- package/src/features/generic_events/constants.js +2 -0
- package/src/features/generic_events/instrument/index.js +51 -1
- package/src/features/metrics/aggregate/framework-detection.js +2 -0
- package/src/features/metrics/aggregate/harvest-metadata.js +42 -0
- package/src/features/metrics/aggregate/index.js +21 -0
- package/src/features/session_replay/aggregate/index.js +1 -1
- package/src/features/session_replay/shared/recorder.js +5 -1
- package/src/loaders/api/noticeError.js +1 -0
- package/src/loaders/configure/configure.js +1 -0
|
@@ -135,14 +135,16 @@ export function send (agentRef, { endpoint, targetApp, payload, localOpts = {},
|
|
|
135
135
|
|
|
136
136
|
const fullUrl = `${url}?${baseParams}${payloadParams}`
|
|
137
137
|
const gzip = !!qs?.attributes?.includes('gzip')
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
138
|
+
|
|
139
|
+
// all gzipped data is already in the correct format and needs no transformation
|
|
140
|
+
// all features going to 'events' endpoint should already be serialized & stringified
|
|
141
|
+
let stringBody = gzip || endpoint === EVENTS ? body : stringify(body)
|
|
143
142
|
|
|
144
143
|
// If body is null, undefined, or an empty object or array after stringifying, send an empty string instead.
|
|
145
|
-
if (!
|
|
144
|
+
if (!stringBody || stringBody.length === 0 || stringBody === '{}' || stringBody === '[]') stringBody = ''
|
|
145
|
+
|
|
146
|
+
// Warn--once per endpoint--if the agent tries to send large payloads
|
|
147
|
+
if (endpoint !== BLOBS && stringBody.length > 750000 && (warnings[endpoint] = (warnings[endpoint] || 0) + 1) === 1) warn(28, endpoint)
|
|
146
148
|
|
|
147
149
|
const headers = [{ key: 'content-type', value: 'text/plain' }]
|
|
148
150
|
|
|
@@ -150,7 +152,7 @@ export function send (agentRef, { endpoint, targetApp, payload, localOpts = {},
|
|
|
150
152
|
Because they still do permit synch XHR, the idea is that at final harvest time (worker is closing),
|
|
151
153
|
we just make a BLOCKING request--trivial impact--with the remaining data as a temp fill-in for sendBeacon.
|
|
152
154
|
Following the removal of img-element method. */
|
|
153
|
-
let result = submitMethod({ url: fullUrl, body, sync: localOpts.isFinalHarvest && isWorkerScope, headers })
|
|
155
|
+
let result = submitMethod({ url: fullUrl, body: stringBody, sync: localOpts.isFinalHarvest && isWorkerScope, headers })
|
|
154
156
|
|
|
155
157
|
if (!localOpts.isFinalHarvest && cbFinished) { // final harvests don't hold onto buffer data (shouldRetryOnFail is false), so cleanup isn't needed
|
|
156
158
|
if (submitMethod === xhrMethod) {
|
|
@@ -160,6 +162,9 @@ export function send (agentRef, { endpoint, targetApp, payload, localOpts = {},
|
|
|
160
162
|
const cbResult = { sent: this.status !== 0, status: this.status, retry: shouldRetry(this.status), fullUrl, xhr: this, targetApp }
|
|
161
163
|
if (localOpts.needResponse) cbResult.responseText = this.responseText
|
|
162
164
|
cbFinished(cbResult)
|
|
165
|
+
|
|
166
|
+
/** temporary audit of consistency of harvest metadata flags */
|
|
167
|
+
if (!shouldRetry(this.status)) trackHarvestMetadata()
|
|
163
168
|
}, eventListenerOpts(false))
|
|
164
169
|
} else if (submitMethod === fetchMethod) {
|
|
165
170
|
result.then(async function (response) {
|
|
@@ -167,8 +172,30 @@ export function send (agentRef, { endpoint, targetApp, payload, localOpts = {},
|
|
|
167
172
|
const cbResult = { sent: true, status, retry: shouldRetry(status), fullUrl, fetchResponse: response, targetApp }
|
|
168
173
|
if (localOpts.needResponse) cbResult.responseText = await response.text()
|
|
169
174
|
cbFinished(cbResult)
|
|
175
|
+
/** temporary audit of consistency of harvest metadata flags */
|
|
176
|
+
if (!shouldRetry(status)) trackHarvestMetadata()
|
|
170
177
|
})
|
|
171
178
|
}
|
|
179
|
+
|
|
180
|
+
function trackHarvestMetadata () {
|
|
181
|
+
try {
|
|
182
|
+
if (featureName === FEATURE_NAMES.jserrors && !body?.err) return
|
|
183
|
+
|
|
184
|
+
const hasReplay = baseParams.includes('hr=1')
|
|
185
|
+
const hasTrace = baseParams.includes('ht=1')
|
|
186
|
+
const hasError = qs?.attributes?.includes('hasError=true')
|
|
187
|
+
|
|
188
|
+
handle('harvest-metadata', [{
|
|
189
|
+
[featureName]: {
|
|
190
|
+
...(hasReplay && { hasReplay }),
|
|
191
|
+
...(hasTrace && { hasTrace }),
|
|
192
|
+
...(hasError && { hasError })
|
|
193
|
+
}
|
|
194
|
+
}], undefined, FEATURE_NAMES.metrics, agentRef.ee)
|
|
195
|
+
} catch (err) {
|
|
196
|
+
// do nothing
|
|
197
|
+
}
|
|
198
|
+
}
|
|
172
199
|
}
|
|
173
200
|
|
|
174
201
|
dispatchGlobalEvent({
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2020-2025 New Relic, Inc. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
import { globalScope } from '../constants/runtime'
|
|
6
|
+
import { gosNREUMOriginals } from '../window/nreum'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extracts a URL from various target types.
|
|
10
|
+
* @param {string|Request|URL} target - The target to extract the URL from. It can be a string, a Fetch Request object, or a URL object.
|
|
11
|
+
* @returns {string|undefined} The extracted URL as a string, or undefined if the target type is not supported.
|
|
12
|
+
*/
|
|
13
|
+
export function extractUrl (target) {
|
|
14
|
+
if (typeof target === 'string') return target
|
|
15
|
+
else if (target instanceof gosNREUMOriginals().o.REQ) return target.url
|
|
16
|
+
else if (globalScope?.URL && target instanceof URL) return target.href
|
|
17
|
+
}
|
|
@@ -19,6 +19,7 @@ import { FEATURE_NAMES } from '../../../loaders/features/features'
|
|
|
19
19
|
import { SUPPORTABILITY_METRIC } from '../../metrics/constants'
|
|
20
20
|
import { now } from '../../../common/timing/now'
|
|
21
21
|
import { hasUndefinedHostname } from '../../../common/deny-list/deny-list'
|
|
22
|
+
import { extractUrl } from '../../../common/url/extract-url'
|
|
22
23
|
|
|
23
24
|
var handlers = ['load', 'error', 'abort', 'timeout']
|
|
24
25
|
var handlersLen = handlers.length
|
|
@@ -318,16 +319,7 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
|
|
|
318
319
|
|
|
319
320
|
var opts = this.opts || {}
|
|
320
321
|
var target = this.target
|
|
321
|
-
|
|
322
|
-
var url
|
|
323
|
-
if (typeof target === 'string') {
|
|
324
|
-
url = target
|
|
325
|
-
} else if (typeof target === 'object' && target instanceof origRequest) {
|
|
326
|
-
url = target.url
|
|
327
|
-
} else if (globalScope?.URL && typeof target === 'object' && target instanceof URL) {
|
|
328
|
-
url = target.href
|
|
329
|
-
}
|
|
330
|
-
addUrl(this, url)
|
|
322
|
+
addUrl(this, extractUrl(target))
|
|
331
323
|
|
|
332
324
|
var method = ('' + ((target && target instanceof origRequest && target.method) ||
|
|
333
325
|
opts.method || 'GET')).toUpperCase()
|
|
@@ -17,6 +17,8 @@ import { isPureObject } from '../../../common/util/type-check'
|
|
|
17
17
|
|
|
18
18
|
export class Aggregate extends AggregateBase {
|
|
19
19
|
static featureName = FEATURE_NAME
|
|
20
|
+
#userActionAggregator
|
|
21
|
+
|
|
20
22
|
constructor (agentRef) {
|
|
21
23
|
super(agentRef, FEATURE_NAME)
|
|
22
24
|
this.referrerUrl = (isBrowserScope && document.referrer) ? cleanURL(document.referrer) : undefined
|
|
@@ -28,7 +30,7 @@ export class Aggregate extends AggregateBase {
|
|
|
28
30
|
return
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
this
|
|
33
|
+
this.#trackSupportabilityMetrics()
|
|
32
34
|
|
|
33
35
|
registerHandler('api-recordCustomEvent', (timestamp, eventType, attributes) => {
|
|
34
36
|
if (RESERVED_EVENT_TYPES.includes(eventType)) return warn(46)
|
|
@@ -59,8 +61,8 @@ export class Aggregate extends AggregateBase {
|
|
|
59
61
|
|
|
60
62
|
let addUserAction = () => { /** no-op */ }
|
|
61
63
|
if (isBrowserScope && agentRef.init.user_actions.enabled) {
|
|
62
|
-
this
|
|
63
|
-
this.harvestOpts.beforeUnload = () => addUserAction?.(this
|
|
64
|
+
this.#userActionAggregator = new UserActionsAggregator(agentRef.init.feature_flags.includes('user_frustrations'))
|
|
65
|
+
this.harvestOpts.beforeUnload = () => addUserAction?.(this.#userActionAggregator.aggregationEvent)
|
|
64
66
|
|
|
65
67
|
addUserAction = (aggregatedUserAction) => {
|
|
66
68
|
try {
|
|
@@ -68,7 +70,7 @@ export class Aggregate extends AggregateBase {
|
|
|
68
70
|
* so we still need to validate that an event was given to this method before we try to add */
|
|
69
71
|
if (aggregatedUserAction?.event) {
|
|
70
72
|
const { target, timeStamp, type } = aggregatedUserAction.event
|
|
71
|
-
|
|
73
|
+
const userActionEvent = {
|
|
72
74
|
eventType: 'UserAction',
|
|
73
75
|
timestamp: this.toEpoch(timeStamp),
|
|
74
76
|
action: type,
|
|
@@ -84,8 +86,12 @@ export class Aggregate extends AggregateBase {
|
|
|
84
86
|
if (canTrustTargetAttribute(field)) acc[targetAttrName(field)] = String(target[field]).trim().slice(0, 128)
|
|
85
87
|
return acc
|
|
86
88
|
}, {})),
|
|
87
|
-
...aggregatedUserAction.nearestTargetFields
|
|
88
|
-
|
|
89
|
+
...aggregatedUserAction.nearestTargetFields,
|
|
90
|
+
...(aggregatedUserAction.deadClick && { deadClick: true }),
|
|
91
|
+
...(aggregatedUserAction.errorClick && { errorClick: true })
|
|
92
|
+
}
|
|
93
|
+
this.addEvent(userActionEvent)
|
|
94
|
+
this.#trackUserActionSM(userActionEvent)
|
|
89
95
|
|
|
90
96
|
/**
|
|
91
97
|
* Returns the original target field name with `target` prepended and camelCased
|
|
@@ -116,8 +122,11 @@ export class Aggregate extends AggregateBase {
|
|
|
116
122
|
|
|
117
123
|
registerHandler('ua', (evt) => {
|
|
118
124
|
/** the processor will return the previously aggregated event if it has been completed by processing the current event */
|
|
119
|
-
addUserAction(this
|
|
125
|
+
addUserAction(this.#userActionAggregator.process(evt, this.agentRef.init.user_actions.elementAttributes))
|
|
120
126
|
}, this.featureName, this.ee)
|
|
127
|
+
registerHandler('navChange', () => { this.#userActionAggregator.isLiveClick() }, this.featureName, this.ee)
|
|
128
|
+
registerHandler('uaXhr', () => { this.#userActionAggregator.isLiveClick() }, this.featureName, this.ee)
|
|
129
|
+
registerHandler('uaErr', () => this.#userActionAggregator.markAsErrorClick(), this.featureName, this.ee)
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
/**
|
|
@@ -296,7 +305,7 @@ export class Aggregate extends AggregateBase {
|
|
|
296
305
|
return Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp))
|
|
297
306
|
}
|
|
298
307
|
|
|
299
|
-
trackSupportabilityMetrics () {
|
|
308
|
+
#trackSupportabilityMetrics () {
|
|
300
309
|
/** track usage SMs to improve these experimental features */
|
|
301
310
|
const configPerfTag = 'Config/Performance/'
|
|
302
311
|
if (this.agentRef.init.performance.capture_marks) this.reportSupportabilityMetric(configPerfTag + 'CaptureMarks/Enabled')
|
|
@@ -306,4 +315,10 @@ export class Aggregate extends AggregateBase {
|
|
|
306
315
|
if (this.agentRef.init.performance.resources.first_party_domains?.length !== 0) this.reportSupportabilityMetric(configPerfTag + 'Resources/FirstPartyDomains/Changed')
|
|
307
316
|
if (this.agentRef.init.performance.resources.ignore_newrelic === false) this.reportSupportabilityMetric(configPerfTag + 'Resources/IgnoreNewrelic/Changed')
|
|
308
317
|
}
|
|
318
|
+
|
|
319
|
+
#trackUserActionSM (ua) {
|
|
320
|
+
if (ua.rageClick) this.reportSupportabilityMetric('UserAction/RageClick/Seen')
|
|
321
|
+
if (ua.deadClick) this.reportSupportabilityMetric('UserAction/DeadClick/Seen')
|
|
322
|
+
if (ua.errorClick) this.reportSupportabilityMetric('UserAction/ErrorClick/Seen')
|
|
323
|
+
}
|
|
309
324
|
}
|
|
@@ -6,15 +6,17 @@ import { RAGE_CLICK_THRESHOLD_EVENTS, RAGE_CLICK_THRESHOLD_MS } from '../../cons
|
|
|
6
6
|
import { cleanURL } from '../../../../common/url/clean-url'
|
|
7
7
|
|
|
8
8
|
export class AggregatedUserAction {
|
|
9
|
-
constructor (evt,
|
|
9
|
+
constructor (evt, selectorInfo) {
|
|
10
10
|
this.event = evt
|
|
11
11
|
this.count = 1
|
|
12
12
|
this.originMs = Math.floor(evt.timeStamp)
|
|
13
13
|
this.relativeMs = [0]
|
|
14
|
-
this.selectorPath =
|
|
14
|
+
this.selectorPath = selectorInfo.path
|
|
15
15
|
this.rageClick = undefined
|
|
16
|
-
this.nearestTargetFields =
|
|
16
|
+
this.nearestTargetFields = selectorInfo.nearestFields
|
|
17
17
|
this.currentUrl = cleanURL('' + location)
|
|
18
|
+
this.deadClick = false
|
|
19
|
+
this.errorClick = false
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -2,14 +2,27 @@
|
|
|
2
2
|
* Copyright 2020-2025 New Relic, Inc. All rights reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import { OBSERVED_WINDOW_EVENTS } from '../../constants'
|
|
5
|
+
import { analyzeElemPath } from '../../../../common/dom/selector-path'
|
|
6
|
+
import { FRUSTRATION_TIMEOUT_MS, OBSERVED_WINDOW_EVENTS } from '../../constants'
|
|
7
7
|
import { AggregatedUserAction } from './aggregated-user-action'
|
|
8
|
+
import { Timer } from '../../../../common/timer/timer'
|
|
9
|
+
import { gosNREUMOriginals } from '../../../../common/window/nreum'
|
|
8
10
|
|
|
9
11
|
export class UserActionsAggregator {
|
|
10
12
|
/** @type {AggregatedUserAction=} */
|
|
11
13
|
#aggregationEvent = undefined
|
|
12
14
|
#aggregationKey = ''
|
|
15
|
+
#ufEnabled = false
|
|
16
|
+
#deadClickTimer = undefined
|
|
17
|
+
#domObserver = undefined
|
|
18
|
+
#errorClickTimer = undefined
|
|
19
|
+
|
|
20
|
+
constructor (userFrustrationsEnabled) {
|
|
21
|
+
if (userFrustrationsEnabled && gosNREUMOriginals().o.MO) {
|
|
22
|
+
this.#domObserver = new MutationObserver(this.isLiveClick.bind(this))
|
|
23
|
+
this.#ufEnabled = true
|
|
24
|
+
}
|
|
25
|
+
}
|
|
13
26
|
|
|
14
27
|
get aggregationEvent () {
|
|
15
28
|
// if this is accessed externally, we need to be done aggregating on it
|
|
@@ -28,40 +41,83 @@ export class UserActionsAggregator {
|
|
|
28
41
|
*/
|
|
29
42
|
process (evt, targetFields) {
|
|
30
43
|
if (!evt) return
|
|
31
|
-
const
|
|
32
|
-
const
|
|
44
|
+
const targetElem = OBSERVED_WINDOW_EVENTS.includes(evt.type) ? window : evt.target
|
|
45
|
+
const selectorInfo = analyzeElemPath(targetElem, targetFields)
|
|
46
|
+
|
|
47
|
+
// if selectorInfo.path is undefined, aggregation will be skipped for this event
|
|
48
|
+
const aggregationKey = getAggregationKey(evt, selectorInfo.path)
|
|
33
49
|
if (!!aggregationKey && aggregationKey === this.#aggregationKey) {
|
|
34
50
|
// an aggregation exists already, so lets just continue to increment
|
|
35
51
|
this.#aggregationEvent.aggregate(evt)
|
|
36
52
|
} else {
|
|
37
53
|
// return the prev existing one (if there is one)
|
|
38
54
|
const finishedEvent = this.#aggregationEvent
|
|
39
|
-
|
|
55
|
+
if (this.#ufEnabled) {
|
|
56
|
+
this.#deadClickCleanup()
|
|
57
|
+
this.#errorClickCleanup()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// then start new event aggregation
|
|
40
61
|
this.#aggregationKey = aggregationKey
|
|
41
|
-
this.#aggregationEvent = new AggregatedUserAction(evt,
|
|
62
|
+
this.#aggregationEvent = new AggregatedUserAction(evt, selectorInfo)
|
|
63
|
+
if (this.#ufEnabled && evt.type === 'click' && (selectorInfo.hasButton || selectorInfo.hasLink)) {
|
|
64
|
+
this.#deadClickSetup(this.#aggregationEvent)
|
|
65
|
+
this.#errorClickSetup()
|
|
66
|
+
}
|
|
42
67
|
return finishedEvent
|
|
43
68
|
}
|
|
44
69
|
}
|
|
45
|
-
}
|
|
46
70
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
markAsErrorClick () {
|
|
72
|
+
if (this.#aggregationEvent && this.#errorClickTimer) {
|
|
73
|
+
this.#aggregationEvent.errorClick = true
|
|
74
|
+
this.#errorClickCleanup()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#errorClickSetup () {
|
|
79
|
+
this.#errorClickTimer = new Timer({
|
|
80
|
+
onEnd: () => {
|
|
81
|
+
this.#errorClickCleanup()
|
|
82
|
+
}
|
|
83
|
+
}, FRUSTRATION_TIMEOUT_MS)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#errorClickCleanup () {
|
|
87
|
+
this.#errorClickTimer?.clear()
|
|
88
|
+
this.#errorClickTimer = undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#deadClickSetup (userAction) {
|
|
92
|
+
if (this.#isEvaluatingDeadClick() || !this.#domObserver) return
|
|
93
|
+
|
|
94
|
+
this.#domObserver.observe(document, {
|
|
95
|
+
attributes: true,
|
|
96
|
+
characterData: true,
|
|
97
|
+
childList: true,
|
|
98
|
+
subtree: true
|
|
99
|
+
})
|
|
100
|
+
this.#deadClickTimer = new Timer({
|
|
101
|
+
onEnd: () => {
|
|
102
|
+
userAction.deadClick = true
|
|
103
|
+
this.#deadClickCleanup()
|
|
104
|
+
}
|
|
105
|
+
}, FRUSTRATION_TIMEOUT_MS)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#deadClickCleanup () {
|
|
109
|
+
this.#domObserver?.disconnect()
|
|
110
|
+
this.#deadClickTimer?.clear()
|
|
111
|
+
this.#deadClickTimer = undefined
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#isEvaluatingDeadClick () {
|
|
115
|
+
return this.#deadClickTimer !== undefined
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
isLiveClick () {
|
|
119
|
+
if (this.#isEvaluatingDeadClick()) this.#deadClickCleanup()
|
|
62
120
|
}
|
|
63
|
-
// if STILL no selectorPath, it will return undefined which will skip aggregation for this event
|
|
64
|
-
return { selectorPath, nearestTargetFields }
|
|
65
121
|
}
|
|
66
122
|
|
|
67
123
|
/**
|
|
@@ -12,6 +12,8 @@ export const OBSERVED_WINDOW_EVENTS = ['focus', 'blur']
|
|
|
12
12
|
export const RAGE_CLICK_THRESHOLD_EVENTS = 4
|
|
13
13
|
export const RAGE_CLICK_THRESHOLD_MS = 1000
|
|
14
14
|
|
|
15
|
+
export const FRUSTRATION_TIMEOUT_MS = 2000
|
|
16
|
+
|
|
15
17
|
export const RESERVED_EVENT_TYPES = ['PageAction', 'UserAction', 'BrowserPerformance']
|
|
16
18
|
|
|
17
19
|
export const FEATURE_FLAGS = {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { globalScope, isBrowserScope } from '../../../common/constants/runtime'
|
|
7
7
|
import { handle } from '../../../common/event-emitter/handle'
|
|
8
|
-
import { windowAddEventListener } from '../../../common/event-listener/event-listener-opts'
|
|
8
|
+
import { eventListenerOpts, windowAddEventListener } from '../../../common/event-listener/event-listener-opts'
|
|
9
9
|
import { debounce } from '../../../common/util/invoke'
|
|
10
10
|
import { setupAddPageActionAPI } from '../../../loaders/api/addPageAction'
|
|
11
11
|
import { setupFinishedAPI } from '../../../loaders/api/finished'
|
|
@@ -14,6 +14,12 @@ import { setupRegisterAPI } from '../../../loaders/api/register'
|
|
|
14
14
|
import { setupMeasureAPI } from '../../../loaders/api/measure'
|
|
15
15
|
import { InstrumentBase } from '../../utils/instrument-base'
|
|
16
16
|
import { FEATURE_NAME, OBSERVED_EVENTS, OBSERVED_WINDOW_EVENTS } from '../constants'
|
|
17
|
+
import { FEATURE_NAMES } from '../../../loaders/features/features'
|
|
18
|
+
import { wrapHistory } from '../../../common/wrap/wrap-history'
|
|
19
|
+
import { wrapFetch } from '../../../common/wrap/wrap-fetch'
|
|
20
|
+
import { wrapXhr } from '../../../common/wrap/wrap-xhr'
|
|
21
|
+
import { parseUrl } from '../../../common/url/parse-url'
|
|
22
|
+
import { extractUrl } from '../../../common/url/extract-url'
|
|
17
23
|
|
|
18
24
|
export class Instrument extends InstrumentBase {
|
|
19
25
|
static featureName = FEATURE_NAME
|
|
@@ -55,6 +61,50 @@ export class Instrument extends InstrumentBase {
|
|
|
55
61
|
})
|
|
56
62
|
observer.observe({ type: 'resource', buffered: true })
|
|
57
63
|
}
|
|
64
|
+
|
|
65
|
+
const historyEE = wrapHistory(this.ee)
|
|
66
|
+
historyEE.on('pushState-end', navigationChange)
|
|
67
|
+
historyEE.on('replaceState-end', navigationChange)
|
|
68
|
+
window.addEventListener('hashchange', navigationChange, eventListenerOpts(true, this.removeOnAbort?.signal))
|
|
69
|
+
window.addEventListener('popstate', navigationChange, eventListenerOpts(true, this.removeOnAbort?.signal))
|
|
70
|
+
function navigationChange () {
|
|
71
|
+
historyEE.emit('navChange')
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
this.removeOnAbort = new AbortController()
|
|
77
|
+
} catch (e) {}
|
|
78
|
+
|
|
79
|
+
this.abortHandler = () => {
|
|
80
|
+
this.removeOnAbort?.abort()
|
|
81
|
+
this.abortHandler = undefined // weakly allow this abort op to run only once
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
globalScope.addEventListener('error', () => {
|
|
85
|
+
handle('uaErr', [], undefined, FEATURE_NAMES.genericEvents, this.ee)
|
|
86
|
+
}, eventListenerOpts(false, this.removeOnAbort?.signal))
|
|
87
|
+
|
|
88
|
+
wrapFetch(this.ee)
|
|
89
|
+
wrapXhr(this.ee)
|
|
90
|
+
this.ee.on('open-xhr-start', (args, xhr) => {
|
|
91
|
+
if (!isInternalTraffic(args[1])) {
|
|
92
|
+
xhr.addEventListener('readystatechange', () => {
|
|
93
|
+
if (xhr.readyState === 2) { // HEADERS_RECEIVED
|
|
94
|
+
handle('uaXhr', [], undefined, FEATURE_NAMES.genericEvents, this.ee)
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
this.ee.on('fetch-start', (fetchArguments) => {
|
|
100
|
+
if (fetchArguments.length >= 1 && !isInternalTraffic(extractUrl(fetchArguments[0]))) {
|
|
101
|
+
handle('uaXhr', [], undefined, FEATURE_NAMES.genericEvents, this.ee)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
function isInternalTraffic (url) {
|
|
106
|
+
const parsedUrl = parseUrl(url)
|
|
107
|
+
return agentRef.beacons.includes(parsedUrl.hostname + ':' + parsedUrl.port)
|
|
58
108
|
}
|
|
59
109
|
|
|
60
110
|
/** If any of the sources are active, import the aggregator. otherwise deregister */
|
|
@@ -28,6 +28,7 @@ const FRAMEWORKS = {
|
|
|
28
28
|
JQUERY: 'Jquery',
|
|
29
29
|
MOOTOOLS: 'MooTools',
|
|
30
30
|
QWIK: 'Qwik',
|
|
31
|
+
FLUTTER: 'Flutter',
|
|
31
32
|
|
|
32
33
|
ELECTRON: 'Electron'
|
|
33
34
|
}
|
|
@@ -71,6 +72,7 @@ export function getFrameworks () {
|
|
|
71
72
|
if (Object.prototype.hasOwnProperty.call(window, 'jQuery')) frameworks.push(FRAMEWORKS.JQUERY)
|
|
72
73
|
if (Object.prototype.hasOwnProperty.call(window, 'MooTools')) frameworks.push(FRAMEWORKS.MOOTOOLS)
|
|
73
74
|
if (Object.prototype.hasOwnProperty.call(window, 'qwikevents')) frameworks.push(FRAMEWORKS.QWIK)
|
|
75
|
+
if (Object.hasOwn(window, '_flutter')) frameworks.push(FRAMEWORKS.FLUTTER)
|
|
74
76
|
|
|
75
77
|
if (detectElectron()) frameworks.push(FRAMEWORKS.ELECTRON)
|
|
76
78
|
} catch (err) {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2020-2025 New Relic, Inc. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function evaluateHarvestMetadata (pageMetadata) {
|
|
7
|
+
try {
|
|
8
|
+
const supportabilityTags = []
|
|
9
|
+
|
|
10
|
+
// Report SM like... audit/<feature_name>/<hasReplay|hasTrace|hasError>/<true|false>/<negative|positive>
|
|
11
|
+
const formTag = (...strings) => strings.join('/')
|
|
12
|
+
|
|
13
|
+
// Track if replay/trace/error harvests actually occurred (key only exists when harvested)
|
|
14
|
+
function evaluateTag (feature, flag, hasFlag, hasHarvest) {
|
|
15
|
+
const AUDIT = 'audit'
|
|
16
|
+
if (hasFlag) {
|
|
17
|
+
// False positive: flag true, but no harvest
|
|
18
|
+
if (!hasHarvest) supportabilityTags.push(formTag(AUDIT, feature, flag, 'false', 'positive'))
|
|
19
|
+
// True positive (correct)
|
|
20
|
+
else supportabilityTags.push(formTag(AUDIT, feature, flag, 'true', 'positive'))
|
|
21
|
+
} else {
|
|
22
|
+
// False negative: flag false, but harvest occurred
|
|
23
|
+
if (hasHarvest) supportabilityTags.push(formTag(AUDIT, feature, flag, 'false', 'negative'))
|
|
24
|
+
// True negative (correct)
|
|
25
|
+
else supportabilityTags.push(formTag(AUDIT, feature, flag, 'true', 'negative'))
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (pageMetadata.page_view_event) {
|
|
30
|
+
evaluateTag('page_view', 'hasReplay', pageMetadata.page_view_event.hasReplay, !!pageMetadata.session_replay)
|
|
31
|
+
evaluateTag('page_view', 'hasTrace', pageMetadata.page_view_event.hasTrace, !!pageMetadata.session_trace)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (pageMetadata.session_replay) {
|
|
35
|
+
evaluateTag('session_replay', 'hasError', pageMetadata.session_replay.hasError, !!pageMetadata.jserrors)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return supportabilityTags
|
|
39
|
+
} catch (err) {
|
|
40
|
+
return []
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -11,6 +11,7 @@ import { windowAddEventListener } from '../../../common/event-listener/event-lis
|
|
|
11
11
|
import { isBrowserScope, isWorkerScope } from '../../../common/constants/runtime'
|
|
12
12
|
import { AggregateBase } from '../../utils/aggregate-base'
|
|
13
13
|
import { isIFrameWindow } from '../../../common/dom/iframe'
|
|
14
|
+
import { evaluateHarvestMetadata } from './harvest-metadata'
|
|
14
15
|
// import { WEBSOCKET_TAG } from '../../../common/wrap/wrap-websocket'
|
|
15
16
|
// import { handleWebsocketEvents } from './websocket-detection'
|
|
16
17
|
|
|
@@ -19,6 +20,15 @@ export class Aggregate extends AggregateBase {
|
|
|
19
20
|
constructor (agentRef) {
|
|
20
21
|
super(agentRef, FEATURE_NAME)
|
|
21
22
|
this.harvestOpts.aggregatorTypes = ['cm', 'sm'] // the types in EventAggregator this feature cares about
|
|
23
|
+
|
|
24
|
+
/** all the harvest metadata metrics need to be evaluated simulataneously at unload time so just temporarily buffer them and dont make SMs immediately from the data */
|
|
25
|
+
this.harvestMetadata = {}
|
|
26
|
+
this.harvestOpts.beforeUnload = () => {
|
|
27
|
+
evaluateHarvestMetadata(this.harvestMetadata).forEach(smTag => {
|
|
28
|
+
this.storeSupportabilityMetrics(smTag)
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
// This feature only harvests once per potential EoL of the page, which is handled by the central harvester.
|
|
23
33
|
|
|
24
34
|
// this must be read/stored synchronously, as the currentScript is removed from the DOM after this script is executed and this lookup will be void
|
|
@@ -126,6 +136,17 @@ export class Aggregate extends AggregateBase {
|
|
|
126
136
|
// handleWebsocketEvents(this.storeSupportabilityMetrics.bind(this), tag, ...args)
|
|
127
137
|
// }, this.featureName, this.ee)
|
|
128
138
|
// })
|
|
139
|
+
|
|
140
|
+
/** all the harvest metadata metrics need to be evaluated simulataneously at unload time so just temporarily buffer them and dont make SMs immediately from the data */
|
|
141
|
+
registerHandler('harvest-metadata', (harvestMetadataObject = {}) => {
|
|
142
|
+
try {
|
|
143
|
+
Object.keys(harvestMetadataObject).forEach(key => {
|
|
144
|
+
Object.assign(this.harvestMetadata[key] ??= {}, harvestMetadataObject[key])
|
|
145
|
+
})
|
|
146
|
+
} catch (e) {
|
|
147
|
+
// failed to merge harvest metadata... ignore
|
|
148
|
+
}
|
|
149
|
+
}, this.featureName, this.ee)
|
|
129
150
|
}
|
|
130
151
|
|
|
131
152
|
eachSessionChecks () {
|
|
@@ -136,7 +136,7 @@ export class Aggregate extends AggregateBase {
|
|
|
136
136
|
if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(TRIGGERS.SWITCH_TO_FULL, this.mode) // off --> full
|
|
137
137
|
this.syncWithSessionManager({ sessionReplayMode: this.mode })
|
|
138
138
|
} else {
|
|
139
|
-
this.initializeRecording(MODE.FULL, true)
|
|
139
|
+
this.initializeRecording(MODE.FULL, true, TRIGGERS.SWITCH_TO_FULL)
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
@@ -57,7 +57,11 @@ export class Recorder {
|
|
|
57
57
|
this.stopRecording()
|
|
58
58
|
}, this.srFeatureName, this.ee)
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
/** If Agg is already drained before importing the recorder (likely deferred API call pattern),
|
|
61
|
+
* registerHandler wont do anything. Just set up the on listener directly */
|
|
62
|
+
const processReplayNode = (event, isCheckout) => { this.audit(event, isCheckout) }
|
|
63
|
+
if (this.srInstrument.featAggregate?.drained) this.ee.on(RRWEB_DATA_CHANNEL, processReplayNode)
|
|
64
|
+
else registerHandler(RRWEB_DATA_CHANNEL, processReplayNode, this.srFeatureName, this.ee)
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
get trigger () {
|
|
@@ -15,4 +15,5 @@ export function setupNoticeErrorAPI (agent) {
|
|
|
15
15
|
export function noticeError (err, customAttributes, agentRef, targetEntityGuid, timestamp = now()) {
|
|
16
16
|
if (typeof err === 'string') err = new Error(err)
|
|
17
17
|
handle('err', [err, timestamp, false, customAttributes, agentRef.runtime.isRecording, undefined, targetEntityGuid], undefined, FEATURE_NAMES.jserrors, agentRef.ee)
|
|
18
|
+
handle('uaErr', [], undefined, FEATURE_NAMES.genericEvents, agentRef.ee)
|
|
18
19
|
}
|
|
@@ -50,6 +50,7 @@ export function configure (agent, opts = {}, loaderType, forceDrain) {
|
|
|
50
50
|
internalTrafficList.push(updatedInit.proxy.assets)
|
|
51
51
|
}
|
|
52
52
|
if (updatedInit.proxy.beacon) internalTrafficList.push(updatedInit.proxy.beacon)
|
|
53
|
+
agent.beacons = [...internalTrafficList]
|
|
53
54
|
|
|
54
55
|
setTopLevelCallers(agent) // no need to set global APIs on newrelic obj more than once
|
|
55
56
|
addToNREUM('activatedFeatures', activatedFeatures)
|