Skip to content

Commit 3e60081

Browse files
authored
Merge branch 'main' into NR-347721-add-delete-experiment-gha
2 parents 0d64dc5 + 0e8f48b commit 3e60081

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1932
-90
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.277.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.276.0...v1.277.0) (2024-12-18)
7+
8+
9+
### Features
10+
11+
* Add custom events API ([#1263](https://github.com/newrelic/newrelic-browser-agent/issues/1263)) ([9395415](https://github.com/newrelic/newrelic-browser-agent/commit/9395415d942b55e88e89438aa203c6a1642d9e6b))
12+
13+
14+
### Bug Fixes
15+
16+
* Soft navigation bug fixes and new soft navigation tests ([#1268](https://github.com/newrelic/newrelic-browser-agent/issues/1268)) ([7624928](https://github.com/newrelic/newrelic-browser-agent/commit/762492896a7b96564269aab1aadeb6e44a4da242))
17+
618
## [1.276.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.275.0...v1.276.0) (2024-12-16)
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": "9395415d942b55e88e89438aa203c6a1642d9e6b",
9+
"message": "Add custom events API",
10+
"issues": [
11+
"1263"
12+
]
13+
},
14+
{
15+
"type": "fix",
16+
"sha": "762492896a7b96564269aab1aadeb6e44a4da242",
17+
"message": "Soft navigation bug fixes and new soft navigation tests",
18+
"issues": [
19+
"1268"
20+
]
21+
}
22+
],
23+
"version": "1.277.0",
24+
"language": "JAVASCRIPT",
25+
"artifactName": "@newrelic/browser-agent",
26+
"id": "e01beb28-b225-4333-9334-f7ac3e63180d",
27+
"createTime": "2024-12-18T17:31:02.842Z"
28+
},
429
{
530
"changes": [
631
{
@@ -2309,5 +2334,5 @@
23092334
"createTime": "2023-05-08T21:11:35.144Z"
23102335
}
23112336
],
2312-
"updateTime": "2024-12-16T20:51:42.875Z"
2337+
"updateTime": "2024-12-18T17:31:02.842Z"
23132338
}

docs/warning-codes.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,6 @@
9090
### 44
9191
`Invalid object passed to generic event aggregate. Missing "eventType".`
9292
### 45
93-
`An internal agent process failed to execute.`
93+
`An internal agent process failed to execute.`
94+
### 46
95+
`A reserved eventType was provided to recordCustomEvent(...) -- The event was not recorded.`

package-lock.json

Lines changed: 2 additions & 2 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.276.0",
3+
"version": "1.277.0",
44
"private": false,
55
"author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
66
"description": "New Relic Browser Agent",

src/features/generic_events/aggregate/index.js

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import { stringify } from '../../../common/util/stringify'
66
import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
77
import { cleanURL } from '../../../common/url/clean-url'
8-
import { FEATURE_NAME } from '../constants'
8+
import { FEATURE_NAME, RESERVED_EVENT_TYPES } from '../constants'
99
import { globalScope, initialLocation, isBrowserScope } from '../../../common/constants/runtime'
1010
import { AggregateBase } from '../../utils/aggregate-base'
1111
import { warn } from '../../../common/util/console'
@@ -37,12 +37,21 @@ export class Aggregate extends AggregateBase {
3737

3838
this.trackSupportabilityMetrics()
3939

40+
registerHandler('api-recordCustomEvent', (timestamp, eventType, attributes) => {
41+
if (RESERVED_EVENT_TYPES.includes(eventType)) return warn(46)
42+
this.addEvent({
43+
eventType,
44+
timestamp: this.toEpoch(timestamp),
45+
...attributes
46+
})
47+
}, this.featureName, this.ee)
48+
4049
if (agentRef.init.page_action.enabled) {
4150
registerHandler('api-addPageAction', (timestamp, name, attributes) => {
4251
this.addEvent({
4352
...attributes,
4453
eventType: 'PageAction',
45-
timestamp: Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp)),
54+
timestamp: this.toEpoch(timestamp),
4655
timeSinceLoad: timestamp / 1000,
4756
actionName: name,
4857
referrerUrl: this.referrerUrl,
@@ -66,7 +75,7 @@ export class Aggregate extends AggregateBase {
6675
const { target, timeStamp, type } = aggregatedUserAction.event
6776
this.addEvent({
6877
eventType: 'UserAction',
69-
timestamp: Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timeStamp)),
78+
timestamp: this.toEpoch(timeStamp),
7079
action: type,
7180
actionCount: aggregatedUserAction.count,
7281
actionDuration: aggregatedUserAction.relativeMs[aggregatedUserAction.relativeMs.length - 1],
@@ -118,7 +127,7 @@ export class Aggregate extends AggregateBase {
118127
handle(SUPPORTABILITY_METRIC_CHANNEL, ['Generic/Performance/' + type + '/Seen'])
119128
this.addEvent({
120129
eventType: 'BrowserPerformance',
121-
timestamp: Math.floor(agentRef.runtime.timeKeeper.correctRelativeTimestamp(entry.startTime)),
130+
timestamp: this.toEpoch(entry.startTime),
122131
entryName: cleanURL(entry.name),
123132
entryDuration: entry.duration,
124133
entryType: type,
@@ -250,6 +259,10 @@ export class Aggregate extends AggregateBase {
250259
return { ua: this.agentRef.info.userAttributes, at: this.agentRef.info.atts }
251260
}
252261

262+
toEpoch (timestamp) {
263+
return Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp))
264+
}
265+
253266
trackSupportabilityMetrics () {
254267
/** track usage SMs to improve these experimental features */
255268
const configPerfTag = 'Config/Performance/'

src/features/generic_events/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export const OBSERVED_WINDOW_EVENTS = ['focus', 'blur']
1010
export const RAGE_CLICK_THRESHOLD_EVENTS = 4
1111
export const RAGE_CLICK_THRESHOLD_MS = 1000
1212

13+
export const RESERVED_EVENT_TYPES = ['PageAction', 'UserAction', 'BrowserPerformance']
14+
1315
export const FEATURE_FLAGS = {
1416
MARKS: 'experimental.marks',
1517
MEASURES: 'experimental.measures',

src/features/soft_navigations/aggregate/index.js

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte'
66
import { FEATURE_NAMES, FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
77
import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
88
import { AggregateBase } from '../../utils/aggregate-base'
9-
import { API_TRIGGER_NAME, FEATURE_NAME, INTERACTION_STATUS } from '../constants'
9+
import { API_TRIGGER_NAME, FEATURE_NAME, INTERACTION_STATUS, INTERACTION_TRIGGERS, IPL_TRIGGER_NAME } from '../constants'
1010
import { AjaxNode } from './ajax-node'
1111
import { InitialPageLoadInteraction } from './initial-page-load-interaction'
1212
import { Interaction } from './interaction'
@@ -21,18 +21,21 @@ export class Aggregate extends AggregateBase {
2121
this.domObserver = domObserver
2222

2323
this.initialPageLoadInteraction = new InitialPageLoadInteraction(agentRef.agentIdentifier)
24+
this.initialPageLoadInteraction.onDone.push(() => { // this ensures the .end() method also works with iPL
25+
this.initialPageLoadInteraction.forceSave = true // unless forcibly ignored, iPL always finish by default
26+
this.interactionsToHarvest.add(this.initialPageLoadInteraction)
27+
this.initialPageLoadInteraction = null
28+
})
2429
timeToFirstByte.subscribe(({ attrs }) => {
2530
const loadEventTime = attrs.navigationEntry.loadEventEnd
26-
this.initialPageLoadInteraction.forceSave = true
2731
this.initialPageLoadInteraction.done(loadEventTime)
28-
this.interactionsToHarvest.add(this.initialPageLoadInteraction)
29-
this.initialPageLoadInteraction = null
3032
// Report metric on the initial page load time
3133
handle(SUPPORTABILITY_METRIC_CHANNEL, ['SoftNav/Interaction/InitialPageLoad/Duration/Ms', Math.round(loadEventTime)], undefined, FEATURE_NAMES.metrics, this.ee)
3234
})
3335

3436
this.latestRouteSetByApi = null
3537
this.interactionInProgress = null // aside from the "page load" interaction, there can only ever be 1 ongoing at a time
38+
this.latestHistoryUrl = null
3639

3740
this.blocked = false
3841
this.waitForFlags(['spa']).then(([spaOn]) => {
@@ -53,7 +56,11 @@ export class Aggregate extends AggregateBase {
5356

5457
// By default, a complete UI driven interaction requires event -> URL change -> DOM mod in that exact order.
5558
registerHandler('newUIEvent', (event) => this.startUIInteraction(event.type, Math.floor(event.timeStamp), event.target), this.featureName, this.ee)
56-
registerHandler('newURL', (timestamp, url) => this.interactionInProgress?.updateHistory(timestamp, url), this.featureName, this.ee)
59+
registerHandler('newURL', (timestamp, url) => {
60+
// In the case of 'popstate' trigger, by the time the event fires, the URL has already changed, so we need to store what-will-be the *previous* URL for oldURL of next popstate ixn.
61+
this.latestHistoryUrl = url
62+
this.interactionInProgress?.updateHistory(timestamp, url)
63+
}, this.featureName, this.ee)
5764
registerHandler('newDom', timestamp => {
5865
this.interactionInProgress?.updateDom(timestamp)
5966
if (this.interactionInProgress?.seenHistoryAndDomChange()) this.interactionInProgress.done()
@@ -68,21 +75,23 @@ export class Aggregate extends AggregateBase {
6875
serializer (eventBuffer) {
6976
// The payload depacker takes the first ixn of a payload (if there are multiple ixns) and positively offset the subsequent ixns timestamps by that amount.
7077
// In order to accurately portray the real start & end times of the 2nd & onward ixns, we hence need to negatively offset their start timestamps with that of the 1st ixn.
71-
let firstIxnStartTime = 0 // the very 1st ixn does not require any offsetting
78+
let firstIxnStartTime
7279
const serializedIxnList = []
7380
for (const interaction of eventBuffer) {
7481
serializedIxnList.push(interaction.serialize(firstIxnStartTime))
75-
if (!firstIxnStartTime) firstIxnStartTime = Math.floor(interaction.start)
82+
if (firstIxnStartTime === undefined) firstIxnStartTime = Math.floor(interaction.start) // careful not to match or overwrite on 0 value!
7683
}
7784
return `bel.7;${serializedIxnList.join(';')}`
7885
}
7986

8087
startUIInteraction (eventName, startedAt, sourceElem) { // this is throttled by instrumentation so that it isn't excessively called
8188
if (this.interactionInProgress?.createdByApi) return // api-started interactions cannot be disrupted aka cancelled by UI events (and the vice versa applies as well)
82-
if (this.interactionInProgress?.done() === false) return
89+
if (this.interactionInProgress?.done() === false) return // current in-progress is blocked from closing, e.g. by 'waitForEnd' api option
90+
91+
const oldURL = eventName === INTERACTION_TRIGGERS[3] ? this.latestHistoryUrl : undefined // see related comment in 'newURL' handler above, 'popstate'
92+
this.interactionInProgress = new Interaction(this.agentIdentifier, eventName, startedAt, this.latestRouteSetByApi, oldURL)
8393

84-
this.interactionInProgress = new Interaction(this.agentIdentifier, eventName, startedAt, this.latestRouteSetByApi)
85-
if (eventName === 'click') {
94+
if (eventName === INTERACTION_TRIGGERS[0]) { // 'click'
8695
const sourceElemText = getActionText(sourceElem)
8796
if (sourceElemText) this.interactionInProgress.customAttributes.actionText = sourceElemText
8897
}
@@ -131,7 +140,7 @@ export class Aggregate extends AggregateBase {
131140
for (let idx = interactionsBuffer.length - 1; idx >= 0; idx--) { // reverse search for the latest completed interaction for efficiency
132141
const finishedInteraction = interactionsBuffer[idx]
133142
if (finishedInteraction.isActiveDuring(timestamp)) {
134-
if (finishedInteraction.trigger !== 'initialPageLoad') return finishedInteraction
143+
if (finishedInteraction.trigger !== IPL_TRIGGER_NAME) return finishedInteraction
135144
// It's possible that a complete interaction occurs before page is fully loaded, so we need to consider if a route-change ixn may have overlapped this iPL
136145
else saveIxn = finishedInteraction
137146
}
@@ -196,9 +205,11 @@ export class Aggregate extends AggregateBase {
196205
// In here, 'this' refers to the EventContext specific to per InteractionHandle instance spawned by each .interaction() api call.
197206
// Each api call aka IH instance would therefore retain a reference to either the in-progress interaction *at the time of the call* OR a new api-started interaction.
198207
this.associatedInteraction = thisClass.getInteractionFor(time)
208+
if (this.associatedInteraction?.trigger === IPL_TRIGGER_NAME) this.associatedInteraction = null // the api get-interaction method cannot target IPL
199209
if (!this.associatedInteraction) {
200210
// This new api-driven interaction will be the target of any subsequent .interaction() call, until it is closed by EITHER .end() OR the regular seenHistoryAndDomChange process.
201211
this.associatedInteraction = thisClass.interactionInProgress = new Interaction(thisClass.agentIdentifier, API_TRIGGER_NAME, time, thisClass.latestRouteSetByApi)
212+
thisClass.domObserver.observe(document.body, { attributes: true, childList: true, subtree: true, characterData: true }) // start observing for DOM changes like a regular UI-driven interaction
202213
thisClass.setClosureHandlers()
203214
}
204215
if (waitForEnd === true) this.associatedInteraction.keepOpenUntilEndApi = true

src/features/soft_navigations/aggregate/initial-page-load-interaction.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@ import { numeric } from '../../../common/serialize/bel-serializer'
44
import { firstPaint } from '../../../common/vitals/first-paint'
55
import { firstContentfulPaint } from '../../../common/vitals/first-contentful-paint'
66
import { getInfo } from '../../../common/config/info'
7+
import { IPL_TRIGGER_NAME } from '../constants'
78

89
export class InitialPageLoadInteraction extends Interaction {
910
constructor (agentIdentifier) {
10-
super(agentIdentifier, 'initialPageLoad', 0, null)
11+
super(agentIdentifier, IPL_TRIGGER_NAME, 0, null)
1112
const agentInfo = getInfo(agentIdentifier)
1213
this.queueTime = agentInfo.queueTime
1314
this.appTime = agentInfo.applicationTime

0 commit comments

Comments
 (0)