@newrelic/browser-agent 1.257.0 → 1.258.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/cjs/common/config/state/configurable.js +8 -5
- package/dist/cjs/common/config/state/init.js +0 -2
- package/dist/cjs/common/config/state/runtime.js +10 -8
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/constants/runtime.js +8 -2
- package/dist/cjs/common/session/constants.js +1 -0
- package/dist/cjs/common/session/session-entity.js +3 -0
- package/dist/cjs/common/timing/time-keeper.js +45 -9
- package/dist/cjs/common/vitals/time-to-first-byte.js +1 -1
- package/dist/cjs/common/vitals/vital-metric.js +1 -1
- package/dist/cjs/features/ajax/aggregate/chunk.js +50 -0
- package/dist/cjs/features/ajax/aggregate/index.js +131 -191
- package/dist/cjs/features/ajax/instrument/index.js +0 -3
- package/dist/cjs/features/jserrors/aggregate/index.js +1 -1
- package/dist/cjs/features/page_view_event/aggregate/index.js +3 -3
- package/dist/cjs/features/session_replay/aggregate/index.js +1 -0
- package/dist/cjs/features/session_replay/shared/utils.js +3 -3
- package/dist/cjs/features/session_trace/aggregate/index.js +3 -5
- package/dist/cjs/features/spa/instrument/index.js +0 -2
- package/dist/cjs/features/utils/agent-session.js +1 -5
- package/dist/cjs/loaders/api/apiAsync.js +5 -4
- package/dist/esm/common/config/state/configurable.js +8 -5
- package/dist/esm/common/config/state/init.js +0 -2
- package/dist/esm/common/config/state/runtime.js +11 -9
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/constants/runtime.js +7 -1
- package/dist/esm/common/session/constants.js +1 -0
- package/dist/esm/common/session/session-entity.js +3 -0
- package/dist/esm/common/timing/time-keeper.js +46 -9
- package/dist/esm/common/vitals/time-to-first-byte.js +2 -2
- package/dist/esm/common/vitals/vital-metric.js +1 -1
- package/dist/esm/features/ajax/aggregate/chunk.js +43 -0
- package/dist/esm/features/ajax/aggregate/index.js +130 -191
- package/dist/esm/features/ajax/instrument/index.js +1 -4
- package/dist/esm/features/jserrors/aggregate/index.js +1 -1
- package/dist/esm/features/page_view_event/aggregate/index.js +4 -4
- package/dist/esm/features/session_replay/aggregate/index.js +1 -0
- package/dist/esm/features/session_replay/shared/utils.js +4 -4
- package/dist/esm/features/session_trace/aggregate/index.js +3 -5
- package/dist/esm/features/spa/instrument/index.js +0 -2
- package/dist/esm/features/utils/agent-session.js +1 -5
- package/dist/esm/loaders/api/apiAsync.js +5 -4
- package/dist/types/common/config/state/configurable.d.ts.map +1 -1
- package/dist/types/common/config/state/init.d.ts.map +1 -1
- package/dist/types/common/config/state/runtime.d.ts.map +1 -1
- package/dist/types/common/constants/runtime.d.ts +6 -1
- package/dist/types/common/constants/runtime.d.ts.map +1 -1
- package/dist/types/common/session/constants.d.ts +1 -0
- package/dist/types/common/session/session-entity.d.ts.map +1 -1
- package/dist/types/common/timing/time-keeper.d.ts +1 -1
- package/dist/types/common/timing/time-keeper.d.ts.map +1 -1
- package/dist/types/features/ajax/aggregate/chunk.d.ts +8 -0
- package/dist/types/features/ajax/aggregate/chunk.d.ts.map +1 -0
- package/dist/types/features/ajax/aggregate/index.d.ts +8 -6
- package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/ajax/instrument/index.d.ts +2 -2
- package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/utils.d.ts +3 -3
- package/dist/types/features/session_trace/aggregate/index.d.ts +8 -8
- package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/spa/instrument/index.d.ts.map +1 -1
- package/dist/types/features/utils/agent-session.d.ts.map +1 -1
- package/dist/types/loaders/api/apiAsync.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/config/state/configurable.js +9 -8
- package/src/common/config/state/init.js +0 -1
- package/src/common/config/state/runtime.js +12 -9
- package/src/common/constants/__mocks__/runtime.js +2 -0
- package/src/common/constants/runtime.js +6 -1
- package/src/common/session/constants.js +1 -0
- package/src/common/session/session-entity.js +2 -0
- package/src/common/timing/time-keeper.js +44 -10
- package/src/common/vitals/time-to-first-byte.js +2 -2
- package/src/common/vitals/vital-metric.js +1 -1
- package/src/features/ajax/aggregate/chunk.js +51 -0
- package/src/features/ajax/aggregate/index.js +128 -200
- package/src/features/ajax/instrument/index.js +1 -4
- package/src/features/jserrors/aggregate/index.js +2 -1
- package/src/features/page_view_event/aggregate/index.js +4 -4
- package/src/features/session_replay/aggregate/index.js +2 -0
- package/src/features/session_replay/shared/utils.js +4 -4
- package/src/features/session_trace/aggregate/index.js +3 -6
- package/src/features/spa/instrument/index.js +0 -3
- package/src/features/utils/agent-session.js +1 -7
- package/src/loaders/api/apiAsync.js +5 -4
- package/dist/cjs/common/storage/first-party-cookies.js +0 -36
- package/dist/esm/common/storage/first-party-cookies.js +0 -29
- package/dist/types/common/storage/first-party-cookies.d.ts +0 -8
- package/dist/types/common/storage/first-party-cookies.d.ts.map +0 -1
- package/src/common/storage/first-party-cookies.js +0 -32
|
@@ -38,4 +38,10 @@ export const ffVersion = (() => {
|
|
|
38
38
|
export const isIE = Boolean(isBrowserScope && window.document.documentMode); // deprecated property that only works in IE
|
|
39
39
|
|
|
40
40
|
export const supportsSendBeacon = !!globalScope.navigator?.sendBeacon;
|
|
41
|
-
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Represents the absolute timestamp in milliseconds that the page was loaded
|
|
44
|
+
* according to the browser's local clock.
|
|
45
|
+
* @type {number}
|
|
46
|
+
*/
|
|
47
|
+
export const originTime = Math.floor(Date.now() - performance.now());
|
|
@@ -3,6 +3,7 @@ export const DEFAULT_KEY = 'SESSION';
|
|
|
3
3
|
export const DEFAULT_EXPIRES_MS = 14400000;
|
|
4
4
|
export const DEFAULT_INACTIVE_MS = 1800000;
|
|
5
5
|
export const SESSION_EVENTS = {
|
|
6
|
+
STARTED: 'session-started',
|
|
6
7
|
PAUSE: 'session-pause',
|
|
7
8
|
RESET: 'session-reset',
|
|
8
9
|
RESUME: 'session-resume',
|
|
@@ -24,6 +24,8 @@ const model = {
|
|
|
24
24
|
sessionReplaySentFirstChunk: false,
|
|
25
25
|
sessionTraceMode: MODE.OFF,
|
|
26
26
|
traceHarvestStarted: false,
|
|
27
|
+
serverTimeDiff: null,
|
|
28
|
+
// set by TimeKeeper; "undefined" value will not be stringified and stored but "null" will
|
|
27
29
|
custom: {}
|
|
28
30
|
};
|
|
29
31
|
export class SessionEntity {
|
|
@@ -138,6 +140,7 @@ export class SessionEntity {
|
|
|
138
140
|
// we can use a modeled object here to help us know and manage what values are being used. -- see "model" above
|
|
139
141
|
if (this.isNew) this.write(getModeledObject(this.state, model), true);else this.sync(initialRead);
|
|
140
142
|
this.initialized = true;
|
|
143
|
+
this.ee.emit(SESSION_EVENTS.STARTED, [this.isNew]);
|
|
141
144
|
}
|
|
142
145
|
|
|
143
146
|
// This is the actual key appended to the storage API
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { originTime } from '../constants/runtime';
|
|
2
|
+
import { ee as baseEE } from '../event-emitter/contextual-ee';
|
|
3
|
+
import { getRuntime } from '../config/config';
|
|
4
|
+
import { SESSION_EVENT_TYPES, SESSION_EVENTS } from '../session/constants';
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* Class used to adjust the timestamp of harvested data to New Relic server time. This
|
|
3
8
|
* is done by tracking the performance timings of the RUM call and applying a calculation
|
|
@@ -5,10 +10,10 @@
|
|
|
5
10
|
*/
|
|
6
11
|
export class TimeKeeper {
|
|
7
12
|
/**
|
|
8
|
-
*
|
|
9
|
-
* @type {
|
|
13
|
+
* Pointer to the current agent session if it exists.
|
|
14
|
+
* @type {import('../session/session-entity').SessionEntity}
|
|
10
15
|
*/
|
|
11
|
-
#
|
|
16
|
+
#session;
|
|
12
17
|
|
|
13
18
|
/**
|
|
14
19
|
* Represents the browser origin time corrected to NR server time.
|
|
@@ -29,15 +34,24 @@ export class TimeKeeper {
|
|
|
29
34
|
* @type {number}
|
|
30
35
|
*/
|
|
31
36
|
#ready = false;
|
|
32
|
-
constructor() {
|
|
33
|
-
this.#
|
|
37
|
+
constructor(agentIdentifier) {
|
|
38
|
+
this.#session = getRuntime(agentIdentifier)?.session;
|
|
39
|
+
if (this.#session) {
|
|
40
|
+
const ee = baseEE.get(agentIdentifier);
|
|
41
|
+
ee.on(SESSION_EVENTS.UPDATE, this.#processSessionUpdate.bind(this));
|
|
42
|
+
ee.on(SESSION_EVENTS.STARTED, () => {
|
|
43
|
+
if (this.#ready) {
|
|
44
|
+
this.#session.write({
|
|
45
|
+
serverTimeDiff: this.#localTimeDiff
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
this.#processSessionUpdate(null, this.#session.read());
|
|
50
|
+
}
|
|
34
51
|
}
|
|
35
52
|
get ready() {
|
|
36
53
|
return this.#ready;
|
|
37
54
|
}
|
|
38
|
-
get originTime() {
|
|
39
|
-
return this.#originTime;
|
|
40
|
-
}
|
|
41
55
|
get correctedOriginTime() {
|
|
42
56
|
return this.#correctedOriginTime;
|
|
43
57
|
}
|
|
@@ -49,6 +63,8 @@ export class TimeKeeper {
|
|
|
49
63
|
* @param endTime {number} The end time of the RUM request
|
|
50
64
|
*/
|
|
51
65
|
processRumRequest(rumRequest, startTime, endTime) {
|
|
66
|
+
if (this.#ready) return; // Server time calculated from session entity
|
|
67
|
+
|
|
52
68
|
const responseDateHeader = rumRequest.getResponseHeader('Date');
|
|
53
69
|
if (!responseDateHeader) {
|
|
54
70
|
throw new Error('Missing date header on rum response.');
|
|
@@ -58,10 +74,13 @@ export class TimeKeeper {
|
|
|
58
74
|
|
|
59
75
|
// Corrected page origin time
|
|
60
76
|
this.#correctedOriginTime = Math.floor(Date.parse(responseDateHeader) - serverOffset);
|
|
61
|
-
this.#localTimeDiff =
|
|
77
|
+
this.#localTimeDiff = originTime - this.#correctedOriginTime;
|
|
62
78
|
if (Number.isNaN(this.#correctedOriginTime)) {
|
|
63
79
|
throw new Error('Date header invalid format.');
|
|
64
80
|
}
|
|
81
|
+
if (this.#session) this.#session.write({
|
|
82
|
+
serverTimeDiff: this.#localTimeDiff
|
|
83
|
+
});
|
|
65
84
|
this.#ready = true;
|
|
66
85
|
}
|
|
67
86
|
|
|
@@ -83,4 +102,22 @@ export class TimeKeeper {
|
|
|
83
102
|
correctAbsoluteTimestamp(timestamp) {
|
|
84
103
|
return Math.floor(timestamp - this.#localTimeDiff);
|
|
85
104
|
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Processes a session entity update payload to extract the server time calculated.
|
|
108
|
+
* @param {import('../session/constants').SESSION_EVENT_TYPES | null} type
|
|
109
|
+
* @param {Object} data
|
|
110
|
+
*/
|
|
111
|
+
#processSessionUpdate(type, data) {
|
|
112
|
+
if (typeof data?.serverTimeDiff !== 'number') return;
|
|
113
|
+
if (!type && !this.#ready ||
|
|
114
|
+
// This captures the initial read from the session entity when the timekeeper first initializes
|
|
115
|
+
type === SESSION_EVENT_TYPES.CROSS_TAB // This captures any cross-tab write of the session entity
|
|
116
|
+
) {
|
|
117
|
+
// This captures the initial read from the session entity when the timekeeper first initializes
|
|
118
|
+
this.#localTimeDiff = data.serverTimeDiff;
|
|
119
|
+
this.#correctedOriginTime = originTime - this.#localTimeDiff;
|
|
120
|
+
this.#ready = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
86
123
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { globalScope, isBrowserScope, isiOS,
|
|
1
|
+
import { globalScope, isBrowserScope, isiOS, originTime } from '../constants/runtime';
|
|
2
2
|
import { VITAL_NAMES } from './constants';
|
|
3
3
|
import { VitalMetric } from './vital-metric';
|
|
4
4
|
import { onTTFB } from 'web-vitals/attribution';
|
|
@@ -21,7 +21,7 @@ if (isBrowserScope && typeof PerformanceNavigationTiming !== 'undefined' && !isi
|
|
|
21
21
|
if (!timeToFirstByte.isValid) {
|
|
22
22
|
const entry = {};
|
|
23
23
|
// convert real timestamps to relative timestamps to match web-vitals behavior
|
|
24
|
-
for (let key in globalScope?.performance?.timing || {}) entry[key] = Math.max(globalScope?.performance?.timing[key] -
|
|
24
|
+
for (let key in globalScope?.performance?.timing || {}) entry[key] = Math.max(globalScope?.performance?.timing[key] - originTime, 0);
|
|
25
25
|
|
|
26
26
|
// ttfb is equiv to document's responseStart property in timing API --> https://developer.mozilla.org/en-US/docs/Web/API/PerformanceTiming/responseStart
|
|
27
27
|
timeToFirstByte.update({
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { nullable, numeric, getAddStringContext, addCustomAttributes } from '../../../common/serialize/bel-serializer';
|
|
2
|
+
import { getInfo } from '../../../common/config/config';
|
|
3
|
+
export default class Chunk {
|
|
4
|
+
constructor(events, aggregateInstance) {
|
|
5
|
+
this.addString = getAddStringContext(aggregateInstance.agentIdentifier); // pass agentIdentifier here
|
|
6
|
+
this.events = events;
|
|
7
|
+
this.payload = 'bel.7;';
|
|
8
|
+
for (let i = 0; i < events.length; i++) {
|
|
9
|
+
const event = events[i];
|
|
10
|
+
const fields = [numeric(event.startTime), numeric(event.endTime - event.startTime), numeric(0),
|
|
11
|
+
// callbackEnd
|
|
12
|
+
numeric(0),
|
|
13
|
+
// no callbackDuration for non-SPA events
|
|
14
|
+
this.addString(event.method), numeric(event.status), this.addString(event.domain), this.addString(event.path), numeric(event.requestSize), numeric(event.responseSize), event.type === 'fetch' ? 1 : '', this.addString(0),
|
|
15
|
+
// nodeId
|
|
16
|
+
nullable(event.spanId, this.addString, true) +
|
|
17
|
+
// guid
|
|
18
|
+
nullable(event.traceId, this.addString, true) +
|
|
19
|
+
// traceId
|
|
20
|
+
nullable(event.spanTimestamp, numeric, false) // timestamp
|
|
21
|
+
];
|
|
22
|
+
let insert = '2,';
|
|
23
|
+
|
|
24
|
+
// Since configuration objects (like info) are created new each time they are set, we have to grab the current pointer to the attr object here.
|
|
25
|
+
const jsAttributes = getInfo(aggregateInstance.agentIdentifier).jsAttributes;
|
|
26
|
+
|
|
27
|
+
// add custom attributes
|
|
28
|
+
// gql decorators are added as custom attributes to alleviate need for new BEL schema
|
|
29
|
+
const attrParts = addCustomAttributes({
|
|
30
|
+
...(jsAttributes || {}),
|
|
31
|
+
...(event.gql || {})
|
|
32
|
+
}, this.addString);
|
|
33
|
+
fields.unshift(numeric(attrParts.length));
|
|
34
|
+
insert += fields.join(',');
|
|
35
|
+
if (attrParts && attrParts.length > 0) {
|
|
36
|
+
insert += ';' + attrParts.join(';');
|
|
37
|
+
}
|
|
38
|
+
if (i + 1 < events.length) insert += ';';
|
|
39
|
+
this.payload += insert;
|
|
40
|
+
}
|
|
41
|
+
this.tooBig = this.payload.length * 2 > aggregateInstance.MAX_PAYLOAD_SIZE;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -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,225 +14,165 @@ 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
|
export class Aggregate extends AggregateBase {
|
|
19
19
|
static featureName = FEATURE_NAME;
|
|
20
|
+
#agentInfo;
|
|
21
|
+
#agentRuntime;
|
|
22
|
+
#agentInit;
|
|
20
23
|
constructor(agentIdentifier, aggregator) {
|
|
21
24
|
super(agentIdentifier, aggregator, FEATURE_NAME);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
this
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const agentRuntime = getRuntime(agentIdentifier);
|
|
33
|
-
const denyList = agentRuntime.denyList;
|
|
34
|
-
setDenyList(denyList);
|
|
35
|
-
let ajaxEvents = [];
|
|
36
|
-
let spaAjaxEvents = {};
|
|
37
|
-
let sentAjaxEvents = [];
|
|
38
|
-
const ee = this.ee;
|
|
39
|
-
const harvestTimeSeconds = agentInit.ajax.harvestTimeSeconds || 10;
|
|
40
|
-
const MAX_PAYLOAD_SIZE = agentInit.ajax.maxPayloadSize || 1000000;
|
|
41
|
-
|
|
42
|
-
// Exposes these methods to browser test files -- future TO DO: can be removed once these fns are extracted from the constructor into class func
|
|
43
|
-
this.storeXhr = storeXhr;
|
|
44
|
-
this.prepareHarvest = prepareHarvest;
|
|
45
|
-
this.getStoredEvents = function () {
|
|
46
|
-
return {
|
|
47
|
-
ajaxEvents,
|
|
48
|
-
spaAjaxEvents
|
|
49
|
-
};
|
|
50
|
-
};
|
|
25
|
+
this.#agentInfo = getInfo(agentIdentifier);
|
|
26
|
+
this.#agentRuntime = getRuntime(agentIdentifier);
|
|
27
|
+
this.#agentInit = getConfiguration(agentIdentifier);
|
|
28
|
+
const harvestTimeSeconds = this.#agentInit.ajax.harvestTimeSeconds || 10;
|
|
29
|
+
this.MAX_PAYLOAD_SIZE = this.#agentInit.ajax.maxPayloadSize || 1000000;
|
|
30
|
+
setDenyList(this.#agentRuntime.denyList);
|
|
31
|
+
this.ajaxEvents = [];
|
|
32
|
+
this.spaAjaxEvents = {};
|
|
33
|
+
this.sentAjaxEvents = [];
|
|
34
|
+
const classThis = this;
|
|
51
35
|
|
|
52
36
|
// --- v Used by old spa feature
|
|
53
|
-
ee.on('interactionDone', (interaction, wasSaved) => {
|
|
54
|
-
if (!spaAjaxEvents[interaction.id]) return;
|
|
37
|
+
this.ee.on('interactionDone', (interaction, wasSaved) => {
|
|
38
|
+
if (!this.spaAjaxEvents[interaction.id]) return;
|
|
55
39
|
if (!wasSaved) {
|
|
56
40
|
// 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
|
|
57
|
-
spaAjaxEvents[interaction.id].forEach(
|
|
58
|
-
ajaxEvents.push(item);
|
|
59
|
-
});
|
|
41
|
+
this.spaAjaxEvents[interaction.id].forEach(item => this.ajaxEvents.push(item));
|
|
60
42
|
}
|
|
61
|
-
delete spaAjaxEvents[interaction.id];
|
|
43
|
+
delete this.spaAjaxEvents[interaction.id];
|
|
62
44
|
});
|
|
63
45
|
// --- ^
|
|
64
46
|
// --- v Used by new soft nav
|
|
65
|
-
registerHandler('returnAjax', event => ajaxEvents.push(event), this.featureName, this.ee);
|
|
47
|
+
registerHandler('returnAjax', event => this.ajaxEvents.push(event), this.featureName, this.ee);
|
|
66
48
|
// --- ^
|
|
49
|
+
registerHandler('xhr', function () {
|
|
50
|
+
// 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.
|
|
51
|
+
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
|
|
52
|
+
}, this.featureName, this.ee);
|
|
53
|
+
this.waitForFlags([]).then(() => {
|
|
54
|
+
const scheduler = new HarvestScheduler('events', {
|
|
55
|
+
onFinished: this.onEventsHarvestFinished.bind(this),
|
|
56
|
+
getPayload: this.prepareHarvest.bind(this)
|
|
57
|
+
}, this);
|
|
58
|
+
scheduler.startTimer(harvestTimeSeconds);
|
|
59
|
+
this.drain();
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
storeXhr(params, metrics, startTime, endTime, type, ctx) {
|
|
63
|
+
metrics.time = startTime;
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
// send to session traces
|
|
66
|
+
let hash;
|
|
67
|
+
if (params.cat) {
|
|
68
|
+
hash = stringify([params.status, params.cat]);
|
|
69
|
+
} else {
|
|
70
|
+
hash = stringify([params.status, params.host, params.pathname]);
|
|
71
|
+
}
|
|
72
|
+
const shouldCollect = shouldCollectEvent(params);
|
|
73
|
+
const shouldOmitAjaxMetrics = this.#agentInit.feature_flags?.includes('ajax_metrics_deny_list');
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// store for timeslice metric (harvested by jserrors feature)
|
|
76
|
+
if (shouldCollect || !shouldOmitAjaxMetrics) {
|
|
77
|
+
this.aggregator.store('xhr', hash, params, metrics);
|
|
78
|
+
}
|
|
79
|
+
if (!shouldCollect) {
|
|
80
|
+
if (params.hostname === this.#agentInfo.errorBeacon || this.#agentInit.proxy?.beacon && params.hostname === this.#agentInit.proxy.beacon) {
|
|
81
|
+
// This doesn't make a distinction if the same-domain request is going to a different port or path...
|
|
82
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Events/Excluded/Agent'], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
83
|
+
if (shouldOmitAjaxMetrics) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Metrics/Excluded/Agent'], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
77
84
|
} else {
|
|
78
|
-
|
|
85
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Events/Excluded/App'], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
86
|
+
if (shouldOmitAjaxMetrics) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Metrics/Excluded/App'], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
79
87
|
}
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
return; // do not send this ajax as an event
|
|
89
|
+
}
|
|
90
|
+
handle('bstXhrAgg', ['xhr', hash, params, metrics], undefined, FEATURE_NAMES.sessionTrace, this.ee); // have trace feature harvest AjaxNode
|
|
82
91
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
handle('bstXhrAgg', ['xhr', hash, params, metrics], undefined, FEATURE_NAMES.sessionTrace, ee);
|
|
103
|
-
var xhrContext = this;
|
|
104
|
-
var event = {
|
|
105
|
-
method: params.method,
|
|
106
|
-
status: params.status,
|
|
107
|
-
domain: params.host,
|
|
108
|
-
path: params.pathname,
|
|
109
|
-
requestSize: metrics.txSize,
|
|
110
|
-
responseSize: metrics.rxSize,
|
|
111
|
-
type,
|
|
112
|
-
startTime,
|
|
113
|
-
endTime,
|
|
114
|
-
callbackDuration: metrics.cbTime
|
|
115
|
-
};
|
|
116
|
-
if (xhrContext.dt) {
|
|
117
|
-
event.spanId = xhrContext.dt.spanId;
|
|
118
|
-
event.traceId = xhrContext.dt.traceId;
|
|
119
|
-
event.spanTimestamp = agentRuntime.timeKeeper.correctAbsoluteTimestamp(xhrContext.dt.timestamp);
|
|
120
|
-
}
|
|
92
|
+
const event = {
|
|
93
|
+
method: params.method,
|
|
94
|
+
status: params.status,
|
|
95
|
+
domain: params.host,
|
|
96
|
+
path: params.pathname,
|
|
97
|
+
requestSize: metrics.txSize,
|
|
98
|
+
responseSize: metrics.rxSize,
|
|
99
|
+
type,
|
|
100
|
+
startTime,
|
|
101
|
+
endTime,
|
|
102
|
+
callbackDuration: metrics.cbTime
|
|
103
|
+
};
|
|
104
|
+
if (ctx.dt) {
|
|
105
|
+
event.spanId = ctx.dt.spanId;
|
|
106
|
+
event.traceId = ctx.dt.traceId;
|
|
107
|
+
event.spanTimestamp = this.#agentRuntime.timeKeeper.correctAbsoluteTimestamp(ctx.dt.timestamp);
|
|
108
|
+
}
|
|
121
109
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
110
|
+
// parsed from the AJAX body, looking for operationName param & parsing query for operationType
|
|
111
|
+
event.gql = params.gql = parseGQL({
|
|
112
|
+
body: ctx.body,
|
|
113
|
+
query: ctx.parsedOrigin?.search
|
|
114
|
+
});
|
|
115
|
+
if (event.gql) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
116
|
+
const softNavInUse = Boolean(getNREUMInitializedAgent(this.agentIdentifier)?.features?.[FEATURE_NAMES.softNav]);
|
|
117
|
+
if (softNavInUse) {
|
|
118
|
+
// For newer soft nav (when running), pass the event to it for evaluation -- either part of an interaction or is given back
|
|
119
|
+
handle('ajax', [event], undefined, FEATURE_NAMES.softNav, this.ee);
|
|
120
|
+
} else if (ctx.spaNode) {
|
|
121
|
+
// For old spa (when running), if the ajax happened inside an interaction, hold it until the interaction finishes
|
|
122
|
+
const interactionId = ctx.spaNode.interaction.id;
|
|
123
|
+
this.spaAjaxEvents[interactionId] = this.spaAjaxEvents[interactionId] || [];
|
|
124
|
+
this.spaAjaxEvents[interactionId].push(event);
|
|
125
|
+
} else {
|
|
126
|
+
this.ajaxEvents.push(event);
|
|
140
127
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
body: {
|
|
151
|
-
e: payload[i]
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
if (options.retry) {
|
|
156
|
-
sentAjaxEvents = ajaxEvents.slice();
|
|
128
|
+
}
|
|
129
|
+
prepareHarvest(options) {
|
|
130
|
+
options = options || {};
|
|
131
|
+
if (this.ajaxEvents.length === 0) return null;
|
|
132
|
+
const payload = this.#getPayload(this.ajaxEvents);
|
|
133
|
+
const payloadObjs = [];
|
|
134
|
+
for (let i = 0; i < payload.length; i++) payloadObjs.push({
|
|
135
|
+
body: {
|
|
136
|
+
e: payload[i]
|
|
157
137
|
}
|
|
158
|
-
|
|
159
|
-
|
|
138
|
+
});
|
|
139
|
+
if (options.retry) this.sentAjaxEvents = this.ajaxEvents;
|
|
140
|
+
this.ajaxEvents = [];
|
|
141
|
+
return payloadObjs;
|
|
142
|
+
}
|
|
143
|
+
onEventsHarvestFinished(result) {
|
|
144
|
+
if (result.retry && this.sentAjaxEvents.length > 0) {
|
|
145
|
+
this.ajaxEvents.unshift(...this.sentAjaxEvents);
|
|
146
|
+
this.sentAjaxEvents = [];
|
|
160
147
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
it cant get any smaller and we dont want to recurse forever */
|
|
175
|
-
tooBig = true;
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
} else {
|
|
179
|
-
payload.push(currentChunk.payload);
|
|
148
|
+
}
|
|
149
|
+
#getPayload(events, numberOfChunks) {
|
|
150
|
+
numberOfChunks = numberOfChunks || 1;
|
|
151
|
+
const payload = [];
|
|
152
|
+
const chunkSize = events.length / numberOfChunks;
|
|
153
|
+
const eventChunks = splitChunks.call(this, events, chunkSize);
|
|
154
|
+
let tooBig = false;
|
|
155
|
+
for (let i = 0; i < eventChunks.length; i++) {
|
|
156
|
+
const currentChunk = eventChunks[i];
|
|
157
|
+
if (currentChunk.tooBig) {
|
|
158
|
+
if (currentChunk.events.length > 1) {
|
|
159
|
+
tooBig = true;
|
|
160
|
+
break; // if the payload is too big BUT is made of more than 1 event, we can split it down again
|
|
180
161
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
function onEventsHarvestFinished(result) {
|
|
186
|
-
if (result.retry && sentAjaxEvents.length > 0) {
|
|
187
|
-
ajaxEvents.unshift(...sentAjaxEvents);
|
|
188
|
-
sentAjaxEvents = [];
|
|
162
|
+
// Otherwise, if it consists of one sole event, we do not send it (discarded) since we cannot break it apart any further.
|
|
163
|
+
} else {
|
|
164
|
+
payload.push(currentChunk.payload);
|
|
189
165
|
}
|
|
190
166
|
}
|
|
167
|
+
// Check if the current payload string is too big, if so then run getPayload again with more buckets.
|
|
168
|
+
return tooBig ? this.#getPayload(events, ++numberOfChunks) : payload;
|
|
191
169
|
function splitChunks(arr, chunkSize) {
|
|
192
170
|
chunkSize = chunkSize || arr.length;
|
|
193
|
-
|
|
194
|
-
for (
|
|
195
|
-
chunks.push(new Chunk(arr.slice(i, i + chunkSize)));
|
|
171
|
+
const chunks = [];
|
|
172
|
+
for (let i = 0, len = arr.length; i < len; i += chunkSize) {
|
|
173
|
+
chunks.push(new Chunk(arr.slice(i, i + chunkSize), this));
|
|
196
174
|
}
|
|
197
175
|
return chunks;
|
|
198
176
|
}
|
|
199
|
-
function Chunk(events) {
|
|
200
|
-
this.addString = getAddStringContext(agentIdentifier); // pass agentIdentifier here
|
|
201
|
-
this.events = events;
|
|
202
|
-
this.payload = 'bel.7;';
|
|
203
|
-
for (var i = 0; i < events.length; i++) {
|
|
204
|
-
var event = events[i];
|
|
205
|
-
var fields = [numeric(event.startTime), numeric(event.endTime - event.startTime), numeric(0),
|
|
206
|
-
// callbackEnd
|
|
207
|
-
numeric(0),
|
|
208
|
-
// no callbackDuration for non-SPA events
|
|
209
|
-
this.addString(event.method), numeric(event.status), this.addString(event.domain), this.addString(event.path), numeric(event.requestSize), numeric(event.responseSize), event.type === 'fetch' ? 1 : '', this.addString(0),
|
|
210
|
-
// nodeId
|
|
211
|
-
nullable(event.spanId, this.addString, true) +
|
|
212
|
-
// guid
|
|
213
|
-
nullable(event.traceId, this.addString, true) +
|
|
214
|
-
// traceId
|
|
215
|
-
nullable(event.spanTimestamp, numeric, false) // timestamp
|
|
216
|
-
];
|
|
217
|
-
var insert = '2,';
|
|
218
|
-
|
|
219
|
-
// add custom attributes
|
|
220
|
-
// gql decorators are added as custom attributes to alleviate need for new BEL schema
|
|
221
|
-
var attrParts = addCustomAttributes({
|
|
222
|
-
...(getInfo(agentIdentifier).jsAttributes || {}),
|
|
223
|
-
...(event.gql || {})
|
|
224
|
-
}, this.addString);
|
|
225
|
-
fields.unshift(numeric(attrParts.length));
|
|
226
|
-
insert += fields.join(',');
|
|
227
|
-
if (attrParts && attrParts.length > 0) {
|
|
228
|
-
insert += ';' + attrParts.join(';');
|
|
229
|
-
}
|
|
230
|
-
if (i + 1 < events.length) insert += ';';
|
|
231
|
-
this.payload += insert;
|
|
232
|
-
}
|
|
233
|
-
this.tooBig = function (maxPayloadSize) {
|
|
234
|
-
maxPayloadSize = maxPayloadSize || MAX_PAYLOAD_SIZE;
|
|
235
|
-
return this.payload.length * 2 > maxPayloadSize;
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
177
|
}
|
|
239
178
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Copyright 2020 New Relic Corporation. All rights reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
|
-
import { originals, getLoaderConfig
|
|
5
|
+
import { originals, getLoaderConfig } from '../../../common/config/config';
|
|
6
6
|
import { handle } from '../../../common/event-emitter/handle';
|
|
7
7
|
import { id } from '../../../common/ids/id';
|
|
8
8
|
import { ffVersion, globalScope, isBrowserScope } from '../../../common/constants/runtime';
|
|
@@ -26,9 +26,6 @@ export class Instrument extends InstrumentBase {
|
|
|
26
26
|
constructor(agentIdentifier, aggregator) {
|
|
27
27
|
let auto = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
|
|
28
28
|
super(agentIdentifier, aggregator, FEATURE_NAME, auto);
|
|
29
|
-
|
|
30
|
-
// Very unlikely, but in case the existing XMLHttpRequest.prototype object on the page couldn't be wrapped.
|
|
31
|
-
if (!getRuntime(agentIdentifier).xhrWrappable) return;
|
|
32
29
|
this.dt = new DT(agentIdentifier);
|
|
33
30
|
this.handler = (type, args, ctx, group) => handle(type, args, ctx, group, this.ee);
|
|
34
31
|
|
|
@@ -186,7 +186,7 @@ export class Aggregate extends AggregateBase {
|
|
|
186
186
|
this.pageviewReported[bucketHash] = true;
|
|
187
187
|
}
|
|
188
188
|
params.firstOccurrenceTimestamp = this.observedAt[bucketHash];
|
|
189
|
-
params.timestamp =
|
|
189
|
+
params.timestamp = agentRuntime.timeKeeper.convertRelativeTimestamp(time);
|
|
190
190
|
var type = internal ? 'ierr' : 'err';
|
|
191
191
|
var newMetrics = {
|
|
192
192
|
time
|
|
@@ -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';
|
|
@@ -95,14 +95,14 @@ export class Aggregate extends AggregateBase {
|
|
|
95
95
|
// Navigation Timing level 2 API that replaced PerformanceTiming & PerformanceNavigation
|
|
96
96
|
const navTimingEntry = globalScope?.performance?.getEntriesByType('navigation')?.[0];
|
|
97
97
|
const perf = {
|
|
98
|
-
timing: addPT(
|
|
98
|
+
timing: addPT(originTime, navTimingEntry, {}),
|
|
99
99
|
navigation: addPN(navTimingEntry, {})
|
|
100
100
|
};
|
|
101
101
|
queryParameters.perf = stringify(perf);
|
|
102
102
|
} else if (typeof PerformanceTiming !== 'undefined') {
|
|
103
103
|
// Safari pre-15 did not support level 2 timing
|
|
104
104
|
const perf = {
|
|
105
|
-
timing: addPT(
|
|
105
|
+
timing: addPT(originTime, globalScope.performance.timing, {}, true),
|
|
106
106
|
navigation: addPN(globalScope.performance.navigation, {})
|
|
107
107
|
};
|
|
108
108
|
queryParameters.perf = stringify(perf);
|
|
@@ -134,7 +134,7 @@ export class Aggregate extends AggregateBase {
|
|
|
134
134
|
return;
|
|
135
135
|
}
|
|
136
136
|
try {
|
|
137
|
-
const timeKeeper = new TimeKeeper();
|
|
137
|
+
const timeKeeper = new TimeKeeper(this.agentIdentifier);
|
|
138
138
|
timeKeeper.processRumRequest(xhr, rumStartTime, rumEndTime);
|
|
139
139
|
if (!timeKeeper.ready) throw new Error('TimeKeeper not ready');
|
|
140
140
|
agentRuntime.timeKeeper = timeKeeper;
|