@newrelic/browser-agent 1.256.1 → 1.258.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 +32 -0
- package/dist/cjs/common/config/state/configurable.js +8 -5
- package/dist/cjs/common/config/state/init.js +0 -2
- package/dist/cjs/common/config/state/runtime.js +10 -8
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/constants/runtime.js +8 -2
- package/dist/cjs/common/harvest/harvest.js +7 -5
- package/dist/cjs/common/session/constants.js +1 -0
- package/dist/cjs/common/session/session-entity.js +3 -0
- package/dist/cjs/common/timing/time-keeper.js +45 -9
- package/dist/cjs/common/vitals/time-to-first-byte.js +1 -1
- package/dist/cjs/common/vitals/vital-metric.js +1 -1
- package/dist/cjs/features/ajax/aggregate/chunk.js +50 -0
- package/dist/cjs/features/ajax/aggregate/index.js +131 -191
- package/dist/cjs/features/ajax/instrument/index.js +0 -3
- package/dist/cjs/features/jserrors/aggregate/index.js +26 -13
- package/dist/cjs/features/page_view_event/aggregate/index.js +3 -3
- package/dist/cjs/features/session_replay/aggregate/index.js +12 -5
- package/dist/cjs/features/session_replay/constants.js +2 -1
- package/dist/cjs/features/session_replay/instrument/index.js +15 -5
- package/dist/cjs/features/session_replay/shared/recorder.js +6 -3
- package/dist/cjs/features/session_replay/shared/utils.js +9 -8
- package/dist/cjs/features/session_trace/aggregate/index.js +3 -5
- package/dist/cjs/features/soft_navigations/aggregate/index.js +2 -2
- package/dist/cjs/features/spa/instrument/index.js +0 -2
- package/dist/cjs/features/utils/agent-session.js +1 -5
- package/dist/cjs/features/utils/instrument-base.js +11 -14
- package/dist/cjs/features/utils/nr1-debugger.js +27 -0
- package/dist/cjs/loaders/agent.js +4 -0
- package/dist/cjs/loaders/api/apiAsync.js +5 -4
- package/dist/esm/common/config/state/configurable.js +8 -5
- package/dist/esm/common/config/state/init.js +0 -2
- package/dist/esm/common/config/state/runtime.js +11 -9
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/constants/runtime.js +7 -1
- package/dist/esm/common/harvest/harvest.js +7 -5
- package/dist/esm/common/session/constants.js +1 -0
- package/dist/esm/common/session/session-entity.js +3 -0
- package/dist/esm/common/timing/time-keeper.js +46 -9
- package/dist/esm/common/vitals/time-to-first-byte.js +2 -2
- package/dist/esm/common/vitals/vital-metric.js +1 -1
- package/dist/esm/features/ajax/aggregate/chunk.js +43 -0
- package/dist/esm/features/ajax/aggregate/index.js +130 -191
- package/dist/esm/features/ajax/instrument/index.js +1 -4
- package/dist/esm/features/jserrors/aggregate/index.js +26 -13
- package/dist/esm/features/page_view_event/aggregate/index.js +4 -4
- package/dist/esm/features/session_replay/aggregate/index.js +12 -5
- package/dist/esm/features/session_replay/constants.js +2 -1
- package/dist/esm/features/session_replay/instrument/index.js +16 -6
- package/dist/esm/features/session_replay/shared/recorder.js +6 -3
- package/dist/esm/features/session_replay/shared/utils.js +9 -7
- package/dist/esm/features/session_trace/aggregate/index.js +3 -5
- package/dist/esm/features/soft_navigations/aggregate/index.js +2 -2
- package/dist/esm/features/spa/instrument/index.js +0 -2
- package/dist/esm/features/utils/agent-session.js +1 -5
- package/dist/esm/features/utils/instrument-base.js +11 -14
- package/dist/esm/features/utils/nr1-debugger.js +21 -0
- package/dist/esm/loaders/agent.js +4 -0
- package/dist/esm/loaders/api/apiAsync.js +5 -4
- package/dist/types/common/config/state/configurable.d.ts.map +1 -1
- package/dist/types/common/config/state/init.d.ts.map +1 -1
- package/dist/types/common/config/state/runtime.d.ts.map +1 -1
- package/dist/types/common/constants/runtime.d.ts +6 -1
- package/dist/types/common/constants/runtime.d.ts.map +1 -1
- package/dist/types/common/harvest/harvest.d.ts +1 -1
- package/dist/types/common/harvest/harvest.d.ts.map +1 -1
- package/dist/types/common/session/constants.d.ts +1 -0
- package/dist/types/common/session/session-entity.d.ts.map +1 -1
- package/dist/types/common/timing/time-keeper.d.ts +1 -1
- package/dist/types/common/timing/time-keeper.d.ts.map +1 -1
- package/dist/types/features/ajax/aggregate/chunk.d.ts +8 -0
- package/dist/types/features/ajax/aggregate/chunk.d.ts.map +1 -0
- package/dist/types/features/ajax/aggregate/index.d.ts +8 -6
- package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/ajax/instrument/index.d.ts +2 -2
- package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts +0 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +0 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/constants.d.ts +1 -0
- package/dist/types/features/session_replay/constants.d.ts.map +1 -1
- package/dist/types/features/session_replay/instrument/index.d.ts +1 -0
- package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder.d.ts +1 -0
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/utils.d.ts +5 -5
- package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -1
- package/dist/types/features/session_trace/aggregate/index.d.ts +8 -8
- package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/spa/instrument/index.d.ts.map +1 -1
- package/dist/types/features/utils/agent-session.d.ts.map +1 -1
- package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
- package/dist/types/features/utils/nr1-debugger.d.ts +2 -0
- package/dist/types/features/utils/nr1-debugger.d.ts.map +1 -0
- package/dist/types/loaders/agent.d.ts +5 -1
- package/dist/types/loaders/agent.d.ts.map +1 -1
- package/dist/types/loaders/api/apiAsync.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/config/state/configurable.js +9 -8
- package/src/common/config/state/init.js +0 -1
- package/src/common/config/state/runtime.js +12 -9
- package/src/common/constants/__mocks__/runtime.js +2 -0
- package/src/common/constants/runtime.js +6 -1
- package/src/common/drain/__mocks__/drain.js +2 -0
- package/src/common/harvest/harvest.js +8 -6
- package/src/common/session/constants.js +1 -0
- package/src/common/session/session-entity.js +2 -0
- package/src/common/timing/time-keeper.js +44 -10
- package/src/common/vitals/time-to-first-byte.js +2 -2
- package/src/common/vitals/vital-metric.js +1 -1
- package/src/common/window/__mocks__/load.js +3 -0
- package/src/features/ajax/aggregate/chunk.js +51 -0
- package/src/features/ajax/aggregate/index.js +128 -200
- package/src/features/ajax/instrument/index.js +1 -4
- package/src/features/jserrors/aggregate/index.js +28 -11
- package/src/features/page_view_event/aggregate/index.js +4 -4
- package/src/features/session_replay/aggregate/index.js +19 -7
- package/src/features/session_replay/constants.js +2 -1
- package/src/features/session_replay/instrument/index.js +16 -6
- package/src/features/session_replay/shared/__mocks__/utils.js +2 -0
- package/src/features/session_replay/shared/recorder.js +7 -4
- package/src/features/session_replay/shared/utils.js +9 -7
- package/src/features/session_trace/aggregate/index.js +3 -6
- package/src/features/soft_navigations/aggregate/index.js +2 -2
- package/src/features/spa/instrument/index.js +0 -3
- package/src/features/utils/__mocks__/agent-session.js +1 -0
- package/src/features/utils/__mocks__/feature-base.js +11 -0
- package/src/features/utils/agent-session.js +1 -7
- package/src/features/utils/instrument-base.js +11 -14
- package/src/features/utils/nr1-debugger.js +22 -0
- package/src/loaders/agent.js +4 -0
- package/src/loaders/api/apiAsync.js +5 -4
- package/dist/cjs/common/storage/first-party-cookies.js +0 -36
- package/dist/esm/common/storage/first-party-cookies.js +0 -29
- package/dist/types/common/storage/first-party-cookies.d.ts +0 -8
- package/dist/types/common/storage/first-party-cookies.d.ts.map +0 -1
- package/src/common/storage/first-party-cookies.js +0 -32
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { registerHandler } from '../../../common/event-emitter/register-handler'
|
|
6
6
|
import { stringify } from '../../../common/util/stringify'
|
|
7
|
-
import { nullable, numeric, getAddStringContext, addCustomAttributes } from '../../../common/serialize/bel-serializer'
|
|
8
7
|
import { handle } from '../../../common/event-emitter/handle'
|
|
9
8
|
import { getConfiguration, getInfo, getRuntime } from '../../../common/config/config'
|
|
10
9
|
import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
|
|
@@ -15,251 +14,180 @@ import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
|
|
|
15
14
|
import { AggregateBase } from '../../utils/aggregate-base'
|
|
16
15
|
import { parseGQL } from './gql'
|
|
17
16
|
import { getNREUMInitializedAgent } from '../../../common/window/nreum'
|
|
17
|
+
import Chunk from './chunk'
|
|
18
18
|
|
|
19
19
|
export class Aggregate extends AggregateBase {
|
|
20
20
|
static featureName = FEATURE_NAME
|
|
21
|
+
#agentInfo
|
|
22
|
+
#agentRuntime
|
|
23
|
+
#agentInit
|
|
24
|
+
|
|
21
25
|
constructor (agentIdentifier, aggregator) {
|
|
22
26
|
super(agentIdentifier, aggregator, FEATURE_NAME)
|
|
23
|
-
const agentInit = getConfiguration(agentIdentifier)
|
|
24
|
-
|
|
25
|
-
registerHandler('xhr', storeXhr, this.featureName, this.ee)
|
|
26
27
|
|
|
27
|
-
this
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
getPayload: prepareHarvest
|
|
31
|
-
}, this)
|
|
32
|
-
scheduler.startTimer(harvestTimeSeconds)
|
|
33
|
-
this.drain()
|
|
34
|
-
})
|
|
28
|
+
this.#agentInfo = getInfo(agentIdentifier)
|
|
29
|
+
this.#agentRuntime = getRuntime(agentIdentifier)
|
|
30
|
+
this.#agentInit = getConfiguration(agentIdentifier)
|
|
35
31
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
setDenyList(denyList)
|
|
32
|
+
const harvestTimeSeconds = this.#agentInit.ajax.harvestTimeSeconds || 10
|
|
33
|
+
this.MAX_PAYLOAD_SIZE = this.#agentInit.ajax.maxPayloadSize || 1000000
|
|
34
|
+
setDenyList(this.#agentRuntime.denyList)
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const harvestTimeSeconds = agentInit.ajax.harvestTimeSeconds || 10
|
|
46
|
-
const MAX_PAYLOAD_SIZE = agentInit.ajax.maxPayloadSize || 1000000
|
|
47
|
-
|
|
48
|
-
// Exposes these methods to browser test files -- future TO DO: can be removed once these fns are extracted from the constructor into class func
|
|
49
|
-
this.storeXhr = storeXhr
|
|
50
|
-
this.prepareHarvest = prepareHarvest
|
|
51
|
-
this.getStoredEvents = function () { return { ajaxEvents, spaAjaxEvents } }
|
|
36
|
+
this.ajaxEvents = []
|
|
37
|
+
this.spaAjaxEvents = {}
|
|
38
|
+
this.sentAjaxEvents = []
|
|
39
|
+
const classThis = this
|
|
52
40
|
|
|
53
41
|
// --- v Used by old spa feature
|
|
54
|
-
ee.on('interactionDone', (interaction, wasSaved) => {
|
|
55
|
-
if (!spaAjaxEvents[interaction.id]) return
|
|
42
|
+
this.ee.on('interactionDone', (interaction, wasSaved) => {
|
|
43
|
+
if (!this.spaAjaxEvents[interaction.id]) return
|
|
56
44
|
|
|
57
45
|
if (!wasSaved) { // if the ixn was saved, then its ajax reqs are part of the payload whereas if it was discarded, it should still be harvested in the ajax feature itself
|
|
58
|
-
spaAjaxEvents[interaction.id].forEach(
|
|
59
|
-
ajaxEvents.push(item)
|
|
60
|
-
})
|
|
46
|
+
this.spaAjaxEvents[interaction.id].forEach((item) => this.ajaxEvents.push(item))
|
|
61
47
|
}
|
|
62
|
-
delete spaAjaxEvents[interaction.id]
|
|
48
|
+
delete this.spaAjaxEvents[interaction.id]
|
|
63
49
|
})
|
|
64
50
|
// --- ^
|
|
65
51
|
// --- v Used by new soft nav
|
|
66
|
-
registerHandler('returnAjax', event => ajaxEvents.push(event), this.featureName, this.ee)
|
|
52
|
+
registerHandler('returnAjax', event => this.ajaxEvents.push(event), this.featureName, this.ee)
|
|
67
53
|
// --- ^
|
|
54
|
+
registerHandler('xhr', function () { // the EE-drain system not only switches "this" but also passes a new EventContext with info. Should consider platform refactor to another system which passes a mutable context around separately and predictably to avoid problems like this.
|
|
55
|
+
classThis.storeXhr(...arguments, this) // this switches the context back to the class instance while passing the NR context as an argument -- see "ctx" in storeXhr
|
|
56
|
+
}, this.featureName, this.ee)
|
|
68
57
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
hash = stringify([params.status, params.cat])
|
|
79
|
-
} else {
|
|
80
|
-
hash = stringify([params.status, params.host, params.pathname])
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const shouldCollect = shouldCollectEvent(params)
|
|
84
|
-
const ajaxMetricDenyListEnabled = agentInit.feature_flags?.includes('ajax_metrics_deny_list')
|
|
85
|
-
|
|
86
|
-
// store as metric
|
|
87
|
-
if (shouldCollect || !ajaxMetricDenyListEnabled) {
|
|
88
|
-
aggregator.store('xhr', hash, params, metrics)
|
|
89
|
-
}
|
|
58
|
+
this.waitForFlags(([])).then(() => {
|
|
59
|
+
const scheduler = new HarvestScheduler('events', {
|
|
60
|
+
onFinished: this.onEventsHarvestFinished.bind(this),
|
|
61
|
+
getPayload: this.prepareHarvest.bind(this)
|
|
62
|
+
}, this)
|
|
63
|
+
scheduler.startTimer(harvestTimeSeconds)
|
|
64
|
+
this.drain()
|
|
65
|
+
})
|
|
66
|
+
}
|
|
90
67
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// This doesn't make a distinction if the same-domain request is going to a different port or path...
|
|
94
|
-
handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Events/Excluded/Agent'], undefined, FEATURE_NAMES.metrics, ee)
|
|
68
|
+
storeXhr (params, metrics, startTime, endTime, type, ctx) {
|
|
69
|
+
metrics.time = startTime
|
|
95
70
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
71
|
+
// send to session traces
|
|
72
|
+
let hash
|
|
73
|
+
if (params.cat) {
|
|
74
|
+
hash = stringify([params.status, params.cat])
|
|
75
|
+
} else {
|
|
76
|
+
hash = stringify([params.status, params.host, params.pathname])
|
|
77
|
+
}
|
|
101
78
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return
|
|
107
|
-
}
|
|
79
|
+
const shouldCollect = shouldCollectEvent(params)
|
|
80
|
+
const shouldOmitAjaxMetrics = this.#agentInit.feature_flags?.includes('ajax_metrics_deny_list')
|
|
108
81
|
|
|
109
|
-
|
|
82
|
+
// store for timeslice metric (harvested by jserrors feature)
|
|
83
|
+
if (shouldCollect || !shouldOmitAjaxMetrics) {
|
|
84
|
+
this.aggregator.store('xhr', hash, params, metrics)
|
|
85
|
+
}
|
|
110
86
|
|
|
111
|
-
|
|
87
|
+
if (!shouldCollect) {
|
|
88
|
+
if (params.hostname === this.#agentInfo.errorBeacon || (this.#agentInit.proxy?.beacon && params.hostname === this.#agentInit.proxy.beacon)) {
|
|
89
|
+
// This doesn't make a distinction if the same-domain request is going to a different port or path...
|
|
90
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Events/Excluded/Agent'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
112
91
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
domain: params.host,
|
|
117
|
-
path: params.pathname,
|
|
118
|
-
requestSize: metrics.txSize,
|
|
119
|
-
responseSize: metrics.rxSize,
|
|
120
|
-
type,
|
|
121
|
-
startTime,
|
|
122
|
-
endTime,
|
|
123
|
-
callbackDuration: metrics.cbTime
|
|
124
|
-
}
|
|
92
|
+
if (shouldOmitAjaxMetrics) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Metrics/Excluded/Agent'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
93
|
+
} else {
|
|
94
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Events/Excluded/App'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
125
95
|
|
|
126
|
-
|
|
127
|
-
event.spanId = xhrContext.dt.spanId
|
|
128
|
-
event.traceId = xhrContext.dt.traceId
|
|
129
|
-
event.spanTimestamp = agentRuntime.timeKeeper.correctAbsoluteTimestamp(xhrContext.dt.timestamp)
|
|
96
|
+
if (shouldOmitAjaxMetrics) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Metrics/Excluded/App'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
130
97
|
}
|
|
98
|
+
return // do not send this ajax as an event
|
|
99
|
+
}
|
|
131
100
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
101
|
+
handle('bstXhrAgg', ['xhr', hash, params, metrics], undefined, FEATURE_NAMES.sessionTrace, this.ee) // have trace feature harvest AjaxNode
|
|
102
|
+
|
|
103
|
+
const event = {
|
|
104
|
+
method: params.method,
|
|
105
|
+
status: params.status,
|
|
106
|
+
domain: params.host,
|
|
107
|
+
path: params.pathname,
|
|
108
|
+
requestSize: metrics.txSize,
|
|
109
|
+
responseSize: metrics.rxSize,
|
|
110
|
+
type,
|
|
111
|
+
startTime,
|
|
112
|
+
endTime,
|
|
113
|
+
callbackDuration: metrics.cbTime
|
|
114
|
+
}
|
|
138
115
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const interactionId = this.spaNode.interaction.id
|
|
144
|
-
spaAjaxEvents[interactionId] = spaAjaxEvents[interactionId] || []
|
|
145
|
-
spaAjaxEvents[interactionId].push(event)
|
|
146
|
-
} else {
|
|
147
|
-
ajaxEvents.push(event)
|
|
148
|
-
}
|
|
116
|
+
if (ctx.dt) {
|
|
117
|
+
event.spanId = ctx.dt.spanId
|
|
118
|
+
event.traceId = ctx.dt.traceId
|
|
119
|
+
event.spanTimestamp = this.#agentRuntime.timeKeeper.correctAbsoluteTimestamp(ctx.dt.timestamp)
|
|
149
120
|
}
|
|
150
121
|
|
|
151
|
-
|
|
152
|
-
|
|
122
|
+
// parsed from the AJAX body, looking for operationName param & parsing query for operationType
|
|
123
|
+
event.gql = params.gql = parseGQL({
|
|
124
|
+
body: ctx.body,
|
|
125
|
+
query: ctx.parsedOrigin?.search
|
|
126
|
+
})
|
|
127
|
+
if (event.gql) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
128
|
+
|
|
129
|
+
const softNavInUse = Boolean(getNREUMInitializedAgent(this.agentIdentifier)?.features?.[FEATURE_NAMES.softNav])
|
|
130
|
+
if (softNavInUse) { // For newer soft nav (when running), pass the event to it for evaluation -- either part of an interaction or is given back
|
|
131
|
+
handle('ajax', [event], undefined, FEATURE_NAMES.softNav, this.ee)
|
|
132
|
+
} else if (ctx.spaNode) { // For old spa (when running), if the ajax happened inside an interaction, hold it until the interaction finishes
|
|
133
|
+
const interactionId = ctx.spaNode.interaction.id
|
|
134
|
+
this.spaAjaxEvents[interactionId] = this.spaAjaxEvents[interactionId] || []
|
|
135
|
+
this.spaAjaxEvents[interactionId].push(event)
|
|
136
|
+
} else {
|
|
137
|
+
this.ajaxEvents.push(event)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
153
140
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
141
|
+
prepareHarvest (options) {
|
|
142
|
+
options = options || {}
|
|
143
|
+
if (this.ajaxEvents.length === 0) return null
|
|
157
144
|
|
|
158
|
-
|
|
145
|
+
const payload = this.#getPayload(this.ajaxEvents)
|
|
146
|
+
const payloadObjs = []
|
|
159
147
|
|
|
160
|
-
|
|
161
|
-
for (var i = 0; i < payload.length; i++) {
|
|
162
|
-
payloadObjs.push({ body: { e: payload[i] } })
|
|
163
|
-
}
|
|
148
|
+
for (let i = 0; i < payload.length; i++) payloadObjs.push({ body: { e: payload[i] } })
|
|
164
149
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
150
|
+
if (options.retry) this.sentAjaxEvents = this.ajaxEvents
|
|
151
|
+
this.ajaxEvents = []
|
|
168
152
|
|
|
169
|
-
|
|
153
|
+
return payloadObjs
|
|
154
|
+
}
|
|
170
155
|
|
|
171
|
-
|
|
156
|
+
onEventsHarvestFinished (result) {
|
|
157
|
+
if (result.retry && this.sentAjaxEvents.length > 0) {
|
|
158
|
+
this.ajaxEvents.unshift(...this.sentAjaxEvents)
|
|
159
|
+
this.sentAjaxEvents = []
|
|
172
160
|
}
|
|
161
|
+
}
|
|
173
162
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
because if it's length 1 and still too big for the maxPayloadSize
|
|
187
|
-
it cant get any smaller and we dont want to recurse forever */
|
|
188
|
-
tooBig = true
|
|
189
|
-
break
|
|
190
|
-
}
|
|
191
|
-
} else {
|
|
192
|
-
payload.push(currentChunk.payload)
|
|
163
|
+
#getPayload (events, numberOfChunks) {
|
|
164
|
+
numberOfChunks = numberOfChunks || 1
|
|
165
|
+
const payload = []
|
|
166
|
+
const chunkSize = events.length / numberOfChunks
|
|
167
|
+
const eventChunks = splitChunks.call(this, events, chunkSize)
|
|
168
|
+
let tooBig = false
|
|
169
|
+
for (let i = 0; i < eventChunks.length; i++) {
|
|
170
|
+
const currentChunk = eventChunks[i]
|
|
171
|
+
if (currentChunk.tooBig) {
|
|
172
|
+
if (currentChunk.events.length > 1) {
|
|
173
|
+
tooBig = true
|
|
174
|
+
break // if the payload is too big BUT is made of more than 1 event, we can split it down again
|
|
193
175
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function onEventsHarvestFinished (result) {
|
|
200
|
-
if (result.retry && sentAjaxEvents.length > 0) {
|
|
201
|
-
ajaxEvents.unshift(...sentAjaxEvents)
|
|
202
|
-
sentAjaxEvents = []
|
|
176
|
+
// Otherwise, if it consists of one sole event, we do not send it (discarded) since we cannot break it apart any further.
|
|
177
|
+
} else {
|
|
178
|
+
payload.push(currentChunk.payload)
|
|
203
179
|
}
|
|
204
180
|
}
|
|
181
|
+
// Check if the current payload string is too big, if so then run getPayload again with more buckets.
|
|
182
|
+
return tooBig ? this.#getPayload(events, ++numberOfChunks) : payload
|
|
205
183
|
|
|
206
184
|
function splitChunks (arr, chunkSize) {
|
|
207
185
|
chunkSize = chunkSize || arr.length
|
|
208
|
-
|
|
209
|
-
for (
|
|
210
|
-
chunks.push(new Chunk(arr.slice(i, i + chunkSize)))
|
|
186
|
+
const chunks = []
|
|
187
|
+
for (let i = 0, len = arr.length; i < len; i += chunkSize) {
|
|
188
|
+
chunks.push(new Chunk(arr.slice(i, i + chunkSize), this))
|
|
211
189
|
}
|
|
212
190
|
return chunks
|
|
213
191
|
}
|
|
214
|
-
|
|
215
|
-
function Chunk (events) {
|
|
216
|
-
this.addString = getAddStringContext(agentIdentifier) // pass agentIdentifier here
|
|
217
|
-
this.events = events
|
|
218
|
-
this.payload = 'bel.7;'
|
|
219
|
-
|
|
220
|
-
for (var i = 0; i < events.length; i++) {
|
|
221
|
-
var event = events[i]
|
|
222
|
-
|
|
223
|
-
var fields = [
|
|
224
|
-
numeric(event.startTime),
|
|
225
|
-
numeric(event.endTime - event.startTime),
|
|
226
|
-
numeric(0), // callbackEnd
|
|
227
|
-
numeric(0), // no callbackDuration for non-SPA events
|
|
228
|
-
this.addString(event.method),
|
|
229
|
-
numeric(event.status),
|
|
230
|
-
this.addString(event.domain),
|
|
231
|
-
this.addString(event.path),
|
|
232
|
-
numeric(event.requestSize),
|
|
233
|
-
numeric(event.responseSize),
|
|
234
|
-
event.type === 'fetch' ? 1 : '',
|
|
235
|
-
this.addString(0), // nodeId
|
|
236
|
-
nullable(event.spanId, this.addString, true) + // guid
|
|
237
|
-
nullable(event.traceId, this.addString, true) + // traceId
|
|
238
|
-
nullable(event.spanTimestamp, numeric, false) // timestamp
|
|
239
|
-
]
|
|
240
|
-
|
|
241
|
-
var insert = '2,'
|
|
242
|
-
|
|
243
|
-
// add custom attributes
|
|
244
|
-
// gql decorators are added as custom attributes to alleviate need for new BEL schema
|
|
245
|
-
var attrParts = addCustomAttributes({ ...(getInfo(agentIdentifier).jsAttributes || {}), ...(event.gql || {}) }, this.addString)
|
|
246
|
-
fields.unshift(numeric(attrParts.length))
|
|
247
|
-
|
|
248
|
-
insert += fields.join(',')
|
|
249
|
-
|
|
250
|
-
if (attrParts && attrParts.length > 0) {
|
|
251
|
-
insert += ';' + attrParts.join(';')
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if ((i + 1) < events.length) insert += ';'
|
|
255
|
-
|
|
256
|
-
this.payload += insert
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
this.tooBig = function (maxPayloadSize) {
|
|
260
|
-
maxPayloadSize = maxPayloadSize || MAX_PAYLOAD_SIZE
|
|
261
|
-
return this.payload.length * 2 > maxPayloadSize
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
192
|
}
|
|
265
193
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Copyright 2020 New Relic Corporation. All rights reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
|
-
import { originals, getLoaderConfig
|
|
5
|
+
import { originals, getLoaderConfig } from '../../../common/config/config'
|
|
6
6
|
import { handle } from '../../../common/event-emitter/handle'
|
|
7
7
|
import { id } from '../../../common/ids/id'
|
|
8
8
|
import { ffVersion, globalScope, isBrowserScope } from '../../../common/constants/runtime'
|
|
@@ -29,9 +29,6 @@ export class Instrument extends InstrumentBase {
|
|
|
29
29
|
constructor (agentIdentifier, aggregator, auto = true) {
|
|
30
30
|
super(agentIdentifier, aggregator, FEATURE_NAME, auto)
|
|
31
31
|
|
|
32
|
-
// Very unlikely, but in case the existing XMLHttpRequest.prototype object on the page couldn't be wrapped.
|
|
33
|
-
if (!getRuntime(agentIdentifier).xhrWrappable) return
|
|
34
|
-
|
|
35
32
|
this.dt = new DT(agentIdentifier)
|
|
36
33
|
|
|
37
34
|
this.handler = (type, args, ctx, group) => handle(type, args, ctx, group, this.ee)
|
|
@@ -38,13 +38,10 @@ export class Aggregate extends AggregateBase {
|
|
|
38
38
|
this.bufferedErrorsUnderSpa = {}
|
|
39
39
|
this.currentBody = undefined
|
|
40
40
|
this.errorOnPage = false
|
|
41
|
-
this.replayAborted = false
|
|
42
41
|
|
|
43
42
|
// this will need to change to match whatever ee we use in the instrument
|
|
44
43
|
this.ee.on('interactionDone', (interaction, wasSaved) => this.onInteractionDone(interaction, wasSaved))
|
|
45
44
|
|
|
46
|
-
this.ee.on('REPLAY_ABORTED', () => { this.replayAborted = true })
|
|
47
|
-
|
|
48
45
|
register('err', (...args) => this.storeError(...args), this.featureName, this.ee)
|
|
49
46
|
register('ierr', (...args) => this.storeError(...args), this.featureName, this.ee)
|
|
50
47
|
register('softNavFlush', (interactionId, wasFinished, softNavAttrs) =>
|
|
@@ -82,16 +79,13 @@ export class Aggregate extends AggregateBase {
|
|
|
82
79
|
}
|
|
83
80
|
|
|
84
81
|
if (body && body.err && body.err.length) {
|
|
85
|
-
|
|
86
|
-
body.err.forEach((e) => {
|
|
87
|
-
delete e.params?.hasReplay
|
|
88
|
-
})
|
|
89
|
-
}
|
|
82
|
+
this.#runCrossFeatureChecks(body.err)
|
|
90
83
|
if (!this.errorOnPage) {
|
|
91
84
|
payload.qs.pve = '1'
|
|
92
85
|
this.errorOnPage = true
|
|
93
86
|
}
|
|
94
87
|
}
|
|
88
|
+
|
|
95
89
|
return payload
|
|
96
90
|
}
|
|
97
91
|
|
|
@@ -172,13 +166,14 @@ export class Aggregate extends AggregateBase {
|
|
|
172
166
|
// Do not modify the name ('errorGroup') of params without DEM approval!
|
|
173
167
|
if (filterOutput?.group) params.errorGroup = filterOutput.group
|
|
174
168
|
|
|
169
|
+
if (hasReplay) params.hasReplay = hasReplay
|
|
175
170
|
/**
|
|
176
171
|
* The bucketHash is different from the params.stackHash because the params.stackHash is based on the canonicalized
|
|
177
172
|
* stack trace and is used downstream in NR1 to attempt to group the same errors across different browsers. However,
|
|
178
173
|
* the canonical stack trace excludes items like the column number increasing the hit-rate of different errors potentially
|
|
179
174
|
* bucketing and ultimately resulting in the loss of data in NR1.
|
|
180
175
|
*/
|
|
181
|
-
var bucketHash = stringHashCode(`${stackInfo.name}_${stackInfo.message}_${stackInfo.stackString}`)
|
|
176
|
+
var bucketHash = stringHashCode(`${stackInfo.name}_${stackInfo.message}_${stackInfo.stackString}_${params.hasReplay ? 1 : 0}`)
|
|
182
177
|
|
|
183
178
|
if (!this.stackReported[bucketHash]) {
|
|
184
179
|
this.stackReported[bucketHash] = true
|
|
@@ -199,9 +194,8 @@ export class Aggregate extends AggregateBase {
|
|
|
199
194
|
this.pageviewReported[bucketHash] = true
|
|
200
195
|
}
|
|
201
196
|
|
|
202
|
-
if (hasReplay && !this.replayAborted) params.hasReplay = hasReplay
|
|
203
197
|
params.firstOccurrenceTimestamp = this.observedAt[bucketHash]
|
|
204
|
-
params.timestamp =
|
|
198
|
+
params.timestamp = agentRuntime.timeKeeper.convertRelativeTimestamp(time)
|
|
205
199
|
|
|
206
200
|
var type = internal ? 'ierr' : 'err'
|
|
207
201
|
var newMetrics = { time }
|
|
@@ -296,4 +290,27 @@ export class Aggregate extends AggregateBase {
|
|
|
296
290
|
)
|
|
297
291
|
delete this.bufferedErrorsUnderSpa[interactionId] // wipe the list of jserrors so they aren't duplicated by another call to the same id
|
|
298
292
|
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Dispatches a cross-feature communication event to allow other
|
|
296
|
+
* features to provide flags and data that can be used to mutation
|
|
297
|
+
* to the payload and to allow features to know about a feature
|
|
298
|
+
* harvest happening.
|
|
299
|
+
* @param {any[]} errors Array of errors from the payload body
|
|
300
|
+
*/
|
|
301
|
+
#runCrossFeatureChecks (errors) {
|
|
302
|
+
const errorHashes = errors.map(error => error.params.stackHash)
|
|
303
|
+
const crossFeatureData = {
|
|
304
|
+
errorHashes
|
|
305
|
+
}
|
|
306
|
+
this.ee.emit(`cfc.${this.featureName}`, [crossFeatureData])
|
|
307
|
+
|
|
308
|
+
let hasReplayFlag = errors.find(err => err.params.hasReplay)
|
|
309
|
+
if (hasReplayFlag && !crossFeatureData.hasReplay) {
|
|
310
|
+
// Some errors have `hasReplay` and a replay is not being recorded
|
|
311
|
+
errors.forEach(error => {
|
|
312
|
+
delete error.params.hasReplay
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
}
|
|
299
316
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { globalScope, isBrowserScope } from '../../../common/constants/runtime'
|
|
1
|
+
import { globalScope, isBrowserScope, originTime } from '../../../common/constants/runtime'
|
|
2
2
|
import { addPT, addPN } from '../../../common/timing/nav-timing'
|
|
3
3
|
import { stringify } from '../../../common/util/stringify'
|
|
4
4
|
import { getInfo, getRuntime } from '../../../common/config/config'
|
|
@@ -86,13 +86,13 @@ export class Aggregate extends AggregateBase {
|
|
|
86
86
|
if (typeof PerformanceNavigationTiming !== 'undefined') { // Navigation Timing level 2 API that replaced PerformanceTiming & PerformanceNavigation
|
|
87
87
|
const navTimingEntry = globalScope?.performance?.getEntriesByType('navigation')?.[0]
|
|
88
88
|
const perf = ({
|
|
89
|
-
timing: addPT(
|
|
89
|
+
timing: addPT(originTime, navTimingEntry, {}),
|
|
90
90
|
navigation: addPN(navTimingEntry, {})
|
|
91
91
|
})
|
|
92
92
|
queryParameters.perf = stringify(perf)
|
|
93
93
|
} else if (typeof PerformanceTiming !== 'undefined') { // Safari pre-15 did not support level 2 timing
|
|
94
94
|
const perf = ({
|
|
95
|
-
timing: addPT(
|
|
95
|
+
timing: addPT(originTime, globalScope.performance.timing, {}, true),
|
|
96
96
|
navigation: addPN(globalScope.performance.navigation, {})
|
|
97
97
|
})
|
|
98
98
|
queryParameters.perf = stringify(perf)
|
|
@@ -117,7 +117,7 @@ export class Aggregate extends AggregateBase {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
try {
|
|
120
|
-
const timeKeeper = new TimeKeeper()
|
|
120
|
+
const timeKeeper = new TimeKeeper(this.agentIdentifier)
|
|
121
121
|
timeKeeper.processRumRequest(xhr, rumStartTime, rumEndTime)
|
|
122
122
|
if (!timeKeeper.ready) throw new Error('TimeKeeper not ready')
|
|
123
123
|
|
|
@@ -54,11 +54,18 @@ export class Aggregate extends AggregateBase {
|
|
|
54
54
|
this.timeKeeper = undefined
|
|
55
55
|
|
|
56
56
|
this.recorder = args?.recorder
|
|
57
|
-
this.preloaded = !!this.recorder
|
|
58
57
|
this.errorNoticed = args?.errorNoticed || false
|
|
59
58
|
|
|
60
59
|
handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/Enabled'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
61
60
|
|
|
61
|
+
this.ee.on(`cfc.${FEATURE_NAMES.jserrors}`, (crossFeatureData) => {
|
|
62
|
+
crossFeatureData.hasReplay = !!(this.scheduler?.started &&
|
|
63
|
+
this.recorder &&
|
|
64
|
+
this.mode === MODE.FULL &&
|
|
65
|
+
!this.blocked &&
|
|
66
|
+
this.entitled)
|
|
67
|
+
})
|
|
68
|
+
|
|
62
69
|
// The SessionEntity class can emit a message indicating the session was cleared and reset (expiry, inactivity). This feature must abort and never resume if that occurs.
|
|
63
70
|
this.ee.on(SESSION_EVENTS.RESET, () => {
|
|
64
71
|
this.abort(ABORT_REASONS.RESET)
|
|
@@ -104,6 +111,10 @@ export class Aggregate extends AggregateBase {
|
|
|
104
111
|
this.forceStop(this.mode !== MODE.ERROR)
|
|
105
112
|
}, this.featureName, this.ee)
|
|
106
113
|
|
|
114
|
+
registerHandler(SR_EVENT_EMITTER_TYPES.ERROR_DURING_REPLAY, e => {
|
|
115
|
+
this.handleError(e)
|
|
116
|
+
}, this.featureName, this.ee)
|
|
117
|
+
|
|
107
118
|
const { error_sampling_rate, sampling_rate, autoStart, block_selector, mask_text_selector, mask_all_inputs, inline_stylesheet, inline_images, collect_fonts } = getConfigurationValue(this.agentIdentifier, 'session_replay')
|
|
108
119
|
|
|
109
120
|
this.waitForFlags(['sr']).then(([flagOn]) => {
|
|
@@ -148,15 +159,15 @@ export class Aggregate extends AggregateBase {
|
|
|
148
159
|
}
|
|
149
160
|
|
|
150
161
|
switchToFull () {
|
|
162
|
+
if (!this.entitled || this.blocked) return
|
|
151
163
|
this.mode = MODE.FULL
|
|
152
164
|
// if the error was noticed AFTER the recorder was already imported....
|
|
153
165
|
if (this.recorder && this.initialized) {
|
|
154
|
-
this.recorder.
|
|
155
|
-
this.recorder.startRecording()
|
|
156
|
-
|
|
166
|
+
if (!this.recorder.recording) this.recorder.startRecording()
|
|
157
167
|
this.scheduler.startTimer(this.harvestTimeSeconds)
|
|
158
|
-
|
|
159
168
|
this.syncWithSessionManager({ sessionReplayMode: this.mode })
|
|
169
|
+
} else {
|
|
170
|
+
this.initializeRecording(false, true, true)
|
|
160
171
|
}
|
|
161
172
|
}
|
|
162
173
|
|
|
@@ -213,7 +224,6 @@ export class Aggregate extends AggregateBase {
|
|
|
213
224
|
|
|
214
225
|
// If an error was noticed before the mode could be set (like in the early lifecycle of the page), immediately set to FULL mode
|
|
215
226
|
if (this.mode === MODE.ERROR && this.errorNoticed) this.mode = MODE.FULL
|
|
216
|
-
if (!this.preloaded) this.ee.on('err', e => this.handleError(e))
|
|
217
227
|
|
|
218
228
|
// FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
|
|
219
229
|
// ERROR mode will do this until an error is thrown, and then switch into FULL mode.
|
|
@@ -293,7 +303,7 @@ export class Aggregate extends AggregateBase {
|
|
|
293
303
|
}
|
|
294
304
|
|
|
295
305
|
getCorrectedTimestamp (node) {
|
|
296
|
-
if (!node
|
|
306
|
+
if (!node?.timestamp) return
|
|
297
307
|
if (node.__newrelic) return node.timestamp
|
|
298
308
|
return this.timeKeeper.correctAbsoluteTimestamp(node.timestamp)
|
|
299
309
|
}
|
|
@@ -331,6 +341,7 @@ export class Aggregate extends AggregateBase {
|
|
|
331
341
|
const lastTimestamp = lastEventTimestamp || this.timeKeeper.convertRelativeTimestamp(relativeNow)
|
|
332
342
|
|
|
333
343
|
const agentMetadata = agentRuntime.appMetadata?.agents?.[0] || {}
|
|
344
|
+
|
|
334
345
|
return {
|
|
335
346
|
qs: {
|
|
336
347
|
browser_monitoring_key: info.licenseKey,
|
|
@@ -343,6 +354,7 @@ export class Aggregate extends AggregateBase {
|
|
|
343
354
|
// if not, data could be lost to truncation at time of sending, potentially breaking parsing / API behavior in NR1
|
|
344
355
|
...(!!this.gzipper && !!this.u8 && { content_encoding: 'gzip' }),
|
|
345
356
|
...(agentMetadata.entityGuid && { entityGuid: agentMetadata.entityGuid }),
|
|
357
|
+
harvestId: [agentRuntime.session?.state.value, agentRuntime.ptid, agentRuntime.harvestCount].filter(x => x).join('_'),
|
|
346
358
|
'replay.firstTimestamp': firstTimestamp,
|
|
347
359
|
'replay.lastTimestamp': lastTimestamp,
|
|
348
360
|
'replay.nodes': events.length,
|
|
@@ -6,7 +6,8 @@ export const FEATURE_NAME = FEATURE_NAMES.sessionReplay
|
|
|
6
6
|
export const SR_EVENT_EMITTER_TYPES = {
|
|
7
7
|
RECORD: 'recordReplay',
|
|
8
8
|
PAUSE: 'pauseReplay',
|
|
9
|
-
REPLAY_RUNNING: 'replayRunning'
|
|
9
|
+
REPLAY_RUNNING: 'replayRunning',
|
|
10
|
+
ERROR_DURING_REPLAY: 'errorDuringReplay'
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export const AVG_COMPRESSION = 0.12
|