@newrelic/browser-agent 1.273.1 → 1.275.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 +20 -0
- package/dist/cjs/common/constants/env.cdn.js +2 -2
- package/dist/cjs/common/constants/env.npm.js +2 -2
- package/dist/cjs/common/harvest/harvest.js +21 -0
- package/dist/cjs/common/util/submit-data.js +42 -5
- package/dist/cjs/common/wrap/wrap-events.js +1 -1
- package/dist/cjs/common/wrap/wrap-logger.js +5 -1
- package/dist/cjs/common/wrap/wrap-xhr.js +1 -0
- package/dist/cjs/features/generic_events/aggregate/index.js +10 -10
- package/dist/cjs/features/session_replay/shared/recorder.js +0 -7
- package/dist/esm/common/constants/env.cdn.js +2 -2
- package/dist/esm/common/constants/env.npm.js +2 -2
- package/dist/esm/common/harvest/harvest.js +21 -0
- package/dist/esm/common/util/submit-data.js +41 -5
- package/dist/esm/common/wrap/wrap-events.js +1 -1
- package/dist/esm/common/wrap/wrap-logger.js +5 -2
- package/dist/esm/common/wrap/wrap-xhr.js +1 -0
- package/dist/esm/features/generic_events/aggregate/index.js +10 -10
- package/dist/esm/features/session_replay/shared/recorder.js +0 -7
- package/dist/types/common/harvest/harvest.d.ts.map +1 -1
- package/dist/types/common/util/submit-data.d.ts +18 -1
- package/dist/types/common/util/submit-data.d.ts.map +1 -1
- package/dist/types/common/wrap/wrap-logger.d.ts.map +1 -1
- package/dist/types/common/wrap/wrap-xhr.d.ts.map +1 -1
- package/dist/types/features/generic_events/aggregate/index.d.ts +0 -1
- package/dist/types/features/generic_events/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/common/harvest/harvest.js +19 -0
- package/src/common/util/submit-data.js +37 -4
- package/src/common/wrap/wrap-events.js +1 -1
- package/src/common/wrap/wrap-logger.js +7 -2
- package/src/common/wrap/wrap-xhr.js +2 -0
- package/src/features/generic_events/aggregate/index.js +10 -12
- package/src/features/session_replay/shared/recorder.js +0 -7
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [1.275.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.274.0...v1.275.0) (2024-12-03)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* Allow logs api wrapper to update custom attributes ([#1265](https://github.com/newrelic/newrelic-browser-agent/issues/1265)) ([8d10e14](https://github.com/newrelic/newrelic-browser-agent/commit/8d10e14953f9a5b9ba97e865ba5476fc527ba384))
|
|
12
|
+
* Enable the browser agent to run in extension background contexts ([#1206](https://github.com/newrelic/newrelic-browser-agent/issues/1206)) ([37e976b](https://github.com/newrelic/newrelic-browser-agent/commit/37e976bf360c209efd163855e7fbe84d665e444b))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* Harvest generic events when max size is reached ([#1250](https://github.com/newrelic/newrelic-browser-agent/issues/1250)) ([e00a469](https://github.com/newrelic/newrelic-browser-agent/commit/e00a46975bcc93c48798bd9153f3a503998b0915))
|
|
18
|
+
|
|
19
|
+
## [1.274.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.273.1...v1.274.0) (2024-11-19)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* Upgrade SessionReplay libraries to latest version ([#1251](https://github.com/newrelic/newrelic-browser-agent/issues/1251)) ([2d8d114](https://github.com/newrelic/newrelic-browser-agent/commit/2d8d114e70ba2861fb5639e132f25c9c03df871b))
|
|
25
|
+
|
|
6
26
|
## [1.273.1](https://github.com/newrelic/newrelic-browser-agent/compare/v1.273.0...v1.273.1) (2024-11-18)
|
|
7
27
|
|
|
8
28
|
|
|
@@ -12,7 +12,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.DIST_METHOD = exports.BUILD_EN
|
|
|
12
12
|
/**
|
|
13
13
|
* Exposes the version of the agent
|
|
14
14
|
*/
|
|
15
|
-
const VERSION = exports.VERSION = "1.
|
|
15
|
+
const VERSION = exports.VERSION = "1.275.0";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Exposes the build type of the agent
|
|
@@ -28,4 +28,4 @@ const DIST_METHOD = exports.DIST_METHOD = 'CDN';
|
|
|
28
28
|
/**
|
|
29
29
|
* Exposes the lib version of rrweb
|
|
30
30
|
*/
|
|
31
|
-
const RRWEB_VERSION = exports.RRWEB_VERSION = "2.0.0-alpha.
|
|
31
|
+
const RRWEB_VERSION = exports.RRWEB_VERSION = "^2.0.0-alpha.17";
|
|
@@ -12,7 +12,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.DIST_METHOD = exports.BUILD_EN
|
|
|
12
12
|
/**
|
|
13
13
|
* Exposes the version of the agent
|
|
14
14
|
*/
|
|
15
|
-
const VERSION = exports.VERSION = "1.
|
|
15
|
+
const VERSION = exports.VERSION = "1.275.0";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Exposes the build type of the agent
|
|
@@ -29,4 +29,4 @@ const DIST_METHOD = exports.DIST_METHOD = 'NPM';
|
|
|
29
29
|
/**
|
|
30
30
|
* Exposes the lib version of rrweb
|
|
31
31
|
*/
|
|
32
|
-
const RRWEB_VERSION = exports.RRWEB_VERSION = "2.0.0-alpha.
|
|
32
|
+
const RRWEB_VERSION = exports.RRWEB_VERSION = "^2.0.0-alpha.17";
|
|
@@ -174,6 +174,27 @@ class Harvest extends _sharedContext.SharedContext {
|
|
|
174
174
|
}
|
|
175
175
|
cbFinished(cbResult);
|
|
176
176
|
}, (0, _eventListenerOpts.eventListenerOpts)(false));
|
|
177
|
+
} else if (!opts.unload && cbFinished && submitMethod === submitData.xhrFetch) {
|
|
178
|
+
const harvestScope = this;
|
|
179
|
+
result.then(async function (response) {
|
|
180
|
+
const status = response.status;
|
|
181
|
+
const cbResult = {
|
|
182
|
+
sent: true,
|
|
183
|
+
status,
|
|
184
|
+
fullUrl,
|
|
185
|
+
fetchResponse: response
|
|
186
|
+
};
|
|
187
|
+
if (response.status === 429) {
|
|
188
|
+
cbResult.retry = true;
|
|
189
|
+
cbResult.delay = harvestScope.tooManyRequestsDelay;
|
|
190
|
+
} else if (status === 408 || status === 500 || status === 503) {
|
|
191
|
+
cbResult.retry = true;
|
|
192
|
+
}
|
|
193
|
+
if (opts.needResponse) {
|
|
194
|
+
cbResult.responseText = await response.text();
|
|
195
|
+
}
|
|
196
|
+
cbFinished(cbResult);
|
|
197
|
+
});
|
|
177
198
|
}
|
|
178
199
|
return result;
|
|
179
200
|
}
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.beacon = beacon;
|
|
7
7
|
exports.getSubmitMethod = getSubmitMethod;
|
|
8
8
|
exports.xhr = xhr;
|
|
9
|
+
exports.xhrFetch = xhrFetch;
|
|
9
10
|
var _runtime = require("../constants/runtime");
|
|
10
11
|
/**
|
|
11
12
|
* @file Contains common methods used to transmit harvested data.
|
|
@@ -26,11 +27,47 @@ var _runtime = require("../constants/runtime");
|
|
|
26
27
|
function getSubmitMethod({
|
|
27
28
|
isFinalHarvest = false
|
|
28
29
|
} = {}) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
if (isFinalHarvest && _runtime.isBrowserScope) {
|
|
31
|
+
// Use sendBeacon for final harvest
|
|
32
|
+
return beacon;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// If not final harvest, or not browserScope, use XHR post if available
|
|
36
|
+
if (typeof XMLHttpRequest !== 'undefined') {
|
|
37
|
+
return xhr;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Fallback for web workers where XMLHttpRequest is not available
|
|
41
|
+
return xhrFetch;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
*
|
|
46
|
+
* @param url
|
|
47
|
+
* @param body
|
|
48
|
+
* @param method
|
|
49
|
+
* @param headers
|
|
50
|
+
* @returns {Promise<Response>}
|
|
51
|
+
*/
|
|
52
|
+
function xhrFetch({
|
|
53
|
+
url,
|
|
54
|
+
body = null,
|
|
55
|
+
method = 'POST',
|
|
56
|
+
headers = [{
|
|
57
|
+
key: 'content-type',
|
|
58
|
+
value: 'text/plain'
|
|
59
|
+
}]
|
|
60
|
+
}) {
|
|
61
|
+
const objHeaders = {};
|
|
62
|
+
for (const header of headers) {
|
|
63
|
+
objHeaders[header.key] = header.value;
|
|
64
|
+
}
|
|
65
|
+
return fetch(url, {
|
|
66
|
+
headers: objHeaders,
|
|
67
|
+
method,
|
|
68
|
+
body,
|
|
69
|
+
credentials: 'include'
|
|
70
|
+
});
|
|
34
71
|
}
|
|
35
72
|
|
|
36
73
|
/**
|
|
@@ -44,8 +44,8 @@ function wrapEvents(sharedEE) {
|
|
|
44
44
|
// Guard against instrumenting environments w/o necessary features
|
|
45
45
|
if ('getPrototypeOf' in Object) {
|
|
46
46
|
if (_runtime.isBrowserScope) findEventListenerProtoAndCb(document, wrapNode);
|
|
47
|
+
if (XHR) findEventListenerProtoAndCb(XHR.prototype, wrapNode);
|
|
47
48
|
findEventListenerProtoAndCb(_runtime.globalScope, wrapNode);
|
|
48
|
-
findEventListenerProtoAndCb(XHR.prototype, wrapNode);
|
|
49
49
|
}
|
|
50
50
|
ee.on(ADD_EVENT_LISTENER + '-start', function (args, target) {
|
|
51
51
|
var originalListener = args[1];
|
|
@@ -18,6 +18,8 @@ var _wrapFunction = require("./wrap-function");
|
|
|
18
18
|
* This module is used by: jserrors, spa.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
const contextMap = new Map();
|
|
22
|
+
|
|
21
23
|
/**
|
|
22
24
|
* Wraps a supplied function and adds emitter events under the `-wrap-logger-` prefix
|
|
23
25
|
* @param {Object} sharedEE - The shared event emitter on which a new scoped event emitter will be based.
|
|
@@ -38,9 +40,11 @@ function wrapLogger(sharedEE, parent, loggerFn, context) {
|
|
|
38
40
|
const ctx = new _eventContext.EventContext(_contextualEe.contextId);
|
|
39
41
|
ctx.level = context.level;
|
|
40
42
|
ctx.customAttributes = context.customAttributes;
|
|
43
|
+
const contextLookupKey = parent[loggerFn]?.[_wrapFunction.flag] || parent[loggerFn];
|
|
44
|
+
contextMap.set(contextLookupKey, ctx);
|
|
41
45
|
|
|
42
46
|
/** observe calls to <loggerFn> and emit events prefixed with `wrap-logger-` */
|
|
43
|
-
wrapFn.inPlace(parent, [loggerFn], 'wrap-logger-',
|
|
47
|
+
wrapFn.inPlace(parent, [loggerFn], 'wrap-logger-', () => contextMap.get(contextLookupKey));
|
|
44
48
|
return ee;
|
|
45
49
|
}
|
|
46
50
|
|
|
@@ -33,6 +33,7 @@ const XHR_PROPS = ['open', 'send']; // these are the specific funcs being wrappe
|
|
|
33
33
|
function wrapXhr(sharedEE) {
|
|
34
34
|
var baseEE = sharedEE || _contextualEe.ee;
|
|
35
35
|
const ee = scopedEE(baseEE);
|
|
36
|
+
if (typeof _runtime.globalScope.XMLHttpRequest === 'undefined') return ee;
|
|
36
37
|
|
|
37
38
|
// Notice if our wrapping never ran yet, the falsy NaN will not early return; but if it has,
|
|
38
39
|
// then we increment the count to track # of feats using this at runtime.
|
|
@@ -15,7 +15,6 @@ var _now = require("../../../common/timing/now");
|
|
|
15
15
|
var _registerHandler = require("../../../common/event-emitter/register-handler");
|
|
16
16
|
var _constants2 = require("../../metrics/constants");
|
|
17
17
|
var _traverse = require("../../../common/util/traverse");
|
|
18
|
-
var _agentConstants = require("../../../common/constants/agent-constants");
|
|
19
18
|
var _features = require("../../../loaders/features/features");
|
|
20
19
|
var _userActionsAggregator = require("./user-actions/user-actions-aggregator");
|
|
21
20
|
var _iframe = require("../../../common/dom/iframe");
|
|
@@ -188,8 +187,16 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
188
187
|
/** Event-specific attributes take precedence over agent-level custom attributes and fallbacks */
|
|
189
188
|
...obj
|
|
190
189
|
};
|
|
191
|
-
this.events.add(eventAttributes);
|
|
192
|
-
this.
|
|
190
|
+
const addedEvent = this.events.add(eventAttributes);
|
|
191
|
+
if (!addedEvent && !this.events.isEmpty()) {
|
|
192
|
+
/** could not add the event because it pushed the buffer over the limit
|
|
193
|
+
* so we harvest early, and try to add it again now that the buffer is cleared
|
|
194
|
+
* if it fails again, we do nothing
|
|
195
|
+
*/
|
|
196
|
+
this.ee.emit(_constants2.SUPPORTABILITY_METRIC_CHANNEL, ['GenericEvents/Harvest/Max/Seen']);
|
|
197
|
+
this.harvestScheduler.runHarvest();
|
|
198
|
+
this.events.add(eventAttributes);
|
|
199
|
+
}
|
|
193
200
|
}
|
|
194
201
|
serializer(eventBuffer) {
|
|
195
202
|
return (0, _traverse.applyFnToProps)({
|
|
@@ -202,12 +209,5 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
202
209
|
at: this.agentRef.info.atts
|
|
203
210
|
};
|
|
204
211
|
}
|
|
205
|
-
checkEventLimits() {
|
|
206
|
-
// check if we've reached any harvest limits...
|
|
207
|
-
if (this.events.byteSize() > _agentConstants.IDEAL_PAYLOAD_SIZE) {
|
|
208
|
-
this.ee.emit(_constants2.SUPPORTABILITY_METRIC_CHANNEL, ['GenericEvents/Harvest/Max/Seen']);
|
|
209
|
-
this.harvestScheduler.runHarvest();
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
212
|
}
|
|
213
213
|
exports.Aggregate = Aggregate;
|
|
@@ -110,13 +110,6 @@ class Recorder {
|
|
|
110
110
|
inlineImages: inline_images,
|
|
111
111
|
collectFonts: collect_fonts,
|
|
112
112
|
checkoutEveryNms: _constants.CHECKOUT_MS[this.parent.mode],
|
|
113
|
-
/** Emits errors thrown by rrweb directly before bubbling them up to the window */
|
|
114
|
-
errorHandler: err => {
|
|
115
|
-
/** capture rrweb errors as "internal" errors only */
|
|
116
|
-
this.parent.ee.emit('internal-error', [err]);
|
|
117
|
-
/** returning true informs rrweb to swallow the error instead of throwing it to the window */
|
|
118
|
-
return true;
|
|
119
|
-
},
|
|
120
113
|
recordAfter: 'DOMContentLoaded'
|
|
121
114
|
});
|
|
122
115
|
this.stopRecording = () => {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* Exposes the version of the agent
|
|
8
8
|
*/
|
|
9
|
-
export const VERSION = "1.
|
|
9
|
+
export const VERSION = "1.275.0";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Exposes the build type of the agent
|
|
@@ -22,4 +22,4 @@ export const DIST_METHOD = 'CDN';
|
|
|
22
22
|
/**
|
|
23
23
|
* Exposes the lib version of rrweb
|
|
24
24
|
*/
|
|
25
|
-
export const RRWEB_VERSION = "2.0.0-alpha.
|
|
25
|
+
export const RRWEB_VERSION = "^2.0.0-alpha.17";
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
/**
|
|
7
7
|
* Exposes the version of the agent
|
|
8
8
|
*/
|
|
9
|
-
export const VERSION = "1.
|
|
9
|
+
export const VERSION = "1.275.0";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Exposes the build type of the agent
|
|
@@ -23,4 +23,4 @@ export const DIST_METHOD = 'NPM';
|
|
|
23
23
|
/**
|
|
24
24
|
* Exposes the lib version of rrweb
|
|
25
25
|
*/
|
|
26
|
-
export const RRWEB_VERSION = "2.0.0-alpha.
|
|
26
|
+
export const RRWEB_VERSION = "^2.0.0-alpha.17";
|
|
@@ -166,6 +166,27 @@ export class Harvest extends SharedContext {
|
|
|
166
166
|
}
|
|
167
167
|
cbFinished(cbResult);
|
|
168
168
|
}, eventListenerOpts(false));
|
|
169
|
+
} else if (!opts.unload && cbFinished && submitMethod === submitData.xhrFetch) {
|
|
170
|
+
const harvestScope = this;
|
|
171
|
+
result.then(async function (response) {
|
|
172
|
+
const status = response.status;
|
|
173
|
+
const cbResult = {
|
|
174
|
+
sent: true,
|
|
175
|
+
status,
|
|
176
|
+
fullUrl,
|
|
177
|
+
fetchResponse: response
|
|
178
|
+
};
|
|
179
|
+
if (response.status === 429) {
|
|
180
|
+
cbResult.retry = true;
|
|
181
|
+
cbResult.delay = harvestScope.tooManyRequestsDelay;
|
|
182
|
+
} else if (status === 408 || status === 500 || status === 503) {
|
|
183
|
+
cbResult.retry = true;
|
|
184
|
+
}
|
|
185
|
+
if (opts.needResponse) {
|
|
186
|
+
cbResult.responseText = await response.text();
|
|
187
|
+
}
|
|
188
|
+
cbFinished(cbResult);
|
|
189
|
+
});
|
|
169
190
|
}
|
|
170
191
|
return result;
|
|
171
192
|
}
|
|
@@ -19,11 +19,47 @@ import { isBrowserScope } from '../constants/runtime';
|
|
|
19
19
|
export function getSubmitMethod({
|
|
20
20
|
isFinalHarvest = false
|
|
21
21
|
} = {}) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
if (isFinalHarvest && isBrowserScope) {
|
|
23
|
+
// Use sendBeacon for final harvest
|
|
24
|
+
return beacon;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// If not final harvest, or not browserScope, use XHR post if available
|
|
28
|
+
if (typeof XMLHttpRequest !== 'undefined') {
|
|
29
|
+
return xhr;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Fallback for web workers where XMLHttpRequest is not available
|
|
33
|
+
return xhrFetch;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
*
|
|
38
|
+
* @param url
|
|
39
|
+
* @param body
|
|
40
|
+
* @param method
|
|
41
|
+
* @param headers
|
|
42
|
+
* @returns {Promise<Response>}
|
|
43
|
+
*/
|
|
44
|
+
export function xhrFetch({
|
|
45
|
+
url,
|
|
46
|
+
body = null,
|
|
47
|
+
method = 'POST',
|
|
48
|
+
headers = [{
|
|
49
|
+
key: 'content-type',
|
|
50
|
+
value: 'text/plain'
|
|
51
|
+
}]
|
|
52
|
+
}) {
|
|
53
|
+
const objHeaders = {};
|
|
54
|
+
for (const header of headers) {
|
|
55
|
+
objHeaders[header.key] = header.value;
|
|
56
|
+
}
|
|
57
|
+
return fetch(url, {
|
|
58
|
+
headers: objHeaders,
|
|
59
|
+
method,
|
|
60
|
+
body,
|
|
61
|
+
credentials: 'include'
|
|
62
|
+
});
|
|
27
63
|
}
|
|
28
64
|
|
|
29
65
|
/**
|
|
@@ -36,8 +36,8 @@ export function wrapEvents(sharedEE) {
|
|
|
36
36
|
// Guard against instrumenting environments w/o necessary features
|
|
37
37
|
if ('getPrototypeOf' in Object) {
|
|
38
38
|
if (isBrowserScope) findEventListenerProtoAndCb(document, wrapNode);
|
|
39
|
+
if (XHR) findEventListenerProtoAndCb(XHR.prototype, wrapNode);
|
|
39
40
|
findEventListenerProtoAndCb(globalScope, wrapNode);
|
|
40
|
-
findEventListenerProtoAndCb(XHR.prototype, wrapNode);
|
|
41
41
|
}
|
|
42
42
|
ee.on(ADD_EVENT_LISTENER + '-start', function (args, target) {
|
|
43
43
|
var originalListener = args[1];
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
import { ee as baseEE, contextId } from '../event-emitter/contextual-ee';
|
|
11
11
|
import { EventContext } from '../event-emitter/event-context';
|
|
12
12
|
import { warn } from '../util/console';
|
|
13
|
-
import { createWrapperWithEmitter as wfn } from './wrap-function';
|
|
13
|
+
import { flag, createWrapperWithEmitter as wfn } from './wrap-function';
|
|
14
|
+
const contextMap = new Map();
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Wraps a supplied function and adds emitter events under the `-wrap-logger-` prefix
|
|
@@ -32,9 +33,11 @@ export function wrapLogger(sharedEE, parent, loggerFn, context) {
|
|
|
32
33
|
const ctx = new EventContext(contextId);
|
|
33
34
|
ctx.level = context.level;
|
|
34
35
|
ctx.customAttributes = context.customAttributes;
|
|
36
|
+
const contextLookupKey = parent[loggerFn]?.[flag] || parent[loggerFn];
|
|
37
|
+
contextMap.set(contextLookupKey, ctx);
|
|
35
38
|
|
|
36
39
|
/** observe calls to <loggerFn> and emit events prefixed with `wrap-logger-` */
|
|
37
|
-
wrapFn.inPlace(parent, [loggerFn], 'wrap-logger-',
|
|
40
|
+
wrapFn.inPlace(parent, [loggerFn], 'wrap-logger-', () => contextMap.get(contextLookupKey));
|
|
38
41
|
return ee;
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -26,6 +26,7 @@ const XHR_PROPS = ['open', 'send']; // these are the specific funcs being wrappe
|
|
|
26
26
|
export function wrapXhr(sharedEE) {
|
|
27
27
|
var baseEE = sharedEE || contextualEE;
|
|
28
28
|
const ee = scopedEE(baseEE);
|
|
29
|
+
if (typeof globalScope.XMLHttpRequest === 'undefined') return ee;
|
|
29
30
|
|
|
30
31
|
// Notice if our wrapping never ran yet, the falsy NaN will not early return; but if it has,
|
|
31
32
|
// then we increment the count to track # of feats using this at runtime.
|
|
@@ -13,7 +13,6 @@ import { now } from '../../../common/timing/now';
|
|
|
13
13
|
import { registerHandler } from '../../../common/event-emitter/register-handler';
|
|
14
14
|
import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants';
|
|
15
15
|
import { applyFnToProps } from '../../../common/util/traverse';
|
|
16
|
-
import { IDEAL_PAYLOAD_SIZE } from '../../../common/constants/agent-constants';
|
|
17
16
|
import { FEATURE_TO_ENDPOINT } from '../../../loaders/features/features';
|
|
18
17
|
import { UserActionsAggregator } from './user-actions/user-actions-aggregator';
|
|
19
18
|
import { isIFrameWindow } from '../../../common/dom/iframe';
|
|
@@ -181,8 +180,16 @@ export class Aggregate extends AggregateBase {
|
|
|
181
180
|
/** Event-specific attributes take precedence over agent-level custom attributes and fallbacks */
|
|
182
181
|
...obj
|
|
183
182
|
};
|
|
184
|
-
this.events.add(eventAttributes);
|
|
185
|
-
this.
|
|
183
|
+
const addedEvent = this.events.add(eventAttributes);
|
|
184
|
+
if (!addedEvent && !this.events.isEmpty()) {
|
|
185
|
+
/** could not add the event because it pushed the buffer over the limit
|
|
186
|
+
* so we harvest early, and try to add it again now that the buffer is cleared
|
|
187
|
+
* if it fails again, we do nothing
|
|
188
|
+
*/
|
|
189
|
+
this.ee.emit(SUPPORTABILITY_METRIC_CHANNEL, ['GenericEvents/Harvest/Max/Seen']);
|
|
190
|
+
this.harvestScheduler.runHarvest();
|
|
191
|
+
this.events.add(eventAttributes);
|
|
192
|
+
}
|
|
186
193
|
}
|
|
187
194
|
serializer(eventBuffer) {
|
|
188
195
|
return applyFnToProps({
|
|
@@ -195,11 +202,4 @@ export class Aggregate extends AggregateBase {
|
|
|
195
202
|
at: this.agentRef.info.atts
|
|
196
203
|
};
|
|
197
204
|
}
|
|
198
|
-
checkEventLimits() {
|
|
199
|
-
// check if we've reached any harvest limits...
|
|
200
|
-
if (this.events.byteSize() > IDEAL_PAYLOAD_SIZE) {
|
|
201
|
-
this.ee.emit(SUPPORTABILITY_METRIC_CHANNEL, ['GenericEvents/Harvest/Max/Seen']);
|
|
202
|
-
this.harvestScheduler.runHarvest();
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
205
|
}
|
|
@@ -104,13 +104,6 @@ export class Recorder {
|
|
|
104
104
|
inlineImages: inline_images,
|
|
105
105
|
collectFonts: collect_fonts,
|
|
106
106
|
checkoutEveryNms: CHECKOUT_MS[this.parent.mode],
|
|
107
|
-
/** Emits errors thrown by rrweb directly before bubbling them up to the window */
|
|
108
|
-
errorHandler: err => {
|
|
109
|
-
/** capture rrweb errors as "internal" errors only */
|
|
110
|
-
this.parent.ee.emit('internal-error', [err]);
|
|
111
|
-
/** returning true informs rrweb to swallow the error instead of throwing it to the window */
|
|
112
|
-
return true;
|
|
113
|
-
},
|
|
114
107
|
recordAfter: 'DOMContentLoaded'
|
|
115
108
|
});
|
|
116
109
|
this.stopRecording = () => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"harvest.d.ts","sourceRoot":"","sources":["../../../../src/common/harvest/harvest.js"],"names":[],"mappings":"AAsBA;;;;;;GAMG;AACH;IAII,0BAA2H;IAC3H,gBAA2E;IAE3E,YAAiB;IAGnB;;;;;OAKG;IACH,aAFW,eAAe,WAWzB;IAED;;;OAGG;IACH,YAFW,eAAe,WAMzB;IAED;;;;;;OAMG;IACH,gGAJW,eAAe,GACb,OAAO,
|
|
1
|
+
{"version":3,"file":"harvest.d.ts","sourceRoot":"","sources":["../../../../src/common/harvest/harvest.js"],"names":[],"mappings":"AAsBA;;;;;;GAMG;AACH;IAII,0BAA2H;IAC3H,gBAA2E;IAE3E,YAAiB;IAGnB;;;;;OAKG;IACH,aAFW,eAAe,WAWzB;IAED;;;OAGG;IACH,YAFW,eAAe,WAMzB;IAED;;;;;;OAMG;IACH,gGAJW,eAAe,GACb,OAAO,CAkGnB;IAGD,gDAqBC;IAED;;;;;;;OAOG;IACH,wBALW,yBAAyB,WACzB,6BAA6B,GAE3B,cAAc,CA2B1B;IAED;;;;;;;OAOG;IACH,uBAHW,cAAc,GACZ,cAAc,CAuB1B;IAED;;;;;OAKG;IACH,aAHW,yBAAyB,YACzB,sBAAsB,QAQhC;CACF;8BA1PY,OAAO,YAAY,EAAE,eAAe;wCACpC,OAAO,YAAY,EAAE,yBAAyB;6BAC9C,OAAO,YAAY,EAAE,cAAc;qCACnC,OAAO,YAAY,EAAE,sBAAsB;4CAC3C,OAAO,YAAY,EAAE,6BAA6B;8BAbjC,2BAA2B"}
|
|
@@ -9,7 +9,24 @@
|
|
|
9
9
|
*/
|
|
10
10
|
export function getSubmitMethod({ isFinalHarvest }?: {
|
|
11
11
|
isFinalHarvest: boolean;
|
|
12
|
-
}): typeof xhr | typeof beacon;
|
|
12
|
+
}): typeof xhr | typeof beacon | typeof xhrFetch;
|
|
13
|
+
/**
|
|
14
|
+
*
|
|
15
|
+
* @param url
|
|
16
|
+
* @param body
|
|
17
|
+
* @param method
|
|
18
|
+
* @param headers
|
|
19
|
+
* @returns {Promise<Response>}
|
|
20
|
+
*/
|
|
21
|
+
export function xhrFetch({ url, body, method, headers }: {
|
|
22
|
+
url: any;
|
|
23
|
+
body?: null | undefined;
|
|
24
|
+
method?: string | undefined;
|
|
25
|
+
headers?: {
|
|
26
|
+
key: string;
|
|
27
|
+
value: string;
|
|
28
|
+
}[] | undefined;
|
|
29
|
+
}): Promise<Response>;
|
|
13
30
|
/**
|
|
14
31
|
* Send via XHR
|
|
15
32
|
* @param {Object} args - The args.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"submit-data.d.ts","sourceRoot":"","sources":["../../../../src/common/util/submit-data.js"],"names":[],"mappings":"AAQA;;GAEG;AAEH;;;;;GAKG;AACH,qDAHG;IAAsB,cAAc,EAA5B,OAAO;CAEjB,
|
|
1
|
+
{"version":3,"file":"submit-data.d.ts","sourceRoot":"","sources":["../../../../src/common/util/submit-data.js"],"names":[],"mappings":"AAQA;;GAEG;AAEH;;;;;GAKG;AACH,qDAHG;IAAsB,cAAc,EAA5B,OAAO;CAEjB,gDAcA;AAED;;;;;;;GAOG;AACH;;;;;;;;IAFa,OAAO,CAAC,QAAQ,CAAC,CAkB7B;AAED;;;;;;;;;GASG;AACH,0DAPG;IAAqB,GAAG,EAAhB,MAAM;IACQ,IAAI,GAAlB,MAAM,YAAC;IACQ,IAAI,GAAnB,OAAO,YAAC;IACO,MAAM,GAArB,MAAM,YAAC;IAC+B,OAAO;aAAvC,MAAM;eAAS,MAAM;;CACnC,GAAU,cAAc,CAmB1B;AAED;;;;;;GAMG;AACH,sCAJG;IAAqB,GAAG,EAAhB,MAAM;IACQ,IAAI,GAAlB,MAAM,YAAC;CACf,GAAU,OAAO,CAanB;6BAjGY,0BAAU"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrap-logger.d.ts","sourceRoot":"","sources":["../../../../src/common/wrap/wrap-logger.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"wrap-logger.d.ts","sourceRoot":"","sources":["../../../../src/common/wrap/wrap-logger.js"],"names":[],"mappings":"AAgBA;;;;;;GAMG;AAEH,qCANW,MAAM,UACN,MAAM,YACN,MAAM,iBACJ,MAAM,CAuBlB;AAED;;;;;;GAMG;AACH,mCAJW,MAAM,GAEJ,MAAM,CAIlB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrap-xhr.d.ts","sourceRoot":"","sources":["../../../../src/common/wrap/wrap-xhr.js"],"names":[],"mappings":"AAmBA;;;;;GAKG;AAEH,kCAJW,MAAM,GACJ,MAAM,
|
|
1
|
+
{"version":3,"file":"wrap-xhr.d.ts","sourceRoot":"","sources":["../../../../src/common/wrap/wrap-xhr.js"],"names":[],"mappings":"AAmBA;;;;;GAKG;AAEH,kCAJW,MAAM,GACJ,MAAM,CAiLlB;AAED;;;;;;GAMG;AACH,mCAJW,MAAM,GAEJ,MAAM,CAIlB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/aggregate/index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/generic_events/aggregate/index.js"],"names":[],"mappings":"AAmBA;IACE,2BAAiC;IACjC,2BAiHC;IA9GC,yBAA4B;IAC5B,wBAAyE;IAEzE,gCAAkG;IA4B9F,4CAAuD;IAsEzD,mCAGQ;IASZ;;;;;;;;;;;OAWG;IACH,eAHW,MAAM,YAAC,QA0CjB;IAED,qCAEC;IAED;;;MAEC;CACF;8BA5L6B,4BAA4B;sCAOpB,wCAAwC;iCAX7C,2CAA2C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/shared/recorder.js"],"names":[],"mappings":"AAaA;IAUE,yBAkBC;IAdC,iEAAiE;IACjE,mBAAsB;IACtB,6DAA6D;IAC7D,oCAAuC;IACvC,+IAA+I;IAC/I,yBAA4B;IAC5B,kIAAkI;IAClI,kBAAqB;IACrB,sDAAsD;IACtD,YAAoB;IACpB,0FAA0F;IAC1F,eAAqG;IACrG,uIAAuI;IACvI,0BAAyE;IAG3E;;;;;;;;;MAmBC;IAED,mFAAmF;IACnF,oBAKC;IAED,qDAAqD;IACrD,
|
|
1
|
+
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/shared/recorder.js"],"names":[],"mappings":"AAaA;IAUE,yBAkBC;IAdC,iEAAiE;IACjE,mBAAsB;IACtB,6DAA6D;IAC7D,oCAAuC;IACvC,+IAA+I;IAC/I,yBAA4B;IAC5B,kIAAkI;IAClI,kBAAqB;IACrB,sDAAsD;IACtD,YAAoB;IACpB,0FAA0F;IAC1F,eAAqG;IACrG,uIAAuI;IACvI,0BAAyE;IAG3E;;;;;;;;;MAmBC;IAED,mFAAmF;IACnF,oBAKC;IAED,qDAAqD;IACrD,uBAoCC;IAED;;;;;OAKG;IACH,aAHW,GAAC,cACD,GAAC,QAgCX;IAED,0HAA0H;IAC1H,yCAoDC;IA1CG,8BAAoB;IA4CxB,0HAA0H;IAC1H,yBAOC;IAED,wBAEC;IAED,gCAAgC;IAChC,uCAGC;IAED;;;SAGK;IACL,oCAGC;;CACF;+BApO8B,mBAAmB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@newrelic/browser-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.275.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
|
|
6
6
|
"description": "New Relic Browser Agent",
|
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
},
|
|
196
196
|
"dependencies": {
|
|
197
197
|
"fflate": "0.7.4",
|
|
198
|
-
"rrweb": "2.0.0-alpha.
|
|
198
|
+
"rrweb": "^2.0.0-alpha.17",
|
|
199
199
|
"web-vitals": "4.2.3"
|
|
200
200
|
},
|
|
201
201
|
"devDependencies": {
|
|
@@ -144,6 +144,25 @@ export class Harvest extends SharedContext {
|
|
|
144
144
|
}
|
|
145
145
|
cbFinished(cbResult)
|
|
146
146
|
}, eventListenerOpts(false))
|
|
147
|
+
} else if (!opts.unload && cbFinished && submitMethod === submitData.xhrFetch) {
|
|
148
|
+
const harvestScope = this
|
|
149
|
+
result.then(async function (response) {
|
|
150
|
+
const status = response.status
|
|
151
|
+
const cbResult = { sent: true, status, fullUrl, fetchResponse: response }
|
|
152
|
+
|
|
153
|
+
if (response.status === 429) {
|
|
154
|
+
cbResult.retry = true
|
|
155
|
+
cbResult.delay = harvestScope.tooManyRequestsDelay
|
|
156
|
+
} else if (status === 408 || status === 500 || status === 503) {
|
|
157
|
+
cbResult.retry = true
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (opts.needResponse) {
|
|
161
|
+
cbResult.responseText = await response.text()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
cbFinished(cbResult)
|
|
165
|
+
})
|
|
147
166
|
}
|
|
148
167
|
|
|
149
168
|
return result
|
|
@@ -17,11 +17,44 @@ import { isBrowserScope } from '../constants/runtime'
|
|
|
17
17
|
* a final harvest within the agent.
|
|
18
18
|
*/
|
|
19
19
|
export function getSubmitMethod ({ isFinalHarvest = false } = {}) {
|
|
20
|
-
|
|
20
|
+
if (isFinalHarvest && isBrowserScope) {
|
|
21
21
|
// Use sendBeacon for final harvest
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
return beacon
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// If not final harvest, or not browserScope, use XHR post if available
|
|
26
|
+
if (typeof XMLHttpRequest !== 'undefined') {
|
|
27
|
+
return xhr
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Fallback for web workers where XMLHttpRequest is not available
|
|
31
|
+
return xhrFetch
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
*
|
|
36
|
+
* @param url
|
|
37
|
+
* @param body
|
|
38
|
+
* @param method
|
|
39
|
+
* @param headers
|
|
40
|
+
* @returns {Promise<Response>}
|
|
41
|
+
*/
|
|
42
|
+
export function xhrFetch ({
|
|
43
|
+
url,
|
|
44
|
+
body = null,
|
|
45
|
+
method = 'POST',
|
|
46
|
+
headers = [{
|
|
47
|
+
key: 'content-type',
|
|
48
|
+
value: 'text/plain'
|
|
49
|
+
}]
|
|
50
|
+
}) {
|
|
51
|
+
const objHeaders = {}
|
|
52
|
+
|
|
53
|
+
for (const header of headers) {
|
|
54
|
+
objHeaders[header.key] = header.value
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return fetch(url, { headers: objHeaders, method, body, credentials: 'include' })
|
|
25
58
|
}
|
|
26
59
|
|
|
27
60
|
/**
|
|
@@ -37,8 +37,8 @@ export function wrapEvents (sharedEE) {
|
|
|
37
37
|
// Guard against instrumenting environments w/o necessary features
|
|
38
38
|
if ('getPrototypeOf' in Object) {
|
|
39
39
|
if (isBrowserScope) findEventListenerProtoAndCb(document, wrapNode)
|
|
40
|
+
if (XHR) findEventListenerProtoAndCb(XHR.prototype, wrapNode)
|
|
40
41
|
findEventListenerProtoAndCb(globalScope, wrapNode)
|
|
41
|
-
findEventListenerProtoAndCb(XHR.prototype, wrapNode)
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
ee.on(ADD_EVENT_LISTENER + '-start', function (args, target) {
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
import { ee as baseEE, contextId } from '../event-emitter/contextual-ee'
|
|
11
11
|
import { EventContext } from '../event-emitter/event-context'
|
|
12
12
|
import { warn } from '../util/console'
|
|
13
|
-
import { createWrapperWithEmitter as wfn } from './wrap-function'
|
|
13
|
+
import { flag, createWrapperWithEmitter as wfn } from './wrap-function'
|
|
14
|
+
|
|
15
|
+
const contextMap = new Map()
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Wraps a supplied function and adds emitter events under the `-wrap-logger-` prefix
|
|
@@ -33,8 +35,11 @@ export function wrapLogger(sharedEE, parent, loggerFn, context) {
|
|
|
33
35
|
ctx.level = context.level
|
|
34
36
|
ctx.customAttributes = context.customAttributes
|
|
35
37
|
|
|
38
|
+
const contextLookupKey = parent[loggerFn]?.[flag] || parent[loggerFn]
|
|
39
|
+
contextMap.set(contextLookupKey, ctx)
|
|
40
|
+
|
|
36
41
|
/** observe calls to <loggerFn> and emit events prefixed with `wrap-logger-` */
|
|
37
|
-
wrapFn.inPlace(parent, [loggerFn], 'wrap-logger-',
|
|
42
|
+
wrapFn.inPlace(parent, [loggerFn], 'wrap-logger-', () => contextMap.get(contextLookupKey))
|
|
38
43
|
|
|
39
44
|
return ee
|
|
40
45
|
}
|
|
@@ -28,6 +28,8 @@ export function wrapXhr (sharedEE) {
|
|
|
28
28
|
var baseEE = sharedEE || contextualEE
|
|
29
29
|
const ee = scopedEE(baseEE)
|
|
30
30
|
|
|
31
|
+
if (typeof globalScope.XMLHttpRequest === 'undefined') return ee
|
|
32
|
+
|
|
31
33
|
// Notice if our wrapping never ran yet, the falsy NaN will not early return; but if it has,
|
|
32
34
|
// then we increment the count to track # of feats using this at runtime.
|
|
33
35
|
if (wrapped[ee.debugId]++) return ee
|
|
@@ -13,7 +13,6 @@ import { now } from '../../../common/timing/now'
|
|
|
13
13
|
import { registerHandler } from '../../../common/event-emitter/register-handler'
|
|
14
14
|
import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
|
|
15
15
|
import { applyFnToProps } from '../../../common/util/traverse'
|
|
16
|
-
import { IDEAL_PAYLOAD_SIZE } from '../../../common/constants/agent-constants'
|
|
17
16
|
import { FEATURE_TO_ENDPOINT } from '../../../loaders/features/features'
|
|
18
17
|
import { UserActionsAggregator } from './user-actions/user-actions-aggregator'
|
|
19
18
|
import { isIFrameWindow } from '../../../common/dom/iframe'
|
|
@@ -177,9 +176,16 @@ export class Aggregate extends AggregateBase {
|
|
|
177
176
|
...obj
|
|
178
177
|
}
|
|
179
178
|
|
|
180
|
-
this.events.add(eventAttributes)
|
|
181
|
-
|
|
182
|
-
|
|
179
|
+
const addedEvent = this.events.add(eventAttributes)
|
|
180
|
+
if (!addedEvent && !this.events.isEmpty()) {
|
|
181
|
+
/** could not add the event because it pushed the buffer over the limit
|
|
182
|
+
* so we harvest early, and try to add it again now that the buffer is cleared
|
|
183
|
+
* if it fails again, we do nothing
|
|
184
|
+
*/
|
|
185
|
+
this.ee.emit(SUPPORTABILITY_METRIC_CHANNEL, ['GenericEvents/Harvest/Max/Seen'])
|
|
186
|
+
this.harvestScheduler.runHarvest()
|
|
187
|
+
this.events.add(eventAttributes)
|
|
188
|
+
}
|
|
183
189
|
}
|
|
184
190
|
|
|
185
191
|
serializer (eventBuffer) {
|
|
@@ -189,12 +195,4 @@ export class Aggregate extends AggregateBase {
|
|
|
189
195
|
queryStringsBuilder () {
|
|
190
196
|
return { ua: this.agentRef.info.userAttributes, at: this.agentRef.info.atts }
|
|
191
197
|
}
|
|
192
|
-
|
|
193
|
-
checkEventLimits () {
|
|
194
|
-
// check if we've reached any harvest limits...
|
|
195
|
-
if (this.events.byteSize() > IDEAL_PAYLOAD_SIZE) {
|
|
196
|
-
this.ee.emit(SUPPORTABILITY_METRIC_CHANNEL, ['GenericEvents/Harvest/Max/Seen'])
|
|
197
|
-
this.harvestScheduler.runHarvest()
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
198
|
}
|
|
@@ -99,13 +99,6 @@ export class Recorder {
|
|
|
99
99
|
inlineImages: inline_images,
|
|
100
100
|
collectFonts: collect_fonts,
|
|
101
101
|
checkoutEveryNms: CHECKOUT_MS[this.parent.mode],
|
|
102
|
-
/** Emits errors thrown by rrweb directly before bubbling them up to the window */
|
|
103
|
-
errorHandler: (err) => {
|
|
104
|
-
/** capture rrweb errors as "internal" errors only */
|
|
105
|
-
this.parent.ee.emit('internal-error', [err])
|
|
106
|
-
/** returning true informs rrweb to swallow the error instead of throwing it to the window */
|
|
107
|
-
return true
|
|
108
|
-
},
|
|
109
102
|
recordAfter: 'DOMContentLoaded'
|
|
110
103
|
})
|
|
111
104
|
|