@newrelic/browser-agent 1.246.1 → 1.248.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.
Files changed (107) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +10 -2
  3. package/dist/cjs/common/config/state/info.js +2 -1
  4. package/dist/cjs/common/config/state/init.js +2 -1
  5. package/dist/cjs/common/config/state/loader-config.js +2 -1
  6. package/dist/cjs/common/config/state/runtime.js +2 -1
  7. package/dist/cjs/common/constants/env.cdn.js +1 -1
  8. package/dist/cjs/common/constants/env.npm.js +1 -1
  9. package/dist/cjs/common/constants/runtime.js +3 -1
  10. package/dist/cjs/common/dispatch/global-event.js +19 -0
  11. package/dist/cjs/common/session/session-entity.js +14 -9
  12. package/dist/cjs/common/util/feature-flags.js +17 -12
  13. package/dist/cjs/common/window/load.js +1 -0
  14. package/dist/cjs/common/window/nreum.js +18 -17
  15. package/dist/cjs/features/metrics/aggregate/index.js +4 -2
  16. package/dist/cjs/features/session_replay/aggregate/index.js +70 -20
  17. package/dist/cjs/features/session_trace/aggregate/index.js +33 -18
  18. package/dist/cjs/features/spa/aggregate/index.js +7 -1
  19. package/dist/cjs/features/utils/aggregate-base.js +3 -1
  20. package/dist/cjs/loaders/agent-base.js +19 -0
  21. package/dist/cjs/loaders/agent.js +6 -4
  22. package/dist/cjs/loaders/api/api.js +10 -1
  23. package/dist/cjs/loaders/configure/configure.js +14 -13
  24. package/dist/cjs/loaders/configure/nonce.js +13 -0
  25. package/dist/cjs/loaders/configure/nonce.npm.js +2 -0
  26. package/dist/cjs/loaders/micro-agent.js +5 -4
  27. package/dist/esm/common/config/state/info.js +3 -2
  28. package/dist/esm/common/config/state/init.js +3 -2
  29. package/dist/esm/common/config/state/loader-config.js +3 -2
  30. package/dist/esm/common/config/state/runtime.js +3 -2
  31. package/dist/esm/common/constants/env.cdn.js +1 -1
  32. package/dist/esm/common/constants/env.npm.js +1 -1
  33. package/dist/esm/common/constants/runtime.js +1 -0
  34. package/dist/esm/common/dispatch/global-event.js +13 -0
  35. package/dist/esm/common/session/session-entity.js +14 -9
  36. package/dist/esm/common/util/feature-flags.js +17 -12
  37. package/dist/esm/common/window/load.js +1 -1
  38. package/dist/esm/common/window/nreum.js +16 -16
  39. package/dist/esm/features/metrics/aggregate/index.js +4 -2
  40. package/dist/esm/features/session_replay/aggregate/index.js +69 -19
  41. package/dist/esm/features/session_trace/aggregate/index.js +33 -18
  42. package/dist/esm/features/spa/aggregate/index.js +7 -1
  43. package/dist/esm/features/utils/aggregate-base.js +3 -1
  44. package/dist/esm/loaders/agent-base.js +19 -0
  45. package/dist/esm/loaders/agent.js +7 -5
  46. package/dist/esm/loaders/api/api.js +10 -1
  47. package/dist/esm/loaders/configure/configure.js +15 -14
  48. package/dist/esm/loaders/configure/nonce.js +11 -0
  49. package/dist/esm/loaders/configure/nonce.npm.js +1 -0
  50. package/dist/esm/loaders/micro-agent.js +6 -5
  51. package/dist/types/common/config/state/info.d.ts.map +1 -1
  52. package/dist/types/common/config/state/init.d.ts.map +1 -1
  53. package/dist/types/common/config/state/loader-config.d.ts.map +1 -1
  54. package/dist/types/common/config/state/runtime.d.ts.map +1 -1
  55. package/dist/types/common/constants/runtime.d.ts +1 -0
  56. package/dist/types/common/constants/runtime.d.ts.map +1 -1
  57. package/dist/types/common/dispatch/global-event.d.ts +2 -0
  58. package/dist/types/common/dispatch/global-event.d.ts.map +1 -0
  59. package/dist/types/common/session/session-entity.d.ts +1 -0
  60. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  61. package/dist/types/common/util/feature-flags.d.ts.map +1 -1
  62. package/dist/types/common/window/load.d.ts +1 -0
  63. package/dist/types/common/window/load.d.ts.map +1 -1
  64. package/dist/types/common/window/nreum.d.ts +7 -1
  65. package/dist/types/common/window/nreum.d.ts.map +1 -1
  66. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  67. package/dist/types/features/session_replay/aggregate/index.d.ts +11 -1
  68. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  69. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  70. package/dist/types/features/spa/aggregate/index.d.ts.map +1 -1
  71. package/dist/types/loaders/agent-base.d.ts +13 -0
  72. package/dist/types/loaders/agent-base.d.ts.map +1 -1
  73. package/dist/types/loaders/agent.d.ts.map +1 -1
  74. package/dist/types/loaders/api/api.d.ts +2 -0
  75. package/dist/types/loaders/api/api.d.ts.map +1 -1
  76. package/dist/types/loaders/configure/configure.d.ts +4 -11
  77. package/dist/types/loaders/configure/configure.d.ts.map +1 -1
  78. package/dist/types/loaders/configure/nonce.d.ts +1 -0
  79. package/dist/types/loaders/configure/nonce.d.ts.map +1 -0
  80. package/dist/types/loaders/configure/nonce.npm.d.ts +1 -0
  81. package/dist/types/loaders/configure/nonce.npm.d.ts.map +1 -0
  82. package/dist/types/loaders/micro-agent.d.ts +2 -2
  83. package/dist/types/loaders/micro-agent.d.ts.map +1 -1
  84. package/package.json +2 -1
  85. package/src/common/config/state/info.js +3 -2
  86. package/src/common/config/state/init.js +3 -2
  87. package/src/common/config/state/loader-config.js +3 -2
  88. package/src/common/config/state/runtime.js +3 -2
  89. package/src/common/constants/runtime.js +2 -0
  90. package/src/common/dispatch/global-event.js +12 -0
  91. package/src/common/session/session-entity.js +13 -9
  92. package/src/common/util/feature-flags.js +15 -12
  93. package/src/common/window/__mocks__/nreum.js +2 -1
  94. package/src/common/window/load.js +1 -1
  95. package/src/common/window/nreum.js +15 -18
  96. package/src/features/metrics/aggregate/index.js +5 -1
  97. package/src/features/session_replay/aggregate/index.js +74 -23
  98. package/src/features/session_trace/aggregate/index.js +33 -14
  99. package/src/features/spa/aggregate/index.js +10 -1
  100. package/src/features/utils/aggregate-base.js +1 -1
  101. package/src/loaders/agent-base.js +19 -0
  102. package/src/loaders/agent.js +5 -5
  103. package/src/loaders/api/api.js +12 -1
  104. package/src/loaders/configure/configure.js +17 -15
  105. package/src/loaders/configure/nonce.js +12 -0
  106. package/src/loaders/configure/nonce.npm.js +1 -0
  107. package/src/loaders/micro-agent.js +5 -4
