@newrelic/browser-agent 1.320.0 → 1.320.1-rc.1
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 +8 -0
- package/dist/cjs/common/config/runtime.js +1 -0
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/event-listener/event-listener-opts.js +1 -1
- package/dist/cjs/common/harvest/connector.js +200 -0
- package/dist/cjs/common/harvest/harvester.js +5 -8
- package/dist/cjs/common/harvest/send.js +8 -7
- package/dist/cjs/common/payloads/payloads.js +3 -2
- package/dist/cjs/common/serialize/bel-serializer.js +17 -4
- package/dist/cjs/common/timer/interaction-timer.js +2 -2
- package/dist/cjs/common/unload/eol.js +3 -3
- package/dist/cjs/common/util/console.js +2 -1
- package/dist/cjs/common/util/feature-flags.js +2 -1
- package/dist/cjs/common/v2/mfe-vitals.js +34 -11
- package/dist/cjs/common/window/page-visibility.js +1 -1
- package/dist/cjs/features/ajax/aggregate/index.js +19 -8
- package/dist/cjs/features/page_view_event/aggregate/index.js +2 -6
- package/dist/cjs/features/page_view_event/aggregate_v2/index.js +109 -0
- package/dist/cjs/features/page_view_event/instrument/index.js +6 -1
- package/dist/cjs/features/page_view_timing/aggregate/index.js +33 -14
- package/dist/cjs/features/session_replay/instrument/index.js +9 -0
- package/dist/cjs/features/session_replay/shared/recorder.js +2 -1
- package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +16 -4
- package/dist/cjs/features/soft_navigations/aggregate/interaction.js +10 -3
- package/dist/cjs/features/utils/aggregate-base.js +0 -12
- package/dist/cjs/features/utils/instrument-base.js +72 -29
- package/dist/cjs/loaders/agent.js +2 -1
- package/dist/cjs/loaders/api/consent.js +2 -2
- package/dist/cjs/loaders/features/features.js +2 -1
- package/dist/esm/common/config/runtime.js +1 -0
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/event-listener/event-listener-opts.js +1 -1
- package/dist/esm/common/harvest/connector.js +193 -0
- package/dist/esm/common/harvest/harvester.js +5 -8
- package/dist/esm/common/harvest/send.js +10 -9
- package/dist/esm/common/payloads/payloads.js +3 -2
- package/dist/esm/common/serialize/bel-serializer.js +17 -4
- package/dist/esm/common/timer/interaction-timer.js +2 -2
- package/dist/esm/common/unload/eol.js +3 -3
- package/dist/esm/common/util/console.js +2 -1
- package/dist/esm/common/util/feature-flags.js +2 -1
- package/dist/esm/common/v2/mfe-vitals.js +34 -11
- package/dist/esm/common/window/page-visibility.js +1 -1
- package/dist/esm/features/ajax/aggregate/index.js +19 -8
- package/dist/esm/features/page_view_event/aggregate/index.js +2 -6
- package/dist/esm/features/page_view_event/aggregate_v2/index.js +100 -0
- package/dist/esm/features/page_view_event/instrument/index.js +6 -1
- package/dist/esm/features/page_view_timing/aggregate/index.js +33 -14
- package/dist/esm/features/session_replay/instrument/index.js +9 -0
- package/dist/esm/features/session_replay/shared/recorder.js +2 -1
- package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +16 -4
- package/dist/esm/features/soft_navigations/aggregate/interaction.js +10 -3
- package/dist/esm/features/utils/aggregate-base.js +0 -12
- package/dist/esm/features/utils/instrument-base.js +71 -28
- package/dist/esm/loaders/agent.js +2 -1
- package/dist/esm/loaders/api/consent.js +2 -2
- package/dist/esm/loaders/features/features.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/common/config/runtime.d.ts.map +1 -1
- package/dist/types/common/event-listener/event-listener-opts.d.ts +1 -1
- package/dist/types/common/harvest/connector.d.ts +6 -0
- package/dist/types/common/harvest/connector.d.ts.map +1 -0
- package/dist/types/common/harvest/harvester.d.ts.map +1 -1
- package/dist/types/common/harvest/send.d.ts.map +1 -1
- package/dist/types/common/payloads/payloads.d.ts +3 -2
- package/dist/types/common/payloads/payloads.d.ts.map +1 -1
- package/dist/types/common/serialize/bel-serializer.d.ts +9 -1
- package/dist/types/common/serialize/bel-serializer.d.ts.map +1 -1
- package/dist/types/common/unload/eol.d.ts +1 -1
- package/dist/types/common/unload/eol.d.ts.map +1 -1
- package/dist/types/common/util/console.d.ts +2 -1
- package/dist/types/common/util/console.d.ts.map +1 -1
- package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -1
- package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_event/aggregate_v2/index.d.ts +20 -0
- package/dist/types/features/page_view_event/aggregate_v2/index.d.ts.map +1 -0
- package/dist/types/features/page_view_event/instrument/index.d.ts.map +1 -1
- package/dist/types/features/page_view_timing/aggregate/index.d.ts +0 -1
- package/dist/types/features/page_view_timing/aggregate/index.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.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
- package/dist/types/features/utils/aggregate-base.d.ts +0 -5
- package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
- package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
- package/dist/types/loaders/agent.d.ts.map +1 -1
- package/dist/types/loaders/features/features.d.ts +1 -0
- package/dist/types/loaders/features/features.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/common/config/runtime.js +1 -0
- package/src/common/event-listener/event-listener-opts.js +1 -1
- package/src/common/harvest/connector.js +168 -0
- package/src/common/harvest/harvester.js +7 -9
- package/src/common/harvest/send.js +9 -7
- package/src/common/payloads/payloads.js +3 -2
- package/src/common/serialize/bel-serializer.js +13 -4
- package/src/common/timer/interaction-timer.js +2 -2
- package/src/common/unload/eol.js +3 -3
- package/src/common/util/console.js +2 -1
- package/src/common/util/feature-flags.js +1 -1
- package/src/common/v2/mfe-vitals.js +39 -12
- package/src/common/window/page-visibility.js +1 -1
- package/src/features/ajax/aggregate/index.js +12 -9
- package/src/features/page_view_event/aggregate/index.js +2 -6
- package/src/features/page_view_event/aggregate_v2/index.js +97 -0
- package/src/features/page_view_event/instrument/index.js +6 -1
- package/src/features/page_view_timing/aggregate/index.js +24 -15
- package/src/features/session_replay/instrument/index.js +9 -0
- package/src/features/session_replay/shared/recorder.js +2 -1
- package/src/features/soft_navigations/aggregate/ajax-node.js +10 -7
- package/src/features/soft_navigations/aggregate/interaction.js +6 -5
- package/src/features/utils/aggregate-base.js +0 -12
- package/src/features/utils/instrument-base.js +71 -25
- package/src/loaders/agent.js +2 -1
- package/src/loaders/api/consent.js +2 -2
- package/src/loaders/features/features.js +1 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { isBrowserScope } from '../constants/runtime';
|
|
7
|
+
import { send } from './send';
|
|
8
|
+
import { TimeKeeper } from '../timing/time-keeper';
|
|
9
|
+
import { activateFeatures } from '../util/feature-flags';
|
|
10
|
+
import { now } from '../timing/now';
|
|
11
|
+
import { warn } from '../util/console';
|
|
12
|
+
import { VERSION } from "../constants/env.npm";
|
|
13
|
+
import { CONNECT, FEATURE_NAMES, FEATURE_TO_ENDPOINT } from '../../loaders/features/features';
|
|
14
|
+
import { handle } from '../event-emitter/handle';
|
|
15
|
+
import { SUPPORTABILITY_METRIC_CHANNEL } from '../../features/metrics/constants';
|
|
16
|
+
const MAX_CONNECT_ATTEMPTS = 3;
|
|
17
|
+
const CONNECT_RETRY_BASE_MS = 3000; // affects each retry window proportionally (attempt 2: 0-3s, 3: 0-6s, 4: 0-12s, etc)
|
|
18
|
+
|
|
19
|
+
export class Connector {
|
|
20
|
+
#agentRef;
|
|
21
|
+
#connectStartTime;
|
|
22
|
+
#numOfAttempts = 0;
|
|
23
|
+
constructor(agentRef) {
|
|
24
|
+
if (!agentRef.init.feature_flags.includes('rum_v2')) return; // the Connector does not run in v1; legacy PVE has its own BCS handling
|
|
25
|
+
this.#agentRef = agentRef;
|
|
26
|
+
agentRef.runtime.timeKeeper = new TimeKeeper(agentRef.runtime.session);
|
|
27
|
+
if (isBrowserScope) {
|
|
28
|
+
const cached = agentRef.runtime.session && agentRef.runtime.session.state.cachedRumResponse;
|
|
29
|
+
if (cached) {
|
|
30
|
+
// `cachedRumResponse` is stored flat (`{ app, ...flags }`, matching the old v1 RUM response), so re-nest it before applying.
|
|
31
|
+
const {
|
|
32
|
+
app,
|
|
33
|
+
...config
|
|
34
|
+
} = cached;
|
|
35
|
+
this.#applyConnectResponse({
|
|
36
|
+
app,
|
|
37
|
+
config
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
this.makeConnectRequest();
|
|
43
|
+
}
|
|
44
|
+
makeConnectRequest(retryHeaders) {
|
|
45
|
+
this.#connectStartTime = now();
|
|
46
|
+
return send(this.#agentRef, {
|
|
47
|
+
endpoint: "browser/connect/2/".concat(this.#agentRef.info.licenseKey),
|
|
48
|
+
payload: {
|
|
49
|
+
body: {},
|
|
50
|
+
qs: {
|
|
51
|
+
a: this.#agentRef.info.applicationID,
|
|
52
|
+
v: VERSION,
|
|
53
|
+
s: this.#agentRef.runtime.session?.state.value || '0'
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
localOpts: {
|
|
57
|
+
sendEmptyBody: true,
|
|
58
|
+
headers: retryHeaders
|
|
59
|
+
},
|
|
60
|
+
raw: true,
|
|
61
|
+
featureName: CONNECT,
|
|
62
|
+
cbFinished: this.#processConnectResponse.bind(this)
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
#applyConnectResponse({
|
|
66
|
+
app,
|
|
67
|
+
config
|
|
68
|
+
}) {
|
|
69
|
+
if (!Object.keys(this.#agentRef.runtime.appMetadata).length) this.#agentRef.runtime.appMetadata = app;
|
|
70
|
+
activateFeatures(config, this.#agentRef);
|
|
71
|
+
}
|
|
72
|
+
#processConnectResponse({
|
|
73
|
+
sent,
|
|
74
|
+
status,
|
|
75
|
+
retry,
|
|
76
|
+
xhr,
|
|
77
|
+
responseText
|
|
78
|
+
}) {
|
|
79
|
+
const connectEndTime = now();
|
|
80
|
+
const session = this.#agentRef.runtime.session;
|
|
81
|
+
if (session) {
|
|
82
|
+
// Double check that there isn't a response saved (e.g. from another agent/browser tab) btwn the async time of the request and now in a race-condition.
|
|
83
|
+
const cachedResp = session.state.cachedRumResponse;
|
|
84
|
+
if (cachedResp) {
|
|
85
|
+
// `cachedRumResponse` is stored flat (`{ app, ...flags }`, matching the old v1 RUM response), so re-nest it before applying.
|
|
86
|
+
const {
|
|
87
|
+
app,
|
|
88
|
+
...config
|
|
89
|
+
} = cachedResp;
|
|
90
|
+
this.#applyConnectResponse({
|
|
91
|
+
app,
|
|
92
|
+
config
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const shouldRetry = sent && retry; // important to note: no retry if HTTP status is 0
|
|
98
|
+
if (shouldRetry && ++this.#numOfAttempts < MAX_CONNECT_ATTEMPTS) {
|
|
99
|
+
// We use an exponential backoff calculation for the retry to avoid thundering herd, skipping any further processing.
|
|
100
|
+
const delay = Math.floor(Math.random() * CONNECT_RETRY_BASE_MS * 2 ** (this.#numOfAttempts - 1));
|
|
101
|
+
const retryHeaders = [{
|
|
102
|
+
key: 'X-Retry-Count',
|
|
103
|
+
value: this.#numOfAttempts
|
|
104
|
+
}, {
|
|
105
|
+
key: 'X-Previous-Status',
|
|
106
|
+
value: status
|
|
107
|
+
}];
|
|
108
|
+
setTimeout(() => this.makeConnectRequest(retryHeaders), delay);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (status >= 400 || status === 0) {
|
|
112
|
+
warn(18, status);
|
|
113
|
+
|
|
114
|
+
// Get estimated payload size of our backlog.
|
|
115
|
+
const textEncoder = new TextEncoder();
|
|
116
|
+
const payloadSize = Object.values(newrelic.ee.backlog).reduce((acc, value) => {
|
|
117
|
+
if (!value) return acc;
|
|
118
|
+
const encoded = textEncoder.encode(value);
|
|
119
|
+
return acc + encoded.byteLength;
|
|
120
|
+
}, 0);
|
|
121
|
+
|
|
122
|
+
// Send SMs about the (last) failed connect request, then abort agent.
|
|
123
|
+
const BCSError = 'BCS/Error/';
|
|
124
|
+
const sm = [{
|
|
125
|
+
params: {
|
|
126
|
+
name: BCSError + status
|
|
127
|
+
},
|
|
128
|
+
stats: {
|
|
129
|
+
c: 1
|
|
130
|
+
}
|
|
131
|
+
}, {
|
|
132
|
+
params: {
|
|
133
|
+
name: BCSError + 'Duration/Ms'
|
|
134
|
+
},
|
|
135
|
+
stats: {
|
|
136
|
+
c: 1,
|
|
137
|
+
t: connectEndTime - this.#connectStartTime
|
|
138
|
+
}
|
|
139
|
+
}, {
|
|
140
|
+
params: {
|
|
141
|
+
name: BCSError + 'Dropped/Bytes'
|
|
142
|
+
},
|
|
143
|
+
stats: {
|
|
144
|
+
c: 1,
|
|
145
|
+
t: payloadSize
|
|
146
|
+
}
|
|
147
|
+
}];
|
|
148
|
+
send(this.#agentRef, {
|
|
149
|
+
// note we have to send it this way because the Metrics feature will never be harvested under this branch of code (BCS failure)
|
|
150
|
+
endpoint: FEATURE_TO_ENDPOINT[FEATURE_NAMES.metrics],
|
|
151
|
+
payload: {
|
|
152
|
+
body: {
|
|
153
|
+
sm
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
featureName: FEATURE_NAMES.metrics
|
|
157
|
+
});
|
|
158
|
+
this.#agentRef.ee.abort();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
let resp;
|
|
162
|
+
try {
|
|
163
|
+
resp = JSON.parse(responseText);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
warn(53, error);
|
|
166
|
+
this.#agentRef.ee.abort();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// store the response for other pages later in same session or in parallel tabs; flatten `.config` into the old v1 RUM response shape
|
|
170
|
+
if (session) session.write({
|
|
171
|
+
cachedRumResponse: {
|
|
172
|
+
app: resp.app,
|
|
173
|
+
...resp.config
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
try {
|
|
177
|
+
const wasReady = this.#agentRef.runtime.timeKeeper.ready;
|
|
178
|
+
this.#agentRef.runtime.timeKeeper.processRumRequest(xhr, this.#connectStartTime, connectEndTime, resp.app.nrServerTime);
|
|
179
|
+
if (!this.#agentRef.runtime.timeKeeper.ready) throw new Error('TimeKeeper not ready');
|
|
180
|
+
|
|
181
|
+
// If timeKeeper's origin time is ahead of nrServerTime, then the timestamp is invalid. Report a supportability metric.
|
|
182
|
+
const timeDiff = this.#agentRef.runtime.timeKeeper.correctedOriginTime - resp.app.nrServerTime;
|
|
183
|
+
if (wasReady && timeDiff > 0) {
|
|
184
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, ['Generic/TimeKeeper/InvalidTimestamp/Seen', timeDiff], undefined, FEATURE_NAMES.metrics, this.#agentRef.ee);
|
|
185
|
+
}
|
|
186
|
+
} catch (error) {
|
|
187
|
+
warn(17, error);
|
|
188
|
+
this.#agentRef.ee.abort();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
this.#applyConnectResponse(resp);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -19,11 +19,13 @@ export class Harvester {
|
|
|
19
19
|
constructor(agentRef) {
|
|
20
20
|
this.agentRef = agentRef;
|
|
21
21
|
|
|
22
|
-
// Create obfuscator for harvest metadata (referrer URL, etc.)
|
|
23
22
|
// No event type specified - applies to all harvests regardless of feature
|
|
24
23
|
this.obfuscator = new Obfuscator(agentRef);
|
|
24
|
+
}
|
|
25
|
+
startTimer(harvestInterval = this.agentRef.init.harvest.interval) {
|
|
26
|
+
if (this.#started) return;
|
|
27
|
+
this.#started = true;
|
|
25
28
|
subscribeToEOL(() => {
|
|
26
|
-
// do one last harvest round or check
|
|
27
29
|
this.initializedAggregates.forEach(aggregateInst => {
|
|
28
30
|
// let all features wrap up things needed to do before ANY harvest in case there's last minute cross-feature data dependencies
|
|
29
31
|
if (typeof aggregateInst.harvestOpts.beforeUnload === 'function') aggregateInst.harvestOpts.beforeUnload();
|
|
@@ -31,12 +33,7 @@ export class Harvester {
|
|
|
31
33
|
this.initializedAggregates.forEach(aggregateInst => this.triggerHarvestFor(aggregateInst, {
|
|
32
34
|
isFinalHarvest: true
|
|
33
35
|
}));
|
|
34
|
-
|
|
35
|
-
}, false);
|
|
36
|
-
}
|
|
37
|
-
startTimer(harvestInterval = this.agentRef.init.harvest.interval) {
|
|
38
|
-
if (this.#started) return;
|
|
39
|
-
this.#started = true;
|
|
36
|
+
});
|
|
40
37
|
const onHarvestInterval = () => {
|
|
41
38
|
this.initializedAggregates.forEach(aggregateInst => this.triggerHarvestFor(aggregateInst));
|
|
42
39
|
setTimeout(onHarvestInterval, harvestInterval * 1000); // repeat in X seconds
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
|
-
import { JSERRORS, RUM, EVENTS, FEATURE_NAMES, BLOBS, LOGS } from '../../loaders/features/features';
|
|
5
|
+
import { JSERRORS, RUM, EVENTS, FEATURE_NAMES, BLOBS, LOGS, CONNECT } from '../../loaders/features/features';
|
|
6
6
|
import { VERSION } from "../constants/env.npm";
|
|
7
7
|
import { globalScope, isWorkerScope } from '../constants/runtime';
|
|
8
8
|
import { handle } from '../event-emitter/handle';
|
|
@@ -12,7 +12,7 @@ import { cleanURL } from '../url/clean-url';
|
|
|
12
12
|
import { obj, param } from '../url/encode';
|
|
13
13
|
import { warn } from '../util/console';
|
|
14
14
|
import { stringify } from '../util/stringify';
|
|
15
|
-
import { xhr as xhrMethod, xhrFetch as fetchMethod } from '../util/submit-data';
|
|
15
|
+
import { xhr as xhrMethod, xhrFetch as fetchMethod, getSubmitMethod } from '../util/submit-data';
|
|
16
16
|
import { dispatchGlobalEvent } from '../dispatch/global-event';
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -31,7 +31,7 @@ export function send(agentRef, {
|
|
|
31
31
|
endpoint,
|
|
32
32
|
payload,
|
|
33
33
|
localOpts = {},
|
|
34
|
-
submitMethod,
|
|
34
|
+
submitMethod = getSubmitMethod(),
|
|
35
35
|
cbFinished,
|
|
36
36
|
raw,
|
|
37
37
|
featureName,
|
|
@@ -52,13 +52,15 @@ export function send(agentRef, {
|
|
|
52
52
|
}
|
|
53
53
|
const protocol = agentRef.init.ssl === false ? 'http' : 'https';
|
|
54
54
|
const perceivedBeacon = agentRef.init.proxy.beacon || agentRef.info.errorBeacon;
|
|
55
|
-
const
|
|
55
|
+
const includeEndpointName = endpoint !== RUM || endpointVersion !== 1;
|
|
56
|
+
const url = raw ? "".concat(protocol, "://").concat(perceivedBeacon, "/").concat(endpoint) : "".concat(protocol, "://").concat(perceivedBeacon).concat(includeEndpointName ? '/' + endpoint : '', "/").concat(endpointVersion, "/").concat(agentRef.info.licenseKey);
|
|
56
57
|
const baseParams = !raw ? baseQueryString(agentRef, qs, endpoint, harvesterObfuscator) : '';
|
|
57
58
|
let payloadParams = obj(qs, agentRef.runtime.maxBytes);
|
|
58
59
|
if (baseParams === '' && payloadParams.startsWith('&')) {
|
|
59
60
|
payloadParams = payloadParams.substring(1);
|
|
60
61
|
}
|
|
61
|
-
const
|
|
62
|
+
const queryString = "".concat(baseParams).concat(payloadParams);
|
|
63
|
+
const fullUrl = queryString ? "".concat(url, "?").concat(queryString) : url; // omit the '?' if there are no query params
|
|
62
64
|
const gzip = !!qs?.attributes?.includes('gzip');
|
|
63
65
|
|
|
64
66
|
// all gzipped data is already in the correct format and needs no transformation
|
|
@@ -73,7 +75,7 @@ export function send(agentRef, {
|
|
|
73
75
|
const headers = [{
|
|
74
76
|
key: 'content-type',
|
|
75
77
|
value: 'text/plain'
|
|
76
|
-
}];
|
|
78
|
+
}, ...(localOpts.headers || [])];
|
|
77
79
|
|
|
78
80
|
/* Since workers don't support sendBeacon right now, they can only use XHR method.
|
|
79
81
|
Because they still do permit synch XHR, the idea is that at final harvest time (worker is closing),
|
|
@@ -122,6 +124,7 @@ export function send(agentRef, {
|
|
|
122
124
|
}
|
|
123
125
|
function trackHarvestMetadata() {
|
|
124
126
|
try {
|
|
127
|
+
if (featureName === CONNECT) return;
|
|
125
128
|
if (featureName === FEATURE_NAMES.jserrors && !body?.err) return;
|
|
126
129
|
const hasReplay = baseParams.includes('hr=1');
|
|
127
130
|
const hasTrace = baseParams.includes('ht=1');
|
|
@@ -207,9 +210,7 @@ function baseQueryString(agentRef, qs, endpoint, harvesterObfuscator) {
|
|
|
207
210
|
const session = agentRef.runtime.session;
|
|
208
211
|
const hr = !!session?.state.sessionReplaySentFirstChunk && session?.state.sessionReplayMode === 1 && endpoint !== JSERRORS;
|
|
209
212
|
const ht = !!session?.state.traceHarvestStarted && session?.state.sessionTraceMode === 1 && ![LOGS, BLOBS].includes(endpoint);
|
|
210
|
-
const qps = ['a=' + agentRef.info.applicationID, param('sa', agentRef.info.sa ? '' + agentRef.info.sa : ''), param('v', VERSION), transactionNameParam(), param('ct', agentRef.runtime.customTransaction), '&rst=' + now(), '&
|
|
211
|
-
// ck param DEPRECATED - still expected by backend
|
|
212
|
-
'&s=' + (session?.state.value || '0'),
|
|
213
|
+
const qps = ['a=' + agentRef.info.applicationID, param('sa', agentRef.info.sa ? '' + agentRef.info.sa : ''), param('v', VERSION), transactionNameParam(), param('ct', agentRef.runtime.customTransaction), '&rst=' + now(), '&s=' + (session?.state.value || '0'),
|
|
213
214
|
// the 0 id encaps all untrackable and default traffic
|
|
214
215
|
param('ref', ref), param('ptid', agentRef.runtime.ptid ? '' + agentRef.runtime.ptid : '')];
|
|
215
216
|
if (hr) qps.push(param('hr', '1', qs));
|
|
@@ -114,11 +114,11 @@ export function truncateAsString(data) {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
|
-
* Creates string adder functions for BEL serialization with obfuscation and optional truncation.
|
|
117
|
+
* Creates string adder functions for BEL serialization with obfuscation and optional truncation, as well as raw string.
|
|
118
118
|
* This ensures a single string table is used while providing separate handling for regular vs payload attributes.
|
|
119
119
|
* @param {Function} getAddStringContext - Function that creates a new string table context
|
|
120
120
|
* @param {Object} obfuscator - Optional obfuscator instance for string obfuscation
|
|
121
|
-
* @returns {{addString: Function, addStringWithTruncation: Function}} Object containing
|
|
121
|
+
* @returns {{addString: Function, addStringRaw: Function, addStringWithTruncation: Function}} Object containing various string adder functions
|
|
122
122
|
*/
|
|
123
123
|
export function createStringAdders(getAddStringContext, obfuscator) {
|
|
124
124
|
const addStringRaw = getAddStringContext();
|
|
@@ -131,6 +131,7 @@ export function createStringAdders(getAddStringContext, obfuscator) {
|
|
|
131
131
|
};
|
|
132
132
|
return {
|
|
133
133
|
addString: str => processString(str, false),
|
|
134
|
+
addStringRaw: str => addStringRaw(str),
|
|
134
135
|
addStringWithTruncation: str => processString(str, true)
|
|
135
136
|
};
|
|
136
137
|
}
|
|
@@ -31,19 +31,32 @@ export function getAddStringContext(obfuscator, truncator) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Attributes are serialized according to their types, using the provided serializers. Only the first 64 attributes will be added to the payload.
|
|
37
|
+
* @param attrs
|
|
38
|
+
* @param serializers
|
|
39
|
+
* @param {function} serializers.addKey -function for serializing attribute keys
|
|
40
|
+
* @param {function} serializers.addVal - function for serializing attribute values
|
|
41
|
+
* @returns {*[]}
|
|
42
|
+
*/
|
|
43
|
+
export function addCustomAttributes(attrs, serializers) {
|
|
44
|
+
const {
|
|
45
|
+
addKey,
|
|
46
|
+
addVal
|
|
47
|
+
} = serializers;
|
|
35
48
|
var attrParts = [];
|
|
36
49
|
Object.entries(attrs || {}).forEach(([key, val]) => {
|
|
37
50
|
if (attrParts.length >= MAX_ATTRIBUTES) return;
|
|
38
51
|
var type = 5;
|
|
39
52
|
var serializedValue;
|
|
40
53
|
// add key to string table first
|
|
41
|
-
key =
|
|
54
|
+
key = addKey(key);
|
|
42
55
|
switch (typeof val) {
|
|
43
56
|
case 'object':
|
|
44
57
|
if (val) {
|
|
45
58
|
// serialize objects to strings
|
|
46
|
-
serializedValue =
|
|
59
|
+
serializedValue = addVal(stringify(val));
|
|
47
60
|
} else {
|
|
48
61
|
// null attribute type
|
|
49
62
|
type = 9;
|
|
@@ -62,7 +75,7 @@ export function addCustomAttributes(attrs, addString) {
|
|
|
62
75
|
type = 9;
|
|
63
76
|
break;
|
|
64
77
|
default:
|
|
65
|
-
serializedValue =
|
|
78
|
+
serializedValue = addVal(val);
|
|
66
79
|
}
|
|
67
80
|
attrParts.push([type, key + (serializedValue ? ',' + serializedValue : '')]);
|
|
68
81
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Copyright 2020-
|
|
2
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
import { Timer } from './timer';
|
|
@@ -52,7 +52,7 @@ export class InteractionTimer extends Timer {
|
|
|
52
52
|
if (state === 'hidden') this.pause();
|
|
53
53
|
// vis change --> visible is treated like a new interaction with the page
|
|
54
54
|
else this.resume();
|
|
55
|
-
}, false,
|
|
55
|
+
}, false, this.abortController?.signal);
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
abort() {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Copyright 2020-
|
|
2
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
import { globalScope, isWorkerScope, isBrowserScope } from '../constants/runtime';
|
|
@@ -22,9 +22,9 @@ if (isWorkerScope) {
|
|
|
22
22
|
* This is used, for example, to submit a final harvest and send all remaining data on best-effort.
|
|
23
23
|
* @param {function} cb - func to run before or during the last reliable event or time of an env's life span
|
|
24
24
|
*/
|
|
25
|
-
export function subscribeToEOL(cb
|
|
25
|
+
export function subscribeToEOL(cb) {
|
|
26
26
|
if (isBrowserScope) {
|
|
27
|
-
subscribeToVisibilityChange(cb, true
|
|
27
|
+
subscribeToVisibilityChange(cb, true); // when user switches tab or hides window, esp. mobile scenario
|
|
28
28
|
} else if (isWorkerScope) {
|
|
29
29
|
globalScope.cleanupTasks.push(cb); // close() should run these tasks before quitting thread
|
|
30
30
|
}
|
|
@@ -68,7 +68,7 @@ import { dispatchGlobalEvent } from '../dispatch/global-event';
|
|
|
68
68
|
* | 50 | Failed to connect. Cannot allow registered API. |
|
|
69
69
|
* | 51 | Container agent is not available to register with. Can not connect. |
|
|
70
70
|
* | 52 | Unexpected problem encountered. There should be at least one app for harvest! |
|
|
71
|
-
* | 53 |
|
|
71
|
+
* | 53 | Failed to parse connect response. |
|
|
72
72
|
* | 54 | An experimental feature is being used. Support can not be offered for issues. |
|
|
73
73
|
* | 55 | Register API has been disabled on the container agent. |
|
|
74
74
|
* | 56 | Could not find a matching entity to store data. |
|
|
@@ -94,6 +94,7 @@ import { dispatchGlobalEvent } from '../dispatch/global-event';
|
|
|
94
94
|
* | 76 | Agent rejected post message, could not match with existing entity. |
|
|
95
95
|
* | 77 | Agent rejected post message, could not validate origin. |
|
|
96
96
|
* | 78 | RegisteredIframeEntity could not determine parent origin and will not register, to avoid trusting messages from any origin. |
|
|
97
|
+
* | 79 | Unable to initialize Connector and/or Harvester. |
|
|
97
98
|
*
|
|
98
99
|
* @param {number} code The warning code to emit, which will be used to link to the warning code documentation
|
|
99
100
|
* @param {*} [secondary] Secondary data to include, usually an extra message, error or object
|
|
@@ -19,7 +19,8 @@ export function activateFeatures(flags, agentRef) {
|
|
|
19
19
|
// let any window level subscribers know that the agent is running, per install docs
|
|
20
20
|
dispatchGlobalEvent({
|
|
21
21
|
loaded: true,
|
|
22
|
-
drained:
|
|
22
|
+
drained: !agentRef.init.feature_flags.includes('rum_v2'),
|
|
23
|
+
// v2 RUM always call activateFeatures before any group is drained (in waitForFlags)
|
|
23
24
|
type: 'lifecycle',
|
|
24
25
|
name: 'load',
|
|
25
26
|
feature: undefined,
|
|
@@ -127,25 +127,33 @@ const observePerformance = (observers, config, onEntry) => {
|
|
|
127
127
|
export function trackMFEVitals(target, timings) {
|
|
128
128
|
let fcpObservedAt;
|
|
129
129
|
let lcpObservedAt;
|
|
130
|
+
// Once disconnect() runs, no further vitals data should be written - even from an observer callback that was
|
|
131
|
+
// already in flight when disconnect() was called.
|
|
132
|
+
let disconnected = false;
|
|
130
133
|
const getTimeRelativeToScriptStart = capturedAt => {
|
|
131
134
|
if (capturedAt == null) return capturedAt;
|
|
132
135
|
return capturedAt - (timings?.scriptStart || timings?.registeredAt || 0);
|
|
133
136
|
};
|
|
134
137
|
const vitals = {
|
|
135
138
|
fcp: {
|
|
139
|
+
// A clock freeze/tab suspend between script registration and first paint can make this value huge once the
|
|
140
|
+
// tab resumes. Rather than report a multi-hour "FCP", drop it so bad data doesn't reach the backend.
|
|
136
141
|
get value() {
|
|
137
|
-
|
|
142
|
+
const relative = getTimeRelativeToScriptStart(fcpObservedAt);
|
|
143
|
+
return relative > CORRELATION_STALE_THRESHOLD_MS ? undefined : relative;
|
|
138
144
|
},
|
|
139
145
|
set value(v) {
|
|
140
|
-
fcpObservedAt = v;
|
|
146
|
+
if (!disconnected) fcpObservedAt = v;
|
|
141
147
|
}
|
|
142
148
|
},
|
|
143
149
|
lcp: {
|
|
150
|
+
// LCP can never occur before FCP, so if FCP was dropped as invalid, LCP is derived from the same untrustworthy clock and must be dropped too.
|
|
144
151
|
get value() {
|
|
152
|
+
if (vitals.fcp.value === undefined) return undefined;
|
|
145
153
|
return getTimeRelativeToScriptStart(lcpObservedAt);
|
|
146
154
|
},
|
|
147
155
|
set value(v) {
|
|
148
|
-
lcpObservedAt = v;
|
|
156
|
+
if (!disconnected) lcpObservedAt = v;
|
|
149
157
|
}
|
|
150
158
|
},
|
|
151
159
|
cls: {
|
|
@@ -165,15 +173,22 @@ export function trackMFEVitals(target, timings) {
|
|
|
165
173
|
if (!fcpObservedAt) vitals.disconnect();
|
|
166
174
|
}, CORRELATION_STALE_THRESHOLD_MS);
|
|
167
175
|
const populateVitalMinimums = () => {
|
|
168
|
-
|
|
176
|
+
if (disconnected) return;
|
|
177
|
+
if (fcpObservedAt == null) {
|
|
178
|
+
const capturedAt = now();
|
|
179
|
+
// If the very first observed render already exceeds the window, the clock is untrustworthy for this
|
|
180
|
+
// MFE (e.g. tab was frozen/suspended between registration and paint) - shut everything down instead
|
|
181
|
+
// of seeding vitals with data derived from it.
|
|
182
|
+
if (getTimeRelativeToScriptStart(capturedAt) > CORRELATION_STALE_THRESHOLD_MS) {
|
|
183
|
+
vitals.disconnect();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
fcpObservedAt = capturedAt;
|
|
187
|
+
}
|
|
169
188
|
lcpObservedAt ??= now();
|
|
170
189
|
vitals.cls.value ??= 0;
|
|
171
190
|
};
|
|
172
191
|
|
|
173
|
-
// if the MFE has already rendered something on the page before we could set up listeners, just populate vital minimums immediately
|
|
174
|
-
const existingRoots = globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
|
|
175
|
-
if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums();
|
|
176
|
-
|
|
177
192
|
// Track FCP - first contentful paint
|
|
178
193
|
const fcpObs = observeMutations(target, (_, obs) => {
|
|
179
194
|
// An observed "FCP" means _something_ rendered, so at minimum we can populate all the baseline values for the vitals
|
|
@@ -188,12 +203,13 @@ export function trackMFEVitals(target, timings) {
|
|
|
188
203
|
const observedElements = new Set();
|
|
189
204
|
try {
|
|
190
205
|
resizeObs = new globalScope.ResizeObserver(entries => {
|
|
206
|
+
if (disconnected) return;
|
|
191
207
|
entries.forEach(entry => {
|
|
192
208
|
try {
|
|
193
209
|
const size = entry.contentRect.width * entry.contentRect.height;
|
|
194
210
|
if (size > largestSize) {
|
|
195
211
|
largestSize = size;
|
|
196
|
-
|
|
212
|
+
vitals.lcp.value = now();
|
|
197
213
|
}
|
|
198
214
|
resizeObs.unobserve(entry.target);
|
|
199
215
|
} catch (e) {
|
|
@@ -254,7 +270,7 @@ export function trackMFEVitals(target, timings) {
|
|
|
254
270
|
type: 'layout-shift',
|
|
255
271
|
buffered: true
|
|
256
272
|
}, entry => {
|
|
257
|
-
if (entry.hadRecentInput) return;
|
|
273
|
+
if (disconnected || entry.hadRecentInput) return;
|
|
258
274
|
(entry.sources || []).some(source => {
|
|
259
275
|
if (isInMFE(source.node, target)) {
|
|
260
276
|
// an observed "CLS" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
|
|
@@ -272,7 +288,7 @@ export function trackMFEVitals(target, timings) {
|
|
|
272
288
|
buffered: true,
|
|
273
289
|
durationThreshold: 40
|
|
274
290
|
}, entry => {
|
|
275
|
-
if (!entry.interactionId || !isInMFE(entry.target, target)) return;
|
|
291
|
+
if (disconnected || !entry.interactionId || !isInMFE(entry.target, target)) return;
|
|
276
292
|
if (vitals.inp.value === undefined || entry.duration > vitals.inp.value) {
|
|
277
293
|
// an observed "INP" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
|
|
278
294
|
populateVitalMinimums();
|
|
@@ -290,6 +306,7 @@ export function trackMFEVitals(target, timings) {
|
|
|
290
306
|
|
|
291
307
|
// Disconnect all observers
|
|
292
308
|
vitals.disconnect = () => {
|
|
309
|
+
disconnected = true;
|
|
293
310
|
// Disconnect all observers
|
|
294
311
|
observers.forEach(obs => {
|
|
295
312
|
try {
|
|
@@ -336,5 +353,11 @@ export function trackMFEVitals(target, timings) {
|
|
|
336
353
|
passive: true
|
|
337
354
|
});
|
|
338
355
|
});
|
|
356
|
+
|
|
357
|
+
// If the MFE has already rendered something on the page before we could set up listeners, just populate vital
|
|
358
|
+
// minimums immediately. Done last, once `observers` is fully populated and `vitals.disconnect` has its real
|
|
359
|
+
// implementation, so a stale-clock detection here can actually tear everything down instead of no-op'ing.
|
|
360
|
+
const existingRoots = globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
|
|
361
|
+
if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums();
|
|
339
362
|
return vitals;
|
|
340
363
|
}
|
|
@@ -145,6 +145,7 @@ export class Aggregate extends AggregateBase {
|
|
|
145
145
|
if (!eventBuffer.length) return;
|
|
146
146
|
const {
|
|
147
147
|
addString,
|
|
148
|
+
addStringRaw,
|
|
148
149
|
addStringWithTruncation
|
|
149
150
|
} = createStringAdders(getAddStringContext, this.obfuscator);
|
|
150
151
|
let payload = 'bel.7;';
|
|
@@ -158,11 +159,11 @@ export class Aggregate extends AggregateBase {
|
|
|
158
159
|
// callbackEnd
|
|
159
160
|
numeric(0),
|
|
160
161
|
// no callbackDuration for non-SPA events
|
|
161
|
-
|
|
162
|
+
addStringRaw(event.method), numeric(event.status), addString(event.domain), addString(event.path), numeric(event.requestSize), numeric(event.responseSize), event.type === 'fetch' ? 1 : '', addStringRaw(0),
|
|
162
163
|
// nodeId
|
|
163
|
-
nullable(event.spanId,
|
|
164
|
+
nullable(event.spanId, addStringRaw, true) +
|
|
164
165
|
// guid
|
|
165
|
-
nullable(event.traceId,
|
|
166
|
+
nullable(event.traceId, addStringRaw, true) +
|
|
166
167
|
// traceId
|
|
167
168
|
nullable(event.spanTimestamp, numeric, false) // timestamp
|
|
168
169
|
];
|
|
@@ -172,14 +173,21 @@ export class Aggregate extends AggregateBase {
|
|
|
172
173
|
const jsAttributes = this.agentRef.info.jsAttributes;
|
|
173
174
|
|
|
174
175
|
// Regular attributes: obfuscate only
|
|
175
|
-
|
|
176
|
+
let regularAttrs = addCustomAttributes({
|
|
176
177
|
...(jsAttributes || {}),
|
|
177
|
-
[AJAX_ID]: event[AJAX_ID],
|
|
178
|
-
// all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
|
|
179
178
|
...(event.targetAttributes || {}),
|
|
180
179
|
// used to supply the version 2 attributes, either MFE target or duplication attributes for the main agent app
|
|
181
180
|
...(event.gql || {})
|
|
182
|
-
},
|
|
181
|
+
}, {
|
|
182
|
+
addKey: addStringRaw,
|
|
183
|
+
addVal: addString
|
|
184
|
+
});
|
|
185
|
+
regularAttrs.push(...addCustomAttributes({
|
|
186
|
+
[AJAX_ID]: event[AJAX_ID] // all AjaxRequest events should have a unique identifier to allow for easier grouping and analysis in the UI
|
|
187
|
+
}, {
|
|
188
|
+
addKey: addStringRaw,
|
|
189
|
+
addVal: addStringRaw
|
|
190
|
+
}));
|
|
183
191
|
|
|
184
192
|
// Payload attributes: obfuscate then truncate
|
|
185
193
|
const payloadAttrs = addCustomAttributes({
|
|
@@ -198,7 +206,10 @@ export class Aggregate extends AggregateBase {
|
|
|
198
206
|
...(event.responseHeaders ? {
|
|
199
207
|
responseHeaders: event.responseHeaders
|
|
200
208
|
} : {})
|
|
201
|
-
},
|
|
209
|
+
}, {
|
|
210
|
+
addKey: addStringRaw,
|
|
211
|
+
addVal: addStringWithTruncation
|
|
212
|
+
});
|
|
202
213
|
const attrParts = [...regularAttrs, ...payloadAttrs];
|
|
203
214
|
fields.unshift(numeric(attrParts.length));
|
|
204
215
|
insert += fields.join(',');
|
|
@@ -132,9 +132,7 @@ export class Aggregate extends AggregateBase {
|
|
|
132
132
|
return queryParameters;
|
|
133
133
|
};
|
|
134
134
|
this.events.add(body);
|
|
135
|
-
if (this.agentRef.runtime.harvester.triggerHarvestFor(this
|
|
136
|
-
sendEmptyBody: true
|
|
137
|
-
}).ranSend) this.sentRum = true;
|
|
135
|
+
if (this.agentRef.runtime.harvester.triggerHarvestFor(this).ranSend) this.sentRum = true;
|
|
138
136
|
}
|
|
139
137
|
serializer(eventBuffer) {
|
|
140
138
|
// this is necessary because PVE sends a single item rather than an array; in the case of undefined, this prevents sending [null] as body
|
|
@@ -175,9 +173,7 @@ export class Aggregate extends AggregateBase {
|
|
|
175
173
|
|
|
176
174
|
if (this.isRetrying && this.retries++ < 1) {
|
|
177
175
|
// Only retry once
|
|
178
|
-
setTimeout(() => this.agentRef.runtime.harvester.triggerHarvestFor(this,
|
|
179
|
-
sendEmptyBody: true
|
|
180
|
-
}), 5000); // Retry sending the RUM event after 5 seconds
|
|
176
|
+
setTimeout(() => this.agentRef.runtime.harvester.triggerHarvestFor(this), 5000); // Retry sending the RUM event after 5 seconds
|
|
181
177
|
return;
|
|
182
178
|
}
|
|
183
179
|
if (status >= 400 || status === 0) {
|