@newrelic/browser-agent 1.256.1 → 1.258.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -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/harvest/harvest.js +7 -5
- 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 +26 -13
- package/dist/cjs/features/page_view_event/aggregate/index.js +3 -3
- package/dist/cjs/features/session_replay/aggregate/index.js +12 -5
- package/dist/cjs/features/session_replay/constants.js +2 -1
- package/dist/cjs/features/session_replay/instrument/index.js +15 -5
- package/dist/cjs/features/session_replay/shared/recorder.js +6 -3
- package/dist/cjs/features/session_replay/shared/utils.js +9 -8
- package/dist/cjs/features/session_trace/aggregate/index.js +3 -5
- package/dist/cjs/features/soft_navigations/aggregate/index.js +2 -2
- package/dist/cjs/features/spa/instrument/index.js +0 -2
- package/dist/cjs/features/utils/agent-session.js +1 -5
- package/dist/cjs/features/utils/instrument-base.js +11 -14
- package/dist/cjs/features/utils/nr1-debugger.js +27 -0
- package/dist/cjs/loaders/agent.js +4 -0
- 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/harvest/harvest.js +7 -5
- 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 +26 -13
- package/dist/esm/features/page_view_event/aggregate/index.js +4 -4
- package/dist/esm/features/session_replay/aggregate/index.js +12 -5
- package/dist/esm/features/session_replay/constants.js +2 -1
- package/dist/esm/features/session_replay/instrument/index.js +16 -6
- package/dist/esm/features/session_replay/shared/recorder.js +6 -3
- package/dist/esm/features/session_replay/shared/utils.js +9 -7
- package/dist/esm/features/session_trace/aggregate/index.js +3 -5
- package/dist/esm/features/soft_navigations/aggregate/index.js +2 -2
- package/dist/esm/features/spa/instrument/index.js +0 -2
- package/dist/esm/features/utils/agent-session.js +1 -5
- package/dist/esm/features/utils/instrument-base.js +11 -14
- package/dist/esm/features/utils/nr1-debugger.js +21 -0
- package/dist/esm/loaders/agent.js +4 -0
- 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/harvest/harvest.d.ts +1 -1
- package/dist/types/common/harvest/harvest.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 +0 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +0 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/constants.d.ts +1 -0
- package/dist/types/features/session_replay/constants.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 +1 -0
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/utils.d.ts +5 -5
- package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -1
- 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/features/utils/instrument-base.d.ts.map +1 -1
- package/dist/types/features/utils/nr1-debugger.d.ts +2 -0
- package/dist/types/features/utils/nr1-debugger.d.ts.map +1 -0
- package/dist/types/loaders/agent.d.ts +5 -1
- package/dist/types/loaders/agent.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/drain/__mocks__/drain.js +2 -0
- package/src/common/harvest/harvest.js +8 -6
- 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/common/window/__mocks__/load.js +3 -0
- 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 +28 -11
- package/src/features/page_view_event/aggregate/index.js +4 -4
- package/src/features/session_replay/aggregate/index.js +19 -7
- package/src/features/session_replay/constants.js +2 -1
- package/src/features/session_replay/instrument/index.js +16 -6
- package/src/features/session_replay/shared/__mocks__/utils.js +2 -0
- package/src/features/session_replay/shared/recorder.js +7 -4
- package/src/features/session_replay/shared/utils.js +9 -7
- package/src/features/session_trace/aggregate/index.js +3 -6
- package/src/features/soft_navigations/aggregate/index.js +2 -2
- package/src/features/spa/instrument/index.js +0 -3
- package/src/features/utils/__mocks__/agent-session.js +1 -0
- package/src/features/utils/__mocks__/feature-base.js +11 -0
- package/src/features/utils/agent-session.js +1 -7
- package/src/features/utils/instrument-base.js +11 -14
- package/src/features/utils/nr1-debugger.js +22 -0
- package/src/loaders/agent.js +4 -0
- 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
|
@@ -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
|
|
|
@@ -37,13 +37,9 @@ export class Aggregate extends AggregateBase {
|
|
|
37
37
|
this.bufferedErrorsUnderSpa = {};
|
|
38
38
|
this.currentBody = undefined;
|
|
39
39
|
this.errorOnPage = false;
|
|
40
|
-
this.replayAborted = false;
|
|
41
40
|
|
|
42
41
|
// this will need to change to match whatever ee we use in the instrument
|
|
43
42
|
this.ee.on('interactionDone', (interaction, wasSaved) => this.onInteractionDone(interaction, wasSaved));
|
|
44
|
-
this.ee.on('REPLAY_ABORTED', () => {
|
|
45
|
-
this.replayAborted = true;
|
|
46
|
-
});
|
|
47
43
|
register('err', function () {
|
|
48
44
|
return _this.storeError(...arguments);
|
|
49
45
|
}, this.featureName, this.ee);
|
|
@@ -89,11 +85,7 @@ export class Aggregate extends AggregateBase {
|
|
|
89
85
|
payload.qs.ri = releaseIds;
|
|
90
86
|
}
|
|
91
87
|
if (body && body.err && body.err.length) {
|
|
92
|
-
|
|
93
|
-
body.err.forEach(e => {
|
|
94
|
-
delete e.params?.hasReplay;
|
|
95
|
-
});
|
|
96
|
-
}
|
|
88
|
+
this.#runCrossFeatureChecks(body.err);
|
|
97
89
|
if (!this.errorOnPage) {
|
|
98
90
|
payload.qs.pve = '1';
|
|
99
91
|
this.errorOnPage = true;
|
|
@@ -167,14 +159,14 @@ export class Aggregate extends AggregateBase {
|
|
|
167
159
|
// Notice if filterOutput isn't false|undefined OR our specified object, this func would've returned already (so it's unnecessary to req-check group).
|
|
168
160
|
// Do not modify the name ('errorGroup') of params without DEM approval!
|
|
169
161
|
if (filterOutput?.group) params.errorGroup = filterOutput.group;
|
|
170
|
-
|
|
162
|
+
if (hasReplay) params.hasReplay = hasReplay;
|
|
171
163
|
/**
|
|
172
164
|
* The bucketHash is different from the params.stackHash because the params.stackHash is based on the canonicalized
|
|
173
165
|
* stack trace and is used downstream in NR1 to attempt to group the same errors across different browsers. However,
|
|
174
166
|
* the canonical stack trace excludes items like the column number increasing the hit-rate of different errors potentially
|
|
175
167
|
* bucketing and ultimately resulting in the loss of data in NR1.
|
|
176
168
|
*/
|
|
177
|
-
var bucketHash = stringHashCode("".concat(stackInfo.name, "_").concat(stackInfo.message, "_").concat(stackInfo.stackString));
|
|
169
|
+
var bucketHash = stringHashCode("".concat(stackInfo.name, "_").concat(stackInfo.message, "_").concat(stackInfo.stackString, "_").concat(params.hasReplay ? 1 : 0));
|
|
178
170
|
if (!this.stackReported[bucketHash]) {
|
|
179
171
|
this.stackReported[bucketHash] = true;
|
|
180
172
|
params.stack_trace = truncateSize(stackInfo.stackString);
|
|
@@ -193,9 +185,8 @@ export class Aggregate extends AggregateBase {
|
|
|
193
185
|
params.pageview = 1;
|
|
194
186
|
this.pageviewReported[bucketHash] = true;
|
|
195
187
|
}
|
|
196
|
-
if (hasReplay && !this.replayAborted) params.hasReplay = hasReplay;
|
|
197
188
|
params.firstOccurrenceTimestamp = this.observedAt[bucketHash];
|
|
198
|
-
params.timestamp =
|
|
189
|
+
params.timestamp = agentRuntime.timeKeeper.convertRelativeTimestamp(time);
|
|
199
190
|
var type = internal ? 'ierr' : 'err';
|
|
200
191
|
var newMetrics = {
|
|
201
192
|
time
|
|
@@ -288,4 +279,26 @@ export class Aggregate extends AggregateBase {
|
|
|
288
279
|
);
|
|
289
280
|
delete this.bufferedErrorsUnderSpa[interactionId]; // wipe the list of jserrors so they aren't duplicated by another call to the same id
|
|
290
281
|
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Dispatches a cross-feature communication event to allow other
|
|
285
|
+
* features to provide flags and data that can be used to mutation
|
|
286
|
+
* to the payload and to allow features to know about a feature
|
|
287
|
+
* harvest happening.
|
|
288
|
+
* @param {any[]} errors Array of errors from the payload body
|
|
289
|
+
*/
|
|
290
|
+
#runCrossFeatureChecks(errors) {
|
|
291
|
+
const errorHashes = errors.map(error => error.params.stackHash);
|
|
292
|
+
const crossFeatureData = {
|
|
293
|
+
errorHashes
|
|
294
|
+
};
|
|
295
|
+
this.ee.emit("cfc.".concat(this.featureName), [crossFeatureData]);
|
|
296
|
+
let hasReplayFlag = errors.find(err => err.params.hasReplay);
|
|
297
|
+
if (hasReplayFlag && !crossFeatureData.hasReplay) {
|
|
298
|
+
// Some errors have `hasReplay` and a replay is not being recorded
|
|
299
|
+
errors.forEach(error => {
|
|
300
|
+
delete error.params.hasReplay;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
291
304
|
}
|
|
@@ -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;
|
|
@@ -52,9 +52,11 @@ export class Aggregate extends AggregateBase {
|
|
|
52
52
|
/** set at BCS response, stored in runtime */
|
|
53
53
|
this.timeKeeper = undefined;
|
|
54
54
|
this.recorder = args?.recorder;
|
|
55
|
-
this.preloaded = !!this.recorder;
|
|
56
55
|
this.errorNoticed = args?.errorNoticed || false;
|
|
57
56
|
handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/Enabled'], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
57
|
+
this.ee.on("cfc.".concat(FEATURE_NAMES.jserrors), crossFeatureData => {
|
|
58
|
+
crossFeatureData.hasReplay = !!(this.scheduler?.started && this.recorder && this.mode === MODE.FULL && !this.blocked && this.entitled);
|
|
59
|
+
});
|
|
58
60
|
|
|
59
61
|
// The SessionEntity class can emit a message indicating the session was cleared and reset (expiry, inactivity). This feature must abort and never resume if that occurs.
|
|
60
62
|
this.ee.on(SESSION_EVENTS.RESET, () => {
|
|
@@ -101,6 +103,9 @@ export class Aggregate extends AggregateBase {
|
|
|
101
103
|
registerHandler(SR_EVENT_EMITTER_TYPES.PAUSE, () => {
|
|
102
104
|
this.forceStop(this.mode !== MODE.ERROR);
|
|
103
105
|
}, this.featureName, this.ee);
|
|
106
|
+
registerHandler(SR_EVENT_EMITTER_TYPES.ERROR_DURING_REPLAY, e => {
|
|
107
|
+
this.handleError(e);
|
|
108
|
+
}, this.featureName, this.ee);
|
|
104
109
|
const {
|
|
105
110
|
error_sampling_rate,
|
|
106
111
|
sampling_rate,
|
|
@@ -149,15 +154,17 @@ export class Aggregate extends AggregateBase {
|
|
|
149
154
|
}
|
|
150
155
|
}
|
|
151
156
|
switchToFull() {
|
|
157
|
+
if (!this.entitled || this.blocked) return;
|
|
152
158
|
this.mode = MODE.FULL;
|
|
153
159
|
// if the error was noticed AFTER the recorder was already imported....
|
|
154
160
|
if (this.recorder && this.initialized) {
|
|
155
|
-
this.recorder.
|
|
156
|
-
this.recorder.startRecording();
|
|
161
|
+
if (!this.recorder.recording) this.recorder.startRecording();
|
|
157
162
|
this.scheduler.startTimer(this.harvestTimeSeconds);
|
|
158
163
|
this.syncWithSessionManager({
|
|
159
164
|
sessionReplayMode: this.mode
|
|
160
165
|
});
|
|
166
|
+
} else {
|
|
167
|
+
this.initializeRecording(false, true, true);
|
|
161
168
|
}
|
|
162
169
|
}
|
|
163
170
|
|
|
@@ -218,7 +225,6 @@ export class Aggregate extends AggregateBase {
|
|
|
218
225
|
|
|
219
226
|
// If an error was noticed before the mode could be set (like in the early lifecycle of the page), immediately set to FULL mode
|
|
220
227
|
if (this.mode === MODE.ERROR && this.errorNoticed) this.mode = MODE.FULL;
|
|
221
|
-
if (!this.preloaded) this.ee.on('err', e => this.handleError(e));
|
|
222
228
|
|
|
223
229
|
// FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
|
|
224
230
|
// ERROR mode will do this until an error is thrown, and then switch into FULL mode.
|
|
@@ -312,7 +318,7 @@ export class Aggregate extends AggregateBase {
|
|
|
312
318
|
return [payload];
|
|
313
319
|
}
|
|
314
320
|
getCorrectedTimestamp(node) {
|
|
315
|
-
if (!node
|
|
321
|
+
if (!node?.timestamp) return;
|
|
316
322
|
if (node.__newrelic) return node.timestamp;
|
|
317
323
|
return this.timeKeeper.correctAbsoluteTimestamp(node.timestamp);
|
|
318
324
|
}
|
|
@@ -362,6 +368,7 @@ export class Aggregate extends AggregateBase {
|
|
|
362
368
|
...(agentMetadata.entityGuid && {
|
|
363
369
|
entityGuid: agentMetadata.entityGuid
|
|
364
370
|
}),
|
|
371
|
+
harvestId: [agentRuntime.session?.state.value, agentRuntime.ptid, agentRuntime.harvestCount].filter(x => x).join('_'),
|
|
365
372
|
'replay.firstTimestamp': firstTimestamp,
|
|
366
373
|
'replay.lastTimestamp': lastTimestamp,
|
|
367
374
|
'replay.nodes': events.length,
|
|
@@ -4,7 +4,8 @@ export const FEATURE_NAME = FEATURE_NAMES.sessionReplay;
|
|
|
4
4
|
export const SR_EVENT_EMITTER_TYPES = {
|
|
5
5
|
RECORD: 'recordReplay',
|
|
6
6
|
PAUSE: 'pauseReplay',
|
|
7
|
-
REPLAY_RUNNING: 'replayRunning'
|
|
7
|
+
REPLAY_RUNNING: 'replayRunning',
|
|
8
|
+
ERROR_DURING_REPLAY: 'errorDuringReplay'
|
|
8
9
|
};
|
|
9
10
|
export const AVG_COMPRESSION = 0.12;
|
|
10
11
|
export const RRWEB_EVENT_TYPES = {
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* It is not production ready, and is not intended to be imported or implemented in any build of the browser agent until
|
|
10
10
|
* functionality is validated and a full user experience is curated.
|
|
11
11
|
*/
|
|
12
|
+
import { handle } from '../../../common/event-emitter/handle';
|
|
12
13
|
import { DEFAULT_KEY, MODE, PREFIX } from '../../../common/session/constants';
|
|
13
14
|
import { InstrumentBase } from '../../utils/instrument-base';
|
|
14
|
-
import { FEATURE_NAME } from '../constants';
|
|
15
|
+
import { FEATURE_NAME, SR_EVENT_EMITTER_TYPES } from '../constants';
|
|
15
16
|
import { isPreloadAllowed } from '../shared/utils';
|
|
16
17
|
export class Instrument extends InstrumentBase {
|
|
17
18
|
static featureName = FEATURE_NAME;
|
|
@@ -19,19 +20,28 @@ export class Instrument extends InstrumentBase {
|
|
|
19
20
|
let auto = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
|
|
20
21
|
super(agentIdentifier, aggregator, FEATURE_NAME, auto);
|
|
21
22
|
let session;
|
|
23
|
+
this.replayRunning = false;
|
|
22
24
|
try {
|
|
23
25
|
session = JSON.parse(localStorage.getItem("".concat(PREFIX, "_").concat(DEFAULT_KEY)));
|
|
24
26
|
} catch (err) {}
|
|
25
27
|
if (this.#canPreloadRecorder(session)) {
|
|
26
|
-
/** If this is preloaded, set up a buffer, if not, later when sampling we will set up a .on for live events */
|
|
27
|
-
this.ee.on('err', e => {
|
|
28
|
-
this.errorNoticed = true;
|
|
29
|
-
if (this.featAggregate) this.featAggregate.handleError();
|
|
30
|
-
});
|
|
31
28
|
this.#startRecording(session?.sessionReplayMode);
|
|
32
29
|
} else {
|
|
33
30
|
this.importAggregator();
|
|
34
31
|
}
|
|
32
|
+
|
|
33
|
+
/** If the recorder is running, we can pass error events on to the agg to help it switch to full mode later */
|
|
34
|
+
this.ee.on('err', e => {
|
|
35
|
+
if (this.replayRunning) {
|
|
36
|
+
this.errorNoticed = true;
|
|
37
|
+
handle(SR_EVENT_EMITTER_TYPES.ERROR_DURING_REPLAY, [e], undefined, this.featureName, this.ee);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/** Emitted by the recorder when it starts capturing data, used to determine if we should pass errors on to the agg */
|
|
42
|
+
this.ee.on(SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, isRunning => {
|
|
43
|
+
this.replayRunning = isRunning;
|
|
44
|
+
});
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
// At this point wherein session state exists already but we haven't init SessionEntity aka verify timers.
|