Skip to content

Commit 38e3592

Browse files
committed
Merge branch 'NR-443294-navigations' into tmp-user-frustrations
2 parents 8bfa165 + 0a00ae3 commit 38e3592

File tree

34 files changed

+455
-229
lines changed

34 files changed

+455
-229
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
33
All notable changes to this project will be documented in this file.
44
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
55

6+
## [1.296.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.295.0...v1.296.0) (2025-08-19)
7+
8+
9+
### Features
10+
11+
* Harvest traces early ([#1532](https://github.com/newrelic/newrelic-browser-agent/issues/1532)) ([58f3c52](https://github.com/newrelic/newrelic-browser-agent/commit/58f3c52db5b57dcb41876792f2a1a14fa907d66d))
12+
13+
14+
### Bug Fixes
15+
16+
* Remove event buffer inspection event ([#1540](https://github.com/newrelic/newrelic-browser-agent/issues/1540)) ([3e3ca33](https://github.com/newrelic/newrelic-browser-agent/commit/3e3ca330a719dc1312019f5d1970c11dff4c6edf))
17+
618
## [1.295.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.294.0...v1.295.0) (2025-08-04)
719

820

changelog.json

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
{
22
"repository": "newrelic/newrelic-browser-agent",
33
"entries": [
4+
{
5+
"changes": [
6+
{
7+
"type": "feat",
8+
"sha": "58f3c52db5b57dcb41876792f2a1a14fa907d66d",
9+
"message": "Harvest traces early",
10+
"issues": [
11+
"1532"
12+
]
13+
},
14+
{
15+
"type": "fix",
16+
"sha": "3e3ca330a719dc1312019f5d1970c11dff4c6edf",
17+
"message": "Remove event buffer inspection event",
18+
"issues": [
19+
"1540"
20+
]
21+
}
22+
],
23+
"version": "1.296.0",
24+
"language": "JAVASCRIPT",
25+
"artifactName": "@newrelic/browser-agent",
26+
"id": "4df80f61-aa97-444e-a813-9734ece9a145",
27+
"createTime": "2025-08-19T21:32:44.379Z"
28+
},
429
{
530
"changes": [
631
{
@@ -3107,5 +3132,5 @@
31073132
"createTime": "2023-05-08T21:11:35.144Z"
31083133
}
31093134
],
3110-
"updateTime": "2025-08-04T18:18:14.079Z"
3135+
"updateTime": "2025-08-19T21:32:44.379Z"
31113136
}

package-lock.json

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@newrelic/browser-agent",
3-
"version": "1.295.0",
3+
"version": "1.296.0",
44
"private": false,
55
"author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
66
"description": "New Relic Browser Agent",

src/common/harvest/harvester.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ const warnings = {}
112112
* @param {NetworkSendSpec} param0 Specification for sending data
113113
* @returns {boolean} True if a network call was made. Note that this does not mean or guarantee that it was successful.
114114
*/
115-
function send (agentRef, { endpoint, targetApp, payload, localOpts = {}, submitMethod, cbFinished, raw, featureName }) {
115+
export function send (agentRef, { endpoint, targetApp, payload, localOpts = {}, submitMethod, cbFinished, raw, featureName }) {
116116
if (!agentRef.info.errorBeacon) return false
117117

118118
let { body, qs } = cleanPayload(payload)
@@ -218,7 +218,7 @@ function send (agentRef, { endpoint, targetApp, payload, localOpts = {}, submitM
218218
function cleanPayload (payload = {}) {
219219
const clean = (input) => {
220220
if ((typeof Uint8Array !== 'undefined' && input instanceof Uint8Array) || Array.isArray(input)) return input
221-
if (typeof input === 'string') return input.length > 0 ? input : null
221+
if (typeof input === 'string') return input
222222
return Object.entries(input || {}).reduce((accumulator, [key, value]) => {
223223
if ((typeof value === 'number') ||
224224
(typeof value === 'string' && value.length > 0) ||

src/features/generic_events/aggregate/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ export class Aggregate extends AggregateBase {
134134
}, this.featureName, this.ee)
135135
registerHandler('err', () => this.userActionAggregator.markAsErrorClick(), this.featureName, this.ee)
136136
registerHandler('xhr', (params) => evalNetworkRequest.call(this, params.host), this.featureName, this.ee)
137+
registerHandler('navChange', () => this.userActionAggregator.treatAsLiveClick(), this.featureName, this.ee)
137138
}
138139

139140
/**

src/features/generic_events/instrument/index.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { globalScope, isBrowserScope } from '../../../common/constants/runtime'
77
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'
99
import { debounce } from '../../../common/util/invoke'
1010
import { setupAddPageActionAPI } from '../../../loaders/api/addPageAction'
1111
import { setupFinishedAPI } from '../../../loaders/api/finished'
@@ -14,6 +14,7 @@ import { setupRegisterAPI } from '../../../loaders/api/register'
1414
import { setupMeasureAPI } from '../../../loaders/api/measure'
1515
import { InstrumentBase } from '../../utils/instrument-base'
1616
import { FEATURE_NAME, OBSERVED_EVENTS, OBSERVED_WINDOW_EVENTS } from '../constants'
17+
import { wrapHistory } from '../../../common/wrap/wrap-history'
1718

1819
export class Instrument extends InstrumentBase {
1920
static featureName = FEATURE_NAME
@@ -55,12 +56,33 @@ export class Instrument extends InstrumentBase {
5556
})
5657
observer.observe({ type: 'resource', buffered: true })
5758
}
59+
60+
try {
61+
this.removeOnAbort = new AbortController()
62+
} catch (e) {}
63+
64+
function navigationChange () {
65+
historyEE.emit('navChange')
66+
}
67+
68+
const historyEE = wrapHistory(this.ee)
69+
historyEE.on('pushState-end', navigationChange)
70+
historyEE.on('replaceState-end', navigationChange)
71+
window.addEventListener('hashchange', navigationChange, eventListenerOpts(true, this.removeOnAbort?.signal))
72+
window.addEventListener('load', navigationChange, eventListenerOpts(true, this.removeOnAbort?.signal))
73+
window.addEventListener('popstate', navigationChange, eventListenerOpts(true, this.removeOnAbort?.signal))
5874
}
5975

76+
this.abortHandler = this.#abort
6077
/** If any of the sources are active, import the aggregator. otherwise deregister */
6178
if (genericEventSourceConfigs.some(x => x)) this.importAggregator(agentRef, () => import(/* webpackChunkName: "generic_events-aggregate" */ '../aggregate'))
6279
else this.deregisterDrain()
6380
}
81+
82+
#abort () {
83+
this.removeOnAbort?.abort()
84+
this.abortHandler = undefined // weakly allow this abort op to run only once
85+
}
6486
}
6587

6688
export const GenericEvents = Instrument

src/features/session_replay/aggregate/index.js

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { MODE, SESSION_EVENTS, SESSION_EVENT_TYPES } from '../../../common/sessi
1818
import { stringify } from '../../../common/util/stringify'
1919
import { stylesheetEvaluator } from '../shared/stylesheet-evaluator'
2020
import { now } from '../../../common/timing/now'
21-
import { buildNRMetaNode } from '../shared/utils'
2221
import { MAX_PAYLOAD_SIZE } from '../../../common/constants/agent-constants'
2322
import { cleanURL } from '../../../common/url/clean-url'
2423
import { canEnableSessionTracking } from '../../utils/feature-gates'
@@ -94,8 +93,7 @@ export class Aggregate extends AggregateBase {
9493
}
9594
return
9695
}
97-
this.drain()
98-
this.initializeRecording(srMode)
96+
this.initializeRecording(srMode).then(() => { this.drain() })
9997
}).then(() => {
10098
if (this.mode === MODE.OFF) {
10199
this.recorder?.stopRecording() // stop any conservative preload recording launched by instrument
@@ -121,7 +119,7 @@ export class Aggregate extends AggregateBase {
121119
}
122120

123121
handleError (e) {
124-
if (this.recorder) this.recorder.currentBufferTarget.hasError = true
122+
if (this.recorder) this.recorder.events.hasError = true
125123
// run once
126124
if (this.mode === MODE.ERROR && globalScope?.document.visibilityState === 'visible') {
127125
this.switchToFull()
@@ -171,10 +169,10 @@ export class Aggregate extends AggregateBase {
171169

172170
if (!this.recorder) {
173171
try {
174-
// Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
172+
// Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
175173
const { Recorder } = (await import(/* webpackChunkName: "recorder" */'../shared/recorder'))
176174
this.recorder = new Recorder(this)
177-
this.recorder.currentBufferTarget.hasError = this.errorNoticed
175+
this.recorder.events.hasError = this.errorNoticed
178176
} catch (err) {
179177
return this.abort(ABORT_REASONS.IMPORT)
180178
}
@@ -189,11 +187,6 @@ export class Aggregate extends AggregateBase {
189187
// ERROR mode will do this until an error is thrown, and then switch into FULL mode.
190188
// The makeHarvestPayload should ensure that no payload is returned if we're not in FULL mode...
191189

192-
// If theres preloaded events and we are in full mode, just harvest immediately to clear up space and for consistency
193-
if (this.mode === MODE.FULL && this.recorder?.getEvents().type === 'preloaded') {
194-
this.prepUtils().then(() => this.agentRef.runtime.harvester.triggerHarvestFor(this))
195-
}
196-
197190
await this.prepUtils()
198191

199192
if (!this.agentRef.runtime.isRecording) this.recorder.startRecording()
@@ -231,24 +224,10 @@ export class Aggregate extends AggregateBase {
231224

232225
let len = 0
233226
if (!!this.gzipper && !!this.u8) {
234-
payload.body = this.gzipper(this.u8(`[${payload.body.map(({ __serialized, ...e }) => {
235-
if (e.__newrelic && __serialized) return __serialized
236-
const output = { ...e }
237-
if (!output.__newrelic) {
238-
output.__newrelic = buildNRMetaNode(e.timestamp, this.timeKeeper)
239-
output.timestamp = this.timeKeeper.correctAbsoluteTimestamp(e.timestamp)
240-
}
241-
return stringify(output)
242-
}).join(',')}]`))
227+
payload.body = this.gzipper(this.u8(`[${payload.body.map(({ __serialized }) => (__serialized)).join(',')}]`))
243228
len = payload.body.length
244229
} else {
245-
payload.body = payload.body.map(({ __serialized, ...node }) => {
246-
if (node.__newrelic) return node
247-
const output = { ...node }
248-
output.__newrelic = buildNRMetaNode(node.timestamp, this.timeKeeper)
249-
output.timestamp = this.timeKeeper.correctAbsoluteTimestamp(node.timestamp)
250-
return output
251-
})
230+
for (let idx in payload.body) delete payload.body[idx].__serialized
252231
len = stringify(payload.body).length
253232
}
254233

@@ -269,12 +248,6 @@ export class Aggregate extends AggregateBase {
269248
return [payloadOutput]
270249
}
271250

272-
getCorrectedTimestamp (node) {
273-
if (!node?.timestamp) return
274-
if (node.__newrelic) return node.timestamp
275-
return this.timeKeeper.correctAbsoluteTimestamp(node.timestamp)
276-
}
277-
278251
/**
279252
* returns the timestamps for the earliest and latest nodes in the provided array, even if out of order
280253
* @param {Object[]} [nodes] - the nodes to evaluate
@@ -318,8 +291,8 @@ export class Aggregate extends AggregateBase {
318291

319292
const { firstEvent, lastEvent } = this.getFirstAndLastNodes(events)
320293
// from rrweb node || from when the harvest cycle started
321-
const firstTimestamp = this.getCorrectedTimestamp(firstEvent) || Math.floor(this.timeKeeper.correctAbsoluteTimestamp(recorderEvents.cycleTimestamp))
322-
const lastTimestamp = this.getCorrectedTimestamp(lastEvent) || Math.floor(this.timeKeeper.correctRelativeTimestamp(relativeNow))
294+
const firstTimestamp = firstEvent?.timestamp || Math.floor(this.timeKeeper.correctAbsoluteTimestamp(recorderEvents.cycleTimestamp))
295+
const lastTimestamp = lastEvent?.timestamp || Math.floor(this.timeKeeper.correctRelativeTimestamp(relativeNow))
323296

324297
const agentMetadata = agentRuntime.appMetadata?.agents?.[0] || {}
325298

src/features/session_replay/instrument/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { handle } from '../../../common/event-emitter/handle'
1010
import { DEFAULT_KEY, MODE, PREFIX } from '../../../common/session/constants'
1111
import { InstrumentBase } from '../../utils/instrument-base'
1212
import { hasReplayPrerequisite, isPreloadAllowed } from '../shared/utils'
13-
import { FEATURE_NAME, SR_EVENT_EMITTER_TYPES, TRIGGERS } from '../constants'
13+
import { FEATURE_NAME, SR_EVENT_EMITTER_TYPES } from '../constants'
1414
import { setupRecordReplayAPI } from '../../../loaders/api/recordReplay'
1515
import { setupPauseReplayAPI } from '../../../loaders/api/pauseReplay'
1616

@@ -69,7 +69,7 @@ export class Instrument extends InstrumentBase {
6969
/**
7070
* This func is use for early pre-load recording prior to replay feature (agg) being loaded onto the page. It should only setup once, including if already called and in-progress.
7171
*/
72-
async #preloadStartRecording (trigger) {
72+
async #preloadStartRecording () {
7373
if (this.#alreadyStarted) return
7474
this.#alreadyStarted = true
7575

@@ -78,7 +78,7 @@ export class Instrument extends InstrumentBase {
7878

7979
// If startReplay() has been used by this point, we must record in full mode regardless of session preload:
8080
// Note: recorder starts here with w/e the mode is at this time, but this may be changed later (see #apiStartOrRestartReplay else-case)
81-
this.recorder ??= new Recorder({ mode: this.#mode, agentIdentifier: this.agentIdentifier, trigger, ee: this.ee, agentRef: this.#agentRef })
81+
this.recorder ??= new Recorder({ ...this, mode: this.#mode, agentRef: this.#agentRef, timeKeeper: this.#agentRef.runtime.timeKeeper }) // if TK exists due to deferred state, pass it
8282
this.recorder.startRecording()
8383
this.abortHandler = this.recorder.stopRecording
8484
} catch (err) {
@@ -95,7 +95,7 @@ export class Instrument extends InstrumentBase {
9595
if (this.featAggregate.mode !== MODE.FULL) this.featAggregate.initializeRecording(MODE.FULL, true)
9696
} else { // pre-load
9797
this.#mode = MODE.FULL
98-
this.#preloadStartRecording(TRIGGERS.API)
98+
this.#preloadStartRecording()
9999
// There's a race here wherein either:
100100
// a. Recorder has not been initialized, and we've set the enforced mode, so we're good, or;
101101
// b. Record has been initialized, possibly with the "wrong" mode, so we have to correct that + restart.

src/features/session_replay/shared/recorder-events.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ export class RecorderEvents {
2626
this.inlinedAllStylesheets = shouldInlineStylesheets
2727
}
2828

29-
add (event) {
30-
this.#events.add(event)
29+
add (event, evaluatedSize) {
30+
this.#events.add(event, evaluatedSize)
3131
}
3232

3333
get events () {

0 commit comments

Comments
 (0)