@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.
Files changed (140) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/dist/cjs/common/config/state/configurable.js +8 -5
  3. package/dist/cjs/common/config/state/init.js +0 -2
  4. package/dist/cjs/common/config/state/runtime.js +10 -8
  5. package/dist/cjs/common/constants/env.cdn.js +1 -1
  6. package/dist/cjs/common/constants/env.npm.js +1 -1
  7. package/dist/cjs/common/constants/runtime.js +8 -2
  8. package/dist/cjs/common/harvest/harvest.js +7 -5
  9. package/dist/cjs/common/session/constants.js +1 -0
  10. package/dist/cjs/common/session/session-entity.js +3 -0
  11. package/dist/cjs/common/timing/time-keeper.js +45 -9
  12. package/dist/cjs/common/vitals/time-to-first-byte.js +1 -1
  13. package/dist/cjs/common/vitals/vital-metric.js +1 -1
  14. package/dist/cjs/features/ajax/aggregate/chunk.js +50 -0
  15. package/dist/cjs/features/ajax/aggregate/index.js +131 -191
  16. package/dist/cjs/features/ajax/instrument/index.js +0 -3
  17. package/dist/cjs/features/jserrors/aggregate/index.js +26 -13
  18. package/dist/cjs/features/page_view_event/aggregate/index.js +3 -3
  19. package/dist/cjs/features/session_replay/aggregate/index.js +12 -5
  20. package/dist/cjs/features/session_replay/constants.js +2 -1
  21. package/dist/cjs/features/session_replay/instrument/index.js +15 -5
  22. package/dist/cjs/features/session_replay/shared/recorder.js +6 -3
  23. package/dist/cjs/features/session_replay/shared/utils.js +9 -8
  24. package/dist/cjs/features/session_trace/aggregate/index.js +3 -5
  25. package/dist/cjs/features/soft_navigations/aggregate/index.js +2 -2
  26. package/dist/cjs/features/spa/instrument/index.js +0 -2
  27. package/dist/cjs/features/utils/agent-session.js +1 -5
  28. package/dist/cjs/features/utils/instrument-base.js +11 -14
  29. package/dist/cjs/features/utils/nr1-debugger.js +27 -0
  30. package/dist/cjs/loaders/agent.js +4 -0
  31. package/dist/cjs/loaders/api/apiAsync.js +5 -4
  32. package/dist/esm/common/config/state/configurable.js +8 -5
  33. package/dist/esm/common/config/state/init.js +0 -2
  34. package/dist/esm/common/config/state/runtime.js +11 -9
  35. package/dist/esm/common/constants/env.cdn.js +1 -1
  36. package/dist/esm/common/constants/env.npm.js +1 -1
  37. package/dist/esm/common/constants/runtime.js +7 -1
  38. package/dist/esm/common/harvest/harvest.js +7 -5
  39. package/dist/esm/common/session/constants.js +1 -0
  40. package/dist/esm/common/session/session-entity.js +3 -0
  41. package/dist/esm/common/timing/time-keeper.js +46 -9
  42. package/dist/esm/common/vitals/time-to-first-byte.js +2 -2
  43. package/dist/esm/common/vitals/vital-metric.js +1 -1
  44. package/dist/esm/features/ajax/aggregate/chunk.js +43 -0
  45. package/dist/esm/features/ajax/aggregate/index.js +130 -191
  46. package/dist/esm/features/ajax/instrument/index.js +1 -4
  47. package/dist/esm/features/jserrors/aggregate/index.js +26 -13
  48. package/dist/esm/features/page_view_event/aggregate/index.js +4 -4
  49. package/dist/esm/features/session_replay/aggregate/index.js +12 -5
  50. package/dist/esm/features/session_replay/constants.js +2 -1
  51. package/dist/esm/features/session_replay/instrument/index.js +16 -6
  52. package/dist/esm/features/session_replay/shared/recorder.js +6 -3
  53. package/dist/esm/features/session_replay/shared/utils.js +9 -7
  54. package/dist/esm/features/session_trace/aggregate/index.js +3 -5
  55. package/dist/esm/features/soft_navigations/aggregate/index.js +2 -2
  56. package/dist/esm/features/spa/instrument/index.js +0 -2
  57. package/dist/esm/features/utils/agent-session.js +1 -5
  58. package/dist/esm/features/utils/instrument-base.js +11 -14
  59. package/dist/esm/features/utils/nr1-debugger.js +21 -0
  60. package/dist/esm/loaders/agent.js +4 -0
  61. package/dist/esm/loaders/api/apiAsync.js +5 -4
  62. package/dist/types/common/config/state/configurable.d.ts.map +1 -1
  63. package/dist/types/common/config/state/init.d.ts.map +1 -1
  64. package/dist/types/common/config/state/runtime.d.ts.map +1 -1
  65. package/dist/types/common/constants/runtime.d.ts +6 -1
  66. package/dist/types/common/constants/runtime.d.ts.map +1 -1
  67. package/dist/types/common/harvest/harvest.d.ts +1 -1
  68. package/dist/types/common/harvest/harvest.d.ts.map +1 -1
  69. package/dist/types/common/session/constants.d.ts +1 -0
  70. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  71. package/dist/types/common/timing/time-keeper.d.ts +1 -1
  72. package/dist/types/common/timing/time-keeper.d.ts.map +1 -1
  73. package/dist/types/features/ajax/aggregate/chunk.d.ts +8 -0
  74. package/dist/types/features/ajax/aggregate/chunk.d.ts.map +1 -0
  75. package/dist/types/features/ajax/aggregate/index.d.ts +8 -6
  76. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  77. package/dist/types/features/ajax/instrument/index.d.ts +2 -2
  78. package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
  79. package/dist/types/features/jserrors/aggregate/index.d.ts +0 -1
  80. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  81. package/dist/types/features/session_replay/aggregate/index.d.ts +0 -1
  82. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  83. package/dist/types/features/session_replay/constants.d.ts +1 -0
  84. package/dist/types/features/session_replay/constants.d.ts.map +1 -1
  85. package/dist/types/features/session_replay/instrument/index.d.ts +1 -0
  86. package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
  87. package/dist/types/features/session_replay/shared/recorder.d.ts +1 -0
  88. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  89. package/dist/types/features/session_replay/shared/utils.d.ts +5 -5
  90. package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -1
  91. package/dist/types/features/session_trace/aggregate/index.d.ts +8 -8
  92. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  93. package/dist/types/features/spa/instrument/index.d.ts.map +1 -1
  94. package/dist/types/features/utils/agent-session.d.ts.map +1 -1
  95. package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
  96. package/dist/types/features/utils/nr1-debugger.d.ts +2 -0
  97. package/dist/types/features/utils/nr1-debugger.d.ts.map +1 -0
  98. package/dist/types/loaders/agent.d.ts +5 -1
  99. package/dist/types/loaders/agent.d.ts.map +1 -1
  100. package/dist/types/loaders/api/apiAsync.d.ts.map +1 -1
  101. package/package.json +1 -1
  102. package/src/common/config/state/configurable.js +9 -8
  103. package/src/common/config/state/init.js +0 -1
  104. package/src/common/config/state/runtime.js +12 -9
  105. package/src/common/constants/__mocks__/runtime.js +2 -0
  106. package/src/common/constants/runtime.js +6 -1
  107. package/src/common/drain/__mocks__/drain.js +2 -0
  108. package/src/common/harvest/harvest.js +8 -6
  109. package/src/common/session/constants.js +1 -0
  110. package/src/common/session/session-entity.js +2 -0
  111. package/src/common/timing/time-keeper.js +44 -10
  112. package/src/common/vitals/time-to-first-byte.js +2 -2
  113. package/src/common/vitals/vital-metric.js +1 -1
  114. package/src/common/window/__mocks__/load.js +3 -0
  115. package/src/features/ajax/aggregate/chunk.js +51 -0
  116. package/src/features/ajax/aggregate/index.js +128 -200
  117. package/src/features/ajax/instrument/index.js +1 -4
  118. package/src/features/jserrors/aggregate/index.js +28 -11
  119. package/src/features/page_view_event/aggregate/index.js +4 -4
  120. package/src/features/session_replay/aggregate/index.js +19 -7
  121. package/src/features/session_replay/constants.js +2 -1
  122. package/src/features/session_replay/instrument/index.js +16 -6
  123. package/src/features/session_replay/shared/__mocks__/utils.js +2 -0
  124. package/src/features/session_replay/shared/recorder.js +7 -4
  125. package/src/features/session_replay/shared/utils.js +9 -7
  126. package/src/features/session_trace/aggregate/index.js +3 -6
  127. package/src/features/soft_navigations/aggregate/index.js +2 -2
  128. package/src/features/spa/instrument/index.js +0 -3
  129. package/src/features/utils/__mocks__/agent-session.js +1 -0
  130. package/src/features/utils/__mocks__/feature-base.js +11 -0
  131. package/src/features/utils/agent-session.js +1 -7
  132. package/src/features/utils/instrument-base.js +11 -14
  133. package/src/features/utils/nr1-debugger.js +22 -0
  134. package/src/loaders/agent.js +4 -0
  135. package/src/loaders/api/apiAsync.js +5 -4
  136. package/dist/cjs/common/storage/first-party-cookies.js +0 -36
  137. package/dist/esm/common/storage/first-party-cookies.js +0 -29
  138. package/dist/types/common/storage/first-party-cookies.d.ts +0 -8
  139. package/dist/types/common/storage/first-party-cookies.d.ts.map +0 -1
  140. 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.waitForFlags(([])).then(() => {
28
- const scheduler = new HarvestScheduler('events', {
29
- onFinished: onEventsHarvestFinished,
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 agentRuntime = getRuntime(agentIdentifier)
37
- const denyList = agentRuntime.denyList
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
- let ajaxEvents = []
41
- let spaAjaxEvents = {}
42
- let sentAjaxEvents = []
43
- const ee = this.ee
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(function (item) {
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
- const beacon = getInfo(agentIdentifier).errorBeacon
70
- const proxyBeacon = agentInit.proxy.beacon
71
-
72
- function storeXhr (params, metrics, startTime, endTime, type) {
73
- metrics.time = startTime
74
-
75
- // send to session traces
76
- var hash
77
- if (params.cat) {
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
- if (!shouldCollect) {
92
- if (params.hostname === beacon || (proxyBeacon && params.hostname === proxyBeacon)) {
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
- if (ajaxMetricDenyListEnabled) {
97
- handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Metrics/Excluded/Agent'], undefined, FEATURE_NAMES.metrics, ee)
98
- }
99
- } else {
100
- handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Events/Excluded/App'], undefined, FEATURE_NAMES.metrics, ee)
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
- if (ajaxMetricDenyListEnabled) {
103
- handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Metrics/Excluded/App'], undefined, FEATURE_NAMES.metrics, ee)
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
- handle('bstXhrAgg', ['xhr', hash, params, metrics], undefined, FEATURE_NAMES.sessionTrace, ee)
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
- var xhrContext = this
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
- var event = {
114
- method: params.method,
115
- status: params.status,
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
- if (xhrContext.dt) {
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
- // parsed from the AJAX body, looking for operationName param & parsing query for operationType
133
- event.gql = params.gql = parseGQL({
134
- body: this.body,
135
- query: this?.parsedOrigin?.search
136
- })
137
- if (event.gql) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length], undefined, FEATURE_NAMES.metrics, ee)
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
- const softNavInUse = Boolean(getNREUMInitializedAgent(agentIdentifier)?.features?.[FEATURE_NAMES.softNav])
140
- if (softNavInUse) { // For newer soft nav (when running), pass the event to it for evaluation -- either part of an interaction or is given back
141
- handle('ajax', [event], undefined, FEATURE_NAMES.softNav, ee)
142
- } else if (this.spaNode) { // For old spa (when running), if the ajax happened inside an interaction, hold it until the interaction finishes
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
- function prepareHarvest (options) {
152
- options = options || {}
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
- if (ajaxEvents.length === 0) {
155
- return null
156
- }
141
+ prepareHarvest (options) {
142
+ options = options || {}
143
+ if (this.ajaxEvents.length === 0) return null
157
144
 
158
- var payload = getPayload(ajaxEvents, options.maxPayloadSize || MAX_PAYLOAD_SIZE)
145
+ const payload = this.#getPayload(this.ajaxEvents)
146
+ const payloadObjs = []
159
147
 
160
- var payloadObjs = []
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
- if (options.retry) {
166
- sentAjaxEvents = ajaxEvents.slice()
167
- }
150
+ if (options.retry) this.sentAjaxEvents = this.ajaxEvents
151
+ this.ajaxEvents = []
168
152
 
169
- ajaxEvents = []
153
+ return payloadObjs
154
+ }
170
155
 
171
- return payloadObjs
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
- function getPayload (events, maxPayloadSize, chunks) {
175
- chunks = chunks || 1
176
- var payload = []
177
- var chunkSize = events.length / chunks
178
- var eventChunks = splitChunks(events, chunkSize)
179
- var tooBig = false
180
- for (var i = 0; i < eventChunks.length; i++) {
181
- var currentChunk = eventChunks[i]
182
- if (currentChunk.tooBig(maxPayloadSize)) {
183
- if (currentChunk.events.length !== 1) {
184
- /* if it is too big BUT it isnt length 1, we can split it down again,
185
- else we just want to NOT push it into payload
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
- // check if the current payload string is too big, if so then run getPayload again with more buckets
196
- return tooBig ? getPayload(events, maxPayloadSize, ++chunks) : payload
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
- var chunks = []
209
- for (var i = 0, len = arr.length; i < len; i += chunkSize) {
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, getRuntime } from '../../../common/config/config'
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
- if (this.replayAborted) {
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 = this.observedAt[bucketHash]
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(agentRuntime.offset, navTimingEntry, {}),
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(agentRuntime.offset, globalScope.performance.timing, {}, true),
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.stopRecording()
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.timestamp) return
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