package/CHANGELOG.md CHANGED
@@ -3,6 +3,30 @@
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.248.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.247.0...v1.248.0) (2023-11-16)
7
+
8
+
9
+ ### Features
10
+
11
+ * Report enduser.id with Session Replay ([#815](https://github.com/newrelic/newrelic-browser-agent/issues/815)) ([8f5446d](https://github.com/newrelic/newrelic-browser-agent/commit/8f5446d1f7679f6a5ea0ba90eb082d1d4deb0d93))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * Fix issue with errors forcefully triggering session traces ([#819](https://github.com/newrelic/newrelic-browser-agent/issues/819)) ([3872c35](https://github.com/newrelic/newrelic-browser-agent/commit/3872c35a173f76644b663df5ca0474971451b7cf))
17
+
18
+ ## [1.247.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.246.1...v1.247.0) (2023-11-14)
19
+
20
+
21
+ ### Features
22
+
23
+ * Add basic support for deferring the browser agent loader script ([#800](https://github.com/newrelic/newrelic-browser-agent/issues/800)) ([92d864c](https://github.com/newrelic/newrelic-browser-agent/commit/92d864cb12a3076fd9b623fcd411d2dc9190110c))
24
+ * Add relative timestamps to Session Replay payloads ([#810](https://github.com/newrelic/newrelic-browser-agent/issues/810)) ([e4d1c70](https://github.com/newrelic/newrelic-browser-agent/commit/e4d1c701228e011d7c6f9d84cdc107044c69ce79))
25
+ * Add session durationMs to Session Replay payloads ([#792](https://github.com/newrelic/newrelic-browser-agent/issues/792)) ([3dfc4d4](https://github.com/newrelic/newrelic-browser-agent/commit/3dfc4d43fa978eeec47ebf432f8741562d0dd864))
26
+ * Enable SRI and nonce attributes for async chunks ([#805](https://github.com/newrelic/newrelic-browser-agent/issues/805)) ([fd9c3f3](https://github.com/newrelic/newrelic-browser-agent/commit/fd9c3f388f17353796ac2ebf18814353ca819dcf))
27
+ * Expose library versions used to capture session replay data ([#809](https://github.com/newrelic/newrelic-browser-agent/issues/809)) ([bc275ee](https://github.com/newrelic/newrelic-browser-agent/commit/bc275ee20242a5208358a0a77ac75e2b7cbd11c4))
28
+ * Session Replay API ([#803](https://github.com/newrelic/newrelic-browser-agent/issues/803)) ([12eb453](https://github.com/newrelic/newrelic-browser-agent/commit/12eb4530cfb5eb1e0a94d858485be0df40582c21))
29
+
6
30
  ## [1.246.1](https://github.com/newrelic/newrelic-browser-agent/compare/v1.246.0...v1.246.1) (2023-10-31)
7
31
 
8
32
 
package/README.md CHANGED
@@ -39,7 +39,7 @@ Before instrumenting your app using the NPM package, a Browser App should be con
39
39
 
40
40
  ## Instantiating the agent
41
41
 
42
- For best results, import and instantiate the `BrowserAgent` class as close to the top of the `head` element of your app's HTML output as possible. The specific location and method will vary based on your application's architecture or framework.
42
+ For best results, import and instantiate the `BrowserAgent` class as close to the top of the `head` element of your app's HTML output as possible. The specific location and method will vary based on your application's architecture or framework. See [Library Support](#library-support) for more information.
43
43
 
44
44
  Populate the `options` parameter using configuration values found in the the *Copy/Paste JavaScript* box in your browser app's *Application settings* page in New Relic One.
45
45
 
@@ -229,6 +229,14 @@ import { Agent } from '@newrelic/browser-agent/loaders/agent'
229
229
  import { Metrics } from '@newrelic/browser-agent/src/features/metrics'
230
230
  ```
231
231
 
232
+ ## Library Support
233
+
234
+ The browser agent is written to be agnostic to any JavaScript library or framework. The agent exposes a number of [API methods](https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/using-browser-apis/) that can be incorporated into libraries and frameworks. For example, export or make available the initialized agent and create a new error boundary in your react application that calls `browserAgent.noticeError()`.
235
+
236
+ ### Server-Side Rendering
237
+
238
+ A lot of new frameworks support the concept of server-side rendering the pages of an application to improve SEO and the performance of the user experience. The browser agent must be imported and instantiated within a browser context and will not work if executed on a server or in the server context of a server-side rendered application. These frameworks typically provide support for defining code that should only be run on the client. Check your frameworks documentation for more information.
239
+
232
240
  ## Disclaimers
233
241
 
234
242
  The session replay library shipping with this version of the browser agent is in *limited preview* and is not turned on by default. To use the feature, users will need to be part of the limited preview customer group and configure their browser application settings in the UI.
@@ -259,6 +267,6 @@ To all contributors, we thank you! Without your contribution, this project would
259
267
 
260
268
  ## License
261
269
 
262
- The Browser agent is licensed under the [Apache 2.0](http://apache.org/licenses/LICENSE-2.0.txt) License.
270
+ The Browser agent is licensed under the [Apache 2.0](https://apache.org/licenses/LICENSE-2.0.txt) License.
263
271
 
264
272
  The Browser agent also uses source code from third-party libraries. Full details on which libraries are used and the terms under which they are licensed can be found in the [third-party notices document](THIRD_PARTY_NOTICES.md).
@@ -46,5 +46,6 @@ function getInfo(id) {
46
46
  function setInfo(id, obj) {
47
47
  if (!id) throw new Error('All info objects require an agent identifier!');
48
48
  _cache[id] = (0, _configurable.getModeledObject)(obj, model);
49
- (0, _nreum.gosNREUMInitializedAgents)(id, _cache[id], 'info');
49
+ const agentInst = (0, _nreum.getNREUMInitializedAgent)(id);
50
+ if (agentInst) agentInst.info = _cache[id];
50
51
  }
@@ -173,7 +173,8 @@ function getConfiguration(id) {
173
173
  function setConfiguration(id, obj) {
174
174
  if (!id) throw new Error(missingAgentIdError);
175
175
  _cache[id] = (0, _configurable.getModeledObject)(obj, model());
176
- (0, _nreum.gosNREUMInitializedAgents)(id, _cache[id], 'config');
176
+ const agentInst = (0, _nreum.getNREUMInitializedAgent)(id);
177
+ if (agentInst) agentInst.init = _cache[id];
177
178
  }
178
179
  function getConfigurationValue(id, path) {
179
180
  if (!id) throw new Error(missingAgentIdError);
@@ -24,5 +24,6 @@ function getLoaderConfig(id) {
24
24
  function setLoaderConfig(id, obj) {
25
25
  if (!id) throw new Error('All loader-config objects require an agent identifier!');
26
26
  _cache[id] = (0, _configurable.getModeledObject)(obj, model);
27
- (0, _nreum.gosNREUMInitializedAgents)(id, _cache[id], 'loader_config');
27
+ const agentInst = (0, _nreum.getNREUMInitializedAgent)(id);
28
+ if (agentInst) agentInst.loader_config = _cache[id];
28
29
  }
@@ -38,5 +38,6 @@ function getRuntime(id) {
38
38
  function setRuntime(id, obj) {
39
39
  if (!id) throw new Error('All runtime objects require an agent identifier!');
40
40
  _cache[id] = (0, _configurable.getModeledObject)(obj, model);
41
- (0, _nreum.gosNREUMInitializedAgents)(id, _cache[id], 'runtime');
41
+ const agentInst = (0, _nreum.getNREUMInitializedAgent)(id);
42
+ if (agentInst) agentInst.runtime = _cache[id];
42
43
  }
@@ -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 = "1.246.1";
15
+ const VERSION = "1.248.0";
16
16
 
17
17
  /**
18
18
  * Exposes the build type of the agent
@@ -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 = "1.246.1";
15
+ const VERSION = "1.248.0";
16
16
 
17
17
  /**
18
18
  * Exposes the build type of the agent
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.supportsSendBeacon = exports.offset = exports.isiOS = exports.isWorkerScope = exports.isIE = exports.isBrowserScope = exports.initiallyHidden = exports.initialLocation = exports.iOSBelow16 = exports.globalScope = exports.ffVersion = void 0;
6
+ exports.supportsSendBeacon = exports.offset = exports.loadedAsDeferredBrowserScript = exports.isiOS = exports.isWorkerScope = exports.isIE = exports.isBrowserScope = exports.initiallyHidden = exports.initialLocation = exports.iOSBelow16 = exports.globalScope = exports.ffVersion = void 0;
7
7
  /**
8
8
  * @file Contains constants about the environment the agent is running
9
9
  * within. These values are derived at the time the agent is first loaded.
@@ -24,6 +24,8 @@ const isWorkerScope = typeof WorkerGlobalScope !== 'undefined' && (typeof self !
24
24
  exports.isWorkerScope = isWorkerScope;
25
25
  const globalScope = isBrowserScope ? window : typeof WorkerGlobalScope !== 'undefined' && (typeof self !== 'undefined' && self instanceof WorkerGlobalScope && self || typeof globalThis !== 'undefined' && globalThis instanceof WorkerGlobalScope && globalThis);
26
26
  exports.globalScope = globalScope;
27
+ const loadedAsDeferredBrowserScript = globalScope?.document?.readyState === 'complete';
28
+ exports.loadedAsDeferredBrowserScript = loadedAsDeferredBrowserScript;
27
29
  const initiallyHidden = Boolean(globalScope?.document?.visibilityState === 'hidden');
28
30
  exports.initiallyHidden = initiallyHidden;
29
31
  const initialLocation = '' + globalScope?.location;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.dispatchGlobalEvent = dispatchGlobalEvent;
7
+ var _runtime = require("../constants/runtime");
8
+ const GLOBAL_EVENT_NAMESPACE = 'newrelic';
9
+ function dispatchGlobalEvent() {
10
+ let detail = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
11
+ try {
12
+ _runtime.globalScope.dispatchEvent(new CustomEvent(GLOBAL_EVENT_NAMESPACE, {
13
+ detail
14
+ }));
15
+ } catch (err) {
16
+ // something happened... dispatchEvent or CustomEvent might not be supported
17
+ // decide what to do about it here
18
+ }
19
+ }
@@ -113,8 +113,8 @@ class SessionEntity {
113
113
  this.expiresTimer = new _timer.Timer({
114
114
  // When the inactive timer ends, collect a SM and reset the session
115
115
  onEnd: () => {
116
- this.collectSM('expired', this);
117
- this.collectSM('duration', this);
116
+ this.collectSM('expired');
117
+ this.collectSM('duration');
118
118
  this.reset();
119
119
  }
120
120
  }, this.state.expiresAt - Date.now());
@@ -130,8 +130,8 @@ class SessionEntity {
130
130
  this.inactiveTimer = new _interactionTimer.InteractionTimer({
131
131
  // When the inactive timer ends, collect a SM and reset the session
132
132
  onEnd: () => {
133
- this.collectSM('inactive', this);
134
- this.collectSM('duration', this);
133
+ this.collectSM('inactive');
134
+ this.collectSM('duration');
135
135
  this.reset();
136
136
  },
137
137
  // When the inactive timer refreshes, it will update the storage values with an update timestamp
@@ -181,14 +181,14 @@ class SessionEntity {
181
181
  if (this.isInvalid(obj)) return {};
182
182
  // if the session expires, collect a SM count before resetting
183
183
  if (this.isExpired(obj.expiresAt)) {
184
- this.collectSM('expired', this);
184
+ this.collectSM('expired');
185
185
  this.collectSM('duration', obj, true);
186
186
  return this.reset();
187
187
  }
188
188
  // if "inactive" timer is expired at "read" time -- esp. initial read -- reset
189
189
  // collect a SM count before resetting
190
190
  if (this.isExpired(obj.inactiveAt)) {
191
- this.collectSM('inactive', this);
191
+ this.collectSM('inactive');
192
192
  this.collectSM('duration', obj, true);
193
193
  return this.reset();
194
194
  }
@@ -279,15 +279,20 @@ class SessionEntity {
279
279
  collectSM(type, data, useUpdatedAt) {
280
280
  let value, tag;
281
281
  if (type === 'duration') {
282
- const startingTimestamp = data.expiresAt - data.expiresMs;
283
- const endingTimestamp = useUpdatedAt ? data.updatedAt : Date.now();
284
- value = endingTimestamp - startingTimestamp;
282
+ value = this.getDuration(data, useUpdatedAt);
285
283
  tag = 'Session/Duration/Ms';
286
284
  }
287
285
  if (type === 'expired') tag = 'Session/Expired/Seen';
288
286
  if (type === 'inactive') tag = 'Session/Inactive/Seen';
289
287
  if (tag) (0, _handle.handle)(_constants2.SUPPORTABILITY_METRIC_CHANNEL, [tag, value], undefined, _features.FEATURE_NAMES.metrics, this.ee);
290
288
  }
289
+ getDuration() {
290
+ let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.state;
291
+ let useUpdatedAt = arguments.length > 1 ? arguments[1] : undefined;
292
+ const startingTimestamp = data.expiresAt - this.expiresMs;
293
+ const endingTimestamp = !useUpdatedAt ? data.updatedAt : Date.now();
294
+ return endingTimestamp - startingTimestamp;
295
+ }
291
296
 
292
297
  /**
293
298
  * @param {number} futureMs - The number of ms to use to generate a future timestamp
@@ -8,6 +8,7 @@ exports.activatedFeatures = void 0;
8
8
  var _contextualEe = require("../event-emitter/contextual-ee");
9
9
  var _handle = require("../event-emitter/handle");
10
10
  var _features = require("../../loaders/features/features");
11
+ var _globalEvent = require("../dispatch/global-event");
11
12
  /*
12
13
  * Copyright 2020 New Relic Corporation. All rights reserved.
13
14
  * SPDX-License-Identifier: Apache-2.0
@@ -26,18 +27,17 @@ const sentIds = new Set();
26
27
  function activateFeatures(flags, agentIdentifier) {
27
28
  const sharedEE = _contextualEe.ee.get(agentIdentifier);
28
29
  if (!(flags && typeof flags === 'object')) return;
29
- if (!sentIds.has(agentIdentifier)) {
30
- Object.entries(flags).forEach(_ref => {
31
- let [flag, num] = _ref;
32
- if (bucketMap[flag]) {
33
- bucketMap[flag].forEach(feat => {
34
- if (!num) (0, _handle.handle)('block-' + flag, [], undefined, feat, sharedEE);else (0, _handle.handle)('feat-' + flag, [], undefined, feat, sharedEE);
35
- (0, _handle.handle)('rumresp-' + flag, [Boolean(num)], undefined, feat, sharedEE); // this is a duplicate of feat-/block- but makes awaiting for 1 event easier than 2
36
- });
37
- } else if (num) (0, _handle.handle)('feat-' + flag, [], undefined, undefined, sharedEE); // not sure what other flags are overlooked, but there's a test for ones not in the map --
38
- activatedFeatures[flag] = Boolean(num);
39
- });
40
- }
30
+ if (sentIds.has(agentIdentifier)) return;
31
+ Object.entries(flags).forEach(_ref => {
32
+ let [flag, num] = _ref;
33
+ if (bucketMap[flag]) {
34
+ bucketMap[flag].forEach(feat => {
35
+ if (!num) (0, _handle.handle)('block-' + flag, [], undefined, feat, sharedEE);else (0, _handle.handle)('feat-' + flag, [], undefined, feat, sharedEE);
36
+ (0, _handle.handle)('rumresp-' + flag, [Boolean(num)], undefined, feat, sharedEE); // this is a duplicate of feat-/block- but makes awaiting for 1 event easier than 2
37
+ });
38
+ } else if (num) (0, _handle.handle)('feat-' + flag, [], undefined, undefined, sharedEE); // not sure what other flags are overlooked, but there's a test for ones not in the map --
39
+ activatedFeatures[flag] = Boolean(num);
40
+ });
41
41
 
42
42
  // Let the features waiting on their respective flags know that RUM response was received and that any missing flags are interpreted as bad entitlement / "off".
43
43
  // Hence, those features will not be hanging forever if their flags aren't included in the response.
@@ -48,6 +48,11 @@ function activateFeatures(flags, agentIdentifier) {
48
48
  }
49
49
  });
50
50
  sentIds.add(agentIdentifier);
51
+
52
+ // let any window level subscribers know that the agent is running
53
+ (0, _globalEvent.dispatchGlobalEvent)({
54
+ loaded: true
55
+ });
51
56
  }
52
57
  const activatedFeatures = {};
53
58
  exports.activatedFeatures = activatedFeatures;
@@ -3,6 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
+ exports.checkState = checkState;
6
7
  exports.onDOMContentLoaded = onDOMContentLoaded;
7
8
  exports.onWindowLoad = onWindowLoad;
8
9
  var _eventListenerOpts = require("../event-listener/event-listener-opts");
@@ -6,13 +6,14 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.NREUMinitialized = NREUMinitialized;
7
7
  exports.addToNREUM = addToNREUM;
8
8
  exports.defaults = void 0;
9
+ exports.getNREUMInitializedAgent = getNREUMInitializedAgent;
9
10
  exports.gosCDN = gosCDN;
10
11
  exports.gosNREUM = gosNREUM;
11
12
  exports.gosNREUMInfo = gosNREUMInfo;
12
13
  exports.gosNREUMInit = gosNREUMInit;
13
- exports.gosNREUMInitializedAgents = gosNREUMInitializedAgents;
14
14
  exports.gosNREUMLoaderConfig = gosNREUMLoaderConfig;
15
15
  exports.gosNREUMOriginals = gosNREUMOriginals;
16
+ exports.setNREUMInitializedAgent = setNREUMInitializedAgent;
16
17
  var _now = require("../timing/now");
17
18
  var _runtime = require("../constants/runtime");
18
19
  const defaults = {
@@ -71,24 +72,24 @@ function gosNREUMOriginals() {
71
72
  }
72
73
  return nr;
73
74
  }
74
- function gosNREUMInitializedAgents(id, obj, target) {
75
+ function setNREUMInitializedAgent(id, newAgentInstance) {
75
76
  let nr = gosNREUM();
76
- const externallySupplied = nr.initializedAgents || {};
77
- const curr = externallySupplied[id] || {};
78
- if (!Object.keys(curr).length) {
79
- curr.initializedAt = {
80
- ms: (0, _now.now)(),
81
- date: new Date()
82
- };
83
- }
84
- nr.initializedAgents = {
85
- ...externallySupplied,
86
- [id]: {
87
- ...curr,
88
- [target]: obj
89
- }
77
+ nr.initializedAgents ??= {};
78
+ newAgentInstance.initializedAt = {
79
+ ms: (0, _now.now)(),
80
+ date: new Date()
90
81
  };
91
- return nr;
82
+ nr.initializedAgents[id] = newAgentInstance;
83
+ }
84
+
85
+ /**
86
+ * Get the agent instance under the associated identifier on the global newrelic object.
87
+ * @see setNREUMInitializedAgents
88
+ * @returns Existing agent instance under newrelic.initializedAgent[id], or undefined if it does not exist.
89
+ */
90
+ function getNREUMInitializedAgent(id) {
91
+ let nr = gosNREUM();
92
+ return nr.initializedAgents?.[id];
92
93
  }
93
94
  function addToNREUM(fnName, fn) {
94
95
  let nr = gosNREUM();
@@ -69,10 +69,12 @@ class Aggregate extends _aggregateBase.AggregateBase {
69
69
  } = (0, _config.getRuntime)(this.agentIdentifier);
70
70
  if (loaderType) this.storeSupportabilityMetrics("Generic/LoaderType/".concat(loaderType, "/Detected"));
71
71
  if (distMethod) this.storeSupportabilityMetrics("Generic/DistMethod/".concat(distMethod, "/Detected"));
72
-
73
- // frameworks on page
74
72
  if (_runtime.isBrowserScope) {
75
73
  this.storeSupportabilityMetrics('Generic/Runtime/Browser/Detected');
74
+ const nonce = document?.currentScript?.nonce;
75
+ if (nonce && nonce !== '') {
76
+ this.storeSupportabilityMetrics('Generic/Runtime/Nonce/Detected');
77
+ }
76
78
 
77
79
  // These SMs are used by the AppExp team
78
80
  (0, _load.onDOMContentLoaded)(() => {
@@ -19,6 +19,7 @@ var _constants2 = require("../../metrics/constants");
19
19
  var _handle = require("../../../common/event-emitter/handle");
20
20
  var _features = require("../../../loaders/features/features");
21
21
  var _env = require("../../../common/constants/env.npm");
22
+ var _now = require("../../../common/timing/now");
22
23
  function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
23
24
  function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } /*
24
25
  * Copyright 2023 New Relic Corporation. All rights reserved.
@@ -70,8 +71,10 @@ const MAX_PAYLOAD_SIZE = 1000000;
70
71
  /** Unloading caps around 64kb */
71
72
  exports.MAX_PAYLOAD_SIZE = MAX_PAYLOAD_SIZE;
72
73
  const IDEAL_PAYLOAD_SIZE = 64000;
73
- /** Interval between forcing new full snapshots -- 15 seconds in error mode (x2), 5 minutes in full mode */
74
+ /** Reserved room for query param attrs */
74
75
  exports.IDEAL_PAYLOAD_SIZE = IDEAL_PAYLOAD_SIZE;
76
+ const QUERY_PARAM_PADDING = 5000;
77
+ /** Interval between forcing new full snapshots -- 15 seconds in error mode (x2), 5 minutes in full mode */
75
78
  const CHECKOUT_MS = {
76
79
  [_sessionEntity.MODE.ERROR]: 15000,
77
80
  [_sessionEntity.MODE.FULL]: 300000,
@@ -120,6 +123,9 @@ class Aggregate extends _aggregateBase.AggregateBase {
120
123
 
121
124
  /** Hold on to the last meta node, so that it can be re-inserted if the meta and snapshot nodes are broken up due to harvesting */
122
125
  this.lastMeta = undefined;
126
+
127
+ /** set by BCS response */
128
+ this.entitled = false;
123
129
  const shouldSetup = (0, _config.getConfigurationValue)(agentIdentifier, 'privacy.cookies_enabled') === true && (0, _config.getConfigurationValue)(agentIdentifier, 'session_trace.enabled') === true;
124
130
 
125
131
  /** The method to stop recording. This defaults to a noop, but is overwritten once the recording library is imported and initialized */
@@ -157,6 +163,18 @@ class Aggregate extends _aggregateBase.AggregateBase {
157
163
  getPayload: this.prepareHarvest.bind(this),
158
164
  raw: true
159
165
  }, this);
166
+ (0, _registerHandler.registerHandler)('recordReplay', () => {
167
+ // if it has aborted or BCS returned bad entitlements, do not allow
168
+ if (this.blocked || !this.entitled) return;
169
+ // if it isnt already (fully) initialized... initialize it
170
+ if (!recorder) this.initializeRecording(false, true, true);
171
+ // its been initialized and imported the recorder but its not recording (mode === off || error)
172
+ else if (this.mode !== _sessionEntity.MODE.FULL) this.switchToFull();
173
+ // if it gets all the way to here, that means a full session is already recording... do nothing
174
+ }, this.featureName, this.ee);
175
+ (0, _registerHandler.registerHandler)('pauseReplay', () => {
176
+ this.forceStop(this.mode !== _sessionEntity.MODE.ERROR);
177
+ }, this.featureName, this.ee);
160
178
 
161
179
  // Wait for an error to be reported. This currently is wrapped around the "Error" feature. This is a feature-feature dependency.
162
180
  // This was to ensure that all errors, including those on the page before load and those handled with "noticeError" are accounted for. Needs evalulation
@@ -165,37 +183,42 @@ class Aggregate extends _aggregateBase.AggregateBase {
165
183
  this.errorNoticed = true;
166
184
  // run once
167
185
  if (this.mode === _sessionEntity.MODE.ERROR && _runtime.globalScope?.document.visibilityState === 'visible') {
168
- this.mode = _sessionEntity.MODE.FULL;
169
- // if the error was noticed AFTER the recorder was already imported....
170
- if (recorder && this.initialized) {
171
- this.stopRecording();
172
- this.startRecording();
173
- this.scheduler.startTimer(this.harvestTimeSeconds);
174
- this.syncWithSessionManager({
175
- sessionReplayMode: this.mode
176
- });
177
- }
186
+ this.switchToFull();
178
187
  }
179
188
  }, this.featureName, this.ee);
180
189
  this.waitForFlags(['sr']).then(_ref => {
181
190
  let [flagOn] = _ref;
182
- return this.initializeRecording(flagOn, Math.random() * 100 < (0, _config.getConfigurationValue)(this.agentIdentifier, 'session_replay.error_sampling_rate'), Math.random() * 100 < (0, _config.getConfigurationValue)(this.agentIdentifier, 'session_replay.sampling_rate'));
191
+ this.entitled = flagOn;
192
+ this.initializeRecording(Math.random() * 100 < (0, _config.getConfigurationValue)(this.agentIdentifier, 'session_replay.error_sampling_rate'), Math.random() * 100 < (0, _config.getConfigurationValue)(this.agentIdentifier, 'session_replay.sampling_rate'));
183
193
  }).then(() => _sharedChannel.sharedChannel.onReplayReady(this.mode)); // notify watchers that replay started with the mode
184
194
 
185
195
  this.drain();
186
196
  }
187
197
  }
198
+ switchToFull() {
199
+ this.mode = _sessionEntity.MODE.FULL;
200
+ // if the error was noticed AFTER the recorder was already imported....
201
+ if (recorder && this.initialized) {
202
+ this.stopRecording();
203
+ this.startRecording();
204
+ this.scheduler.startTimer(this.harvestTimeSeconds);
205
+ this.syncWithSessionManager({
206
+ sessionReplayMode: this.mode
207
+ });
208
+ }
209
+ }
188
210
 
189
211
  /**
190
212
  * Evaluate entitlements and sampling before starting feature mechanics, importing and configuring recording library, and setting storage state
191
213
  * @param {boolean} entitlements - the true/false state of the "sr" flag from RUM response
192
214
  * @param {boolean} errorSample - the true/false state of the error sampling decision
193
215
  * @param {boolean} fullSample - the true/false state of the full sampling decision
216
+ * @param {boolean} ignoreSession - whether to force the method to ignore the session state and use just the sample flags
194
217
  * @returns {void}
195
218
  */
196
- async initializeRecording(entitlements, errorSample, fullSample) {
219
+ async initializeRecording(errorSample, fullSample, ignoreSession) {
197
220
  this.initialized = true;
198
- if (!entitlements) return;
221
+ if (!this.entitled || this.recording) return;
199
222
  const {
200
223
  session
201
224
  } = (0, _config.getRuntime)(this.agentIdentifier);
@@ -205,7 +228,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
205
228
  // we are not actively recording SR... DO NOT import or run the recording library
206
229
  // session replay samples can only be decided on the first load of a session
207
230
  // session replays can continue if already in progress
208
- if (!session.isNew) {
231
+ if (!session.isNew && !ignoreSession) {
209
232
  // inherit the mode of the existing session
210
233
  this.mode = session.state.sessionReplayMode;
211
234
  } else {
@@ -277,6 +300,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
277
300
  getHarvestContents() {
278
301
  const agentRuntime = (0, _config.getRuntime)(this.agentIdentifier);
279
302
  const info = (0, _config.getInfo)(this.agentIdentifier);
303
+ const endUserId = info.jsAttributes?.['enduser.id'];
280
304
  if (this.backloggedEvents.length) this.events = [...this.backloggedEvents, ...this.events];
281
305
 
282
306
  // do not let the first node be a full snapshot node, since this NEEDS to be preceded by a meta node
@@ -296,10 +320,12 @@ class Aggregate extends _aggregateBase.AggregateBase {
296
320
  this.events = this.events.slice(0, this.events.length - 1);
297
321
  this.hasMeta = !!this.events.find(x => x.type === RRWEB_EVENT_TYPES.Meta);
298
322
  }
323
+ const agentOffset = (0, _config.getRuntime)(this.agentIdentifier).offset;
324
+ const relativeNow = (0, _now.now)();
299
325
  const firstEventTimestamp = this.events[0]?.timestamp; // from rrweb node
300
326
  const lastEventTimestamp = this.events[this.events.length - 1]?.timestamp; // from rrweb node
301
327
  const firstTimestamp = firstEventTimestamp || this.cycleTimestamp;
302
- const lastTimestamp = lastEventTimestamp || (0, _config.getRuntime)(this.agentIdentifier).offset + _runtime.globalScope.performance.now();
328
+ const lastTimestamp = lastEventTimestamp || agentOffset + relativeNow;
303
329
  return {
304
330
  qs: {
305
331
  browser_monitoring_key: info.licenseKey,
@@ -307,22 +333,32 @@ class Aggregate extends _aggregateBase.AggregateBase {
307
333
  app_id: info.applicationID,
308
334
  protocol_version: '0',
309
335
  attributes: (0, _encode.obj)({
336
+ // this section of attributes must be controllable and stay below the query param padding limit -- see QUERY_PARAM_PADDING
337
+ // if not, data could be lost to truncation at time of sending, potentially breaking parsing / API behavior in NR1
310
338
  ...(this.shouldCompress && {
311
339
  content_encoding: 'gzip'
312
340
  }),
313
341
  'replay.firstTimestamp': firstTimestamp,
342
+ 'replay.firstTimestampOffset': firstTimestamp - agentOffset,
314
343
  'replay.lastTimestamp': lastTimestamp,
315
344
  'replay.durationMs': lastTimestamp - firstTimestamp,
316
345
  'replay.nodes': this.events.length,
346
+ 'session.durationMs': agentRuntime.session.getDuration(),
317
347
  agentVersion: agentRuntime.version,
318
348
  session: agentRuntime.session.state.value,
349
+ rst: relativeNow,
319
350
  hasMeta: this.hasMeta,
320
351
  hasSnapshot: this.hasSnapshot,
321
352
  hasError: this.hasError,
322
353
  isFirstChunk: agentRuntime.session.state.sessionReplaySentFirstChunk === false,
323
354
  decompressedBytes: this.payloadBytesEstimation,
324
- 'nr.rrweb.version': _env.RRWEB_VERSION
325
- }, MAX_PAYLOAD_SIZE - this.payloadBytesEstimation).substring(1) // remove the leading '&'
355
+ 'rrweb.version': _env.RRWEB_VERSION,
356
+ // customer-defined data should go last so that if it exceeds the query param padding limit it will be truncated instead of important attrs
357
+ ...(endUserId && {
358
+ 'enduser.id': endUserId
359
+ })
360
+ // The Query Param is being arbitrarily limited in length here. It is also applied when estimating the size of the payload in getPayloadSize()
361
+ }, QUERY_PARAM_PADDING).substring(1) // remove the leading '&'
326
362
  },
327
363
 
328
364
  body: this.events
@@ -444,8 +480,22 @@ class Aggregate extends _aggregateBase.AggregateBase {
444
480
  /** Estimate the payload size */
445
481
  getPayloadSize() {
446
482
  let newBytes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
447
- // the 1KB gives us some padding for the other metadata
448
- return this.estimateCompression(this.payloadBytesEstimation + newBytes) + 1000;
483
+ // the query param padding constant gives us some padding for the other metadata to be safely injected
484
+ return this.estimateCompression(this.payloadBytesEstimation + newBytes) + QUERY_PARAM_PADDING;
485
+ }
486
+
487
+ /**
488
+ * Forces the agent into OFF mode so that changing tabs or navigating
489
+ * does not restart the recording. This is used when the customer calls
490
+ * the stopRecording API.
491
+ */
492
+ forceStop(forceHarvest) {
493
+ if (forceHarvest) this.scheduler.runHarvest();
494
+ this.mode = _sessionEntity.MODE.OFF;
495
+ this.stopRecording();
496
+ this.syncWithSessionManager({
497
+ sessionReplayMode: this.mode
498
+ });
449
499
  }
450
500
 
451
501
  /** Abort the feature, once aborted it will not resume */
@@ -89,30 +89,45 @@ class Aggregate extends _aggregateBase.AggregateBase {
89
89
  break;
90
90
  }
91
91
  };
92
+ let seenAnError = false;
93
+ let mostRecentModeKnown;
94
+ this.ee.on(_sessionEntity.SESSION_EVENTS.UPDATE, (eventType, sessionState) => {
95
+ // this will only have an effect if ST is NOT already in full mode
96
+ if (sessionState.sessionReplayMode === _sessionEntity.MODE.FULL) switchToFull();
97
+ });
98
+
99
+ /**
100
+ * The goal of switchToFull is to take external input to trigger a change from off or error to full.
101
+ * It will have no effect if already running in full mode.
102
+ * "external" input in this case means errors thrown on the page or session replay itself being triggered to run in full mode by the API, which updates the session entity.
103
+ */
104
+ const switchToFull = () => {
105
+ if (this.agentRuntime?.session?.state?.sessionReplayMode !== _sessionEntity.MODE.FULL) return;
106
+ if (mostRecentModeKnown !== _sessionEntity.MODE.FULL) {
107
+ const prevMode = mostRecentModeKnown;
108
+ mostRecentModeKnown = _sessionEntity.MODE.FULL;
109
+ sessionEntity.write({
110
+ sessionTraceMode: mostRecentModeKnown
111
+ });
112
+ this.isStandalone = false;
113
+ if (prevMode === _sessionEntity.MODE.ERROR && this.#scheduler) {
114
+ this.trimSTNs(ERROR_MODE_SECONDS_WINDOW); // up until now, Trace would've been just buffering nodes up to max, which needs to be trimmed to last X seconds
115
+ this.#scheduler.runHarvest({
116
+ needResponse: true
117
+ });
118
+ } else {
119
+ controlTraceOp(_sessionEntity.MODE.FULL);
120
+ }
121
+ }
122
+ };
92
123
  if (!sessionEntity) {
93
124
  // Since session manager isn't around, do the old Trace behavior of waiting for RUM response to decide feature activation.
94
125
  this.isStandalone = true;
95
126
  (0, _registerHandler.registerHandler)('rumresp-stn', on => controlTraceOp(on), this.featureName, this.ee);
96
127
  } else {
97
- let seenAnError = false;
98
- let mostRecentModeKnown;
99
128
  (0, _registerHandler.registerHandler)('errorAgg', () => {
100
- // Switch to full capture mode on next harvest on first exception thrown only. Only done once so that sessionTraceMode isn't constantly overwritten after decision block.
101
- if (!seenAnError) {
102
- seenAnError = true;
103
- /* If this cb executes before Trace has started, then no further action needed. But if...
104
- - startTracing already ran under ERROR mode, then it will NOT have kicked off the harvest-scheduler so that needs to be done & switch mode.
105
- - startTracing never ran because mode is OFF or Replay aborted or Traced turned off elsewhere OR trace already in FULL, then this should do nothing. */
106
- if (sessionEntity.state.sessionTraceMode === _sessionEntity.MODE.ERROR && this.#scheduler) {
107
- sessionEntity.write({
108
- sessionTraceMode: mostRecentModeKnown = _sessionEntity.MODE.FULL
109
- });
110
- this.trimSTNs(ERROR_MODE_SECONDS_WINDOW); // up until now, Trace would've been just buffering nodes up to max, which needs to be trimmed to last X seconds
111
- this.#scheduler.runHarvest({
112
- needResponse: true
113
- });
114
- }
115
- }
129
+ seenAnError = true;
130
+ switchToFull();
116
131
  }, this.featureName, this.ee);
117
132
  const stopTracePerm = () => {
118
133
  if (sessionEntity.state.sessionTraceMode !== _sessionEntity.MODE.OFF) sessionEntity.write({