@newrelic/browser-agent 1.260.0 → 1.261.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 (167) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/cjs/cdn/experimental.js +2 -1
  3. package/dist/cjs/cdn/polyfills/pro.js +2 -1
  4. package/dist/cjs/cdn/polyfills/spa.js +2 -1
  5. package/dist/cjs/cdn/pro.js +2 -1
  6. package/dist/cjs/cdn/spa.js +2 -1
  7. package/dist/cjs/common/config/state/init.js +31 -24
  8. package/dist/cjs/common/constants/env.cdn.js +1 -1
  9. package/dist/cjs/common/constants/env.js +1 -1
  10. package/dist/cjs/common/constants/env.npm.js +1 -1
  11. package/dist/cjs/common/constants/runtime.js +2 -1
  12. package/dist/cjs/common/deny-list/deny-list.js +1 -1
  13. package/dist/cjs/common/harvest/harvest-scheduler.js +1 -1
  14. package/dist/cjs/common/harvest/harvest.js +1 -1
  15. package/dist/cjs/common/session/session-entity.js +7 -1
  16. package/dist/cjs/common/timing/time-keeper.js +2 -2
  17. package/dist/cjs/common/wrap/wrap-logger.js +54 -0
  18. package/dist/cjs/features/ajax/aggregate/index.js +1 -1
  19. package/dist/cjs/features/logging/aggregate/index.js +102 -0
  20. package/dist/cjs/features/logging/constants.js +20 -0
  21. package/dist/cjs/features/logging/index.js +12 -0
  22. package/dist/cjs/features/logging/instrument/index.js +28 -0
  23. package/dist/cjs/features/logging/shared/log.js +39 -0
  24. package/dist/cjs/features/logging/shared/utils.js +50 -0
  25. package/dist/cjs/features/page_view_event/aggregate/index.js +1 -1
  26. package/dist/cjs/features/page_view_event/instrument/index.js +1 -1
  27. package/dist/cjs/features/page_view_timing/aggregate/index.js +1 -2
  28. package/dist/cjs/features/session_replay/aggregate/index.js +4 -3
  29. package/dist/cjs/features/session_replay/instrument/index.js +1 -1
  30. package/dist/cjs/features/session_trace/aggregate/index.js +16 -8
  31. package/dist/cjs/features/session_trace/aggregate/trace/storage.js +1 -1
  32. package/dist/cjs/features/session_trace/instrument/index.js +1 -1
  33. package/dist/cjs/features/soft_navigations/aggregate/bel-node.js +1 -2
  34. package/dist/cjs/features/soft_navigations/aggregate/index.js +1 -1
  35. package/dist/cjs/features/soft_navigations/aggregate/interaction.js +5 -4
  36. package/dist/cjs/features/spa/aggregate/index.js +2 -2
  37. package/dist/cjs/features/spa/instrument/index.js +1 -1
  38. package/dist/cjs/features/utils/instrument-base.js +1 -1
  39. package/dist/cjs/features/utils/lazy-feature-loader.js +3 -1
  40. package/dist/cjs/loaders/agent-base.js +23 -2
  41. package/dist/cjs/loaders/api/api-methods.js +1 -1
  42. package/dist/cjs/loaders/api/api.js +29 -2
  43. package/dist/cjs/loaders/features/features.js +7 -5
  44. package/dist/cjs/loaders/micro-agent.js +1 -1
  45. package/dist/esm/cdn/experimental.js +2 -1
  46. package/dist/esm/cdn/polyfills/pro.js +2 -1
  47. package/dist/esm/cdn/polyfills/spa.js +2 -1
  48. package/dist/esm/cdn/pro.js +2 -1
  49. package/dist/esm/cdn/spa.js +2 -1
  50. package/dist/esm/common/config/state/init.js +30 -23
  51. package/dist/esm/common/constants/env.cdn.js +1 -1
  52. package/dist/esm/common/constants/env.npm.js +1 -1
  53. package/dist/esm/common/constants/runtime.js +3 -1
  54. package/dist/esm/common/deny-list/deny-list.js +1 -1
  55. package/dist/esm/common/session/session-entity.js +8 -2
  56. package/dist/esm/common/timing/time-keeper.js +2 -2
  57. package/dist/esm/common/wrap/wrap-logger.js +48 -0
  58. package/dist/esm/features/logging/aggregate/index.js +95 -0
  59. package/dist/esm/features/logging/constants.js +14 -0
  60. package/dist/esm/features/logging/index.js +1 -0
  61. package/dist/esm/features/logging/instrument/index.js +21 -0
  62. package/dist/esm/features/logging/shared/log.js +32 -0
  63. package/dist/esm/features/logging/shared/utils.js +44 -0
  64. package/dist/esm/features/page_view_timing/aggregate/index.js +1 -2
  65. package/dist/esm/features/session_replay/aggregate/index.js +3 -2
  66. package/dist/esm/features/session_trace/aggregate/index.js +16 -8
  67. package/dist/esm/features/session_trace/aggregate/trace/storage.js +1 -1
  68. package/dist/esm/features/soft_navigations/aggregate/bel-node.js +1 -2
  69. package/dist/esm/features/soft_navigations/aggregate/index.js +1 -1
  70. package/dist/esm/features/soft_navigations/aggregate/interaction.js +5 -4
  71. package/dist/esm/features/spa/aggregate/index.js +1 -1
  72. package/dist/esm/features/utils/lazy-feature-loader.js +2 -0
  73. package/dist/esm/loaders/agent-base.js +23 -2
  74. package/dist/esm/loaders/api/api-methods.js +1 -1
  75. package/dist/esm/loaders/api/api.js +28 -1
  76. package/dist/esm/loaders/features/features.js +7 -5
  77. package/dist/types/common/config/state/init.d.ts.map +1 -1
  78. package/dist/types/common/constants/runtime.d.ts +0 -6
  79. package/dist/types/common/constants/runtime.d.ts.map +1 -1
  80. package/dist/types/common/drain/drain.d.ts.map +1 -1
  81. package/dist/types/common/harvest/harvest-scheduler.d.ts.map +1 -1
  82. package/dist/types/common/harvest/harvest.d.ts +5 -5
  83. package/dist/types/common/harvest/types.d.ts +2 -2
  84. package/dist/types/common/harvest/types.d.ts.map +1 -1
  85. package/dist/types/common/ids/id.d.ts.map +1 -1
  86. package/dist/types/common/ids/unique-id.d.ts.map +1 -1
  87. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  88. package/dist/types/common/util/console.d.ts.map +1 -1
  89. package/dist/types/common/util/data-size.d.ts.map +1 -1
  90. package/dist/types/common/util/feature-flags.d.ts.map +1 -1
  91. package/dist/types/common/util/get-or-set.d.ts.map +1 -1
  92. package/dist/types/common/util/invoke.d.ts.map +1 -1
  93. package/dist/types/common/util/stringify.d.ts.map +1 -1
  94. package/dist/types/common/util/submit-data.d.ts.map +1 -1
  95. package/dist/types/common/util/type-check.d.ts.map +1 -1
  96. package/dist/types/common/wrap/wrap-logger.d.ts +17 -0
  97. package/dist/types/common/wrap/wrap-logger.d.ts.map +1 -0
  98. package/dist/types/features/jserrors/aggregate/compute-stack-trace.d.ts.map +1 -1
  99. package/dist/types/features/jserrors/aggregate/index.d.ts +1 -1
  100. package/dist/types/features/logging/aggregate/index.d.ts +40 -0
  101. package/dist/types/features/logging/aggregate/index.d.ts.map +1 -0
  102. package/dist/types/features/logging/constants.d.ts +14 -0
  103. package/dist/types/features/logging/constants.d.ts.map +1 -0
  104. package/dist/types/features/logging/index.d.ts +2 -0
  105. package/dist/types/features/logging/index.d.ts.map +1 -0
  106. package/dist/types/features/logging/instrument/index.d.ts +6 -0
  107. package/dist/types/features/logging/instrument/index.d.ts.map +1 -0
  108. package/dist/types/features/logging/shared/log.d.ts +18 -0
  109. package/dist/types/features/logging/shared/log.d.ts.map +1 -0
  110. package/dist/types/features/logging/shared/utils.d.ts +16 -0
  111. package/dist/types/features/logging/shared/utils.d.ts.map +1 -0
  112. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  113. package/dist/types/features/session_replay/aggregate/index.d.ts +1 -1
  114. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  115. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  116. package/dist/types/features/session_trace/aggregate/index.d.ts +9 -6
  117. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  118. package/dist/types/features/session_trace/aggregate/trace/storage.d.ts.map +1 -1
  119. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +0 -1
  120. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
  121. package/dist/types/features/soft_navigations/aggregate/bel-node.d.ts +1 -1
  122. package/dist/types/features/soft_navigations/aggregate/bel-node.d.ts.map +1 -1
  123. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts +0 -1
  124. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
  125. package/dist/types/features/utils/feature-base.d.ts +1 -1
  126. package/dist/types/features/utils/feature-base.d.ts.map +1 -1
  127. package/dist/types/features/utils/instrument-base.d.ts +2 -2
  128. package/dist/types/features/utils/lazy-feature-loader.d.ts.map +1 -1
  129. package/dist/types/loaders/agent-base.d.ts +25 -4
  130. package/dist/types/loaders/agent-base.d.ts.map +1 -1
  131. package/dist/types/loaders/api/api.d.ts +8 -0
  132. package/dist/types/loaders/api/api.d.ts.map +1 -1
  133. package/dist/types/loaders/api/interaction-types.d.ts.map +1 -1
  134. package/dist/types/loaders/features/features.d.ts +1 -0
  135. package/dist/types/loaders/features/features.d.ts.map +1 -1
  136. package/dist/types/loaders/micro-agent.d.ts.map +1 -1
  137. package/package.json +16 -28
  138. package/src/cdn/experimental.js +2 -0
  139. package/src/cdn/polyfills/pro.js +3 -1
  140. package/src/cdn/polyfills/spa.js +2 -0
  141. package/src/cdn/pro.js +3 -1
  142. package/src/cdn/spa.js +2 -0
  143. package/src/common/config/state/init.js +17 -15
  144. package/src/common/constants/runtime.js +3 -1
  145. package/src/common/deny-list/deny-list.js +1 -1
  146. package/src/common/session/session-entity.js +7 -2
  147. package/src/common/timing/time-keeper.js +2 -2
  148. package/src/common/wrap/wrap-logger.js +49 -0
  149. package/src/features/logging/aggregate/index.js +101 -0
  150. package/src/features/logging/constants.js +19 -0
  151. package/src/features/logging/index.js +1 -0
  152. package/src/features/logging/instrument/index.js +18 -0
  153. package/src/features/logging/shared/log.js +28 -0
  154. package/src/features/logging/shared/utils.js +43 -0
  155. package/src/features/page_view_timing/aggregate/index.js +1 -2
  156. package/src/features/session_replay/aggregate/index.js +3 -3
  157. package/src/features/session_trace/aggregate/index.js +15 -8
  158. package/src/features/session_trace/aggregate/trace/storage.js +1 -2
  159. package/src/features/soft_navigations/aggregate/bel-node.js +1 -3
  160. package/src/features/soft_navigations/aggregate/index.js +1 -1
  161. package/src/features/soft_navigations/aggregate/interaction.js +5 -4
  162. package/src/features/spa/aggregate/index.js +1 -1
  163. package/src/features/utils/lazy-feature-loader.js +2 -0
  164. package/src/loaders/agent-base.js +23 -2
  165. package/src/loaders/api/api-methods.js +1 -1
  166. package/src/loaders/api/api.js +19 -1
  167. package/src/loaders/features/features.js +7 -5
@@ -1,3 +1,4 @@
1
+ import { LOG_LEVELS } from '../../../features/logging/constants';
1
2
  import { isValidSelector } from '../../dom/query-selector';
2
3
  import { DEFAULT_EXPIRES_MS, DEFAULT_INACTIVE_MS } from '../../session/constants';
3
4
  import { warn } from '../../util/console';
@@ -29,16 +30,6 @@ const model = () => {
29
30
  }
30
31
  };
31
32
  return {
32
- feature_flags: [],
33
- proxy: {
34
- assets: undefined,
35
- // if this value is set, it will be used to overwrite the webpack asset path used to fetch assets
36
- beacon: undefined // likewise for the url to which we send analytics
37
- },
38
- privacy: {
39
- cookies_enabled: true
40
- },
41
- // *cli - per discussion, default should be true
42
33
  ajax: {
43
34
  deny_list: undefined,
44
35
  block_internal: true,
@@ -53,21 +44,26 @@ const model = () => {
53
44
  cors_use_tracecontext_headers: undefined,
54
45
  allowed_origins: undefined
55
46
  },
56
- session: {
57
- expiresMs: DEFAULT_EXPIRES_MS,
58
- inactiveMs: DEFAULT_INACTIVE_MS
47
+ feature_flags: [],
48
+ harvest: {
49
+ tooManyRequestsDelay: 60
59
50
  },
60
- ssl: undefined,
61
- obfuscate: undefined,
62
51
  jserrors: {
63
52
  enabled: true,
64
53
  harvestTimeSeconds: 10,
65
54
  autoStart: true
66
55
  },
56
+ logging: {
57
+ enabled: true,
58
+ harvestTimeSeconds: 10,
59
+ autoStart: true,
60
+ level: LOG_LEVELS.INFO
61
+ },
67
62
  metrics: {
68
63
  enabled: true,
69
64
  autoStart: true
70
65
  },
66
+ obfuscate: undefined,
71
67
  page_action: {
72
68
  enabled: true,
73
69
  harvestTimeSeconds: 30,
@@ -83,13 +79,18 @@ const model = () => {
83
79
  long_task: false,
84
80
  autoStart: true
85
81
  },
86
- session_trace: {
87
- enabled: true,
88
- harvestTimeSeconds: 10,
89
- autoStart: true
82
+ privacy: {
83
+ cookies_enabled: true
90
84
  },
91
- harvest: {
92
- tooManyRequestsDelay: 60
85
+ // *cli - per discussion, default should be true
86
+ proxy: {
87
+ assets: undefined,
88
+ // if this value is set, it will be used to overwrite the webpack asset path used to fetch assets
89
+ beacon: undefined // likewise for the url to which we send analytics
90
+ },
91
+ session: {
92
+ expiresMs: DEFAULT_EXPIRES_MS,
93
+ inactiveMs: DEFAULT_INACTIVE_MS
93
94
  },
94
95
  session_replay: {
95
96
  // feature settings
@@ -146,7 +147,7 @@ const model = () => {
146
147
  };else warn('An invalid session_replay.mask_input_option was provided and will not be used', val);
147
148
  }
148
149
  },
149
- spa: {
150
+ session_trace: {
150
151
  enabled: true,
151
152
  harvestTimeSeconds: 10,
152
153
  autoStart: true
@@ -155,7 +156,13 @@ const model = () => {
155
156
  enabled: true,
156
157
  harvestTimeSeconds: 10,
157
158
  autoStart: true
158
- }
159
+ },
160
+ spa: {
161
+ enabled: true,
162
+ harvestTimeSeconds: 10,
163
+ autoStart: true
164
+ },
165
+ ssl: undefined
159
166
  };
160
167
  };
161
168
  const _cache = {};
@@ -6,7 +6,7 @@
6
6
  /**
7
7
  * Exposes the version of the agent
8
8
  */
9
- export const VERSION = "1.260.0";
9
+ export const VERSION = "1.261.0";
10
10
 
11
11
  /**
12
12
  * Exposes the build type of the agent
@@ -6,7 +6,7 @@
6
6
  /**
7
7
  * Exposes the version of the agent
8
8
  */
9
- export const VERSION = "1.260.0";
9
+ export const VERSION = "1.261.0";
10
10
 
11
11
  /**
12
12
  * Exposes the build type of the agent
@@ -5,6 +5,8 @@
5
5
  * @license Apache-2.0
6
6
  */
7
7
 
8
+ import { now } from '../timing/now';
9
+
8
10
  /**
9
11
  * Indicates if the agent is running within a normal browser window context.
10
12
  */
@@ -44,4 +46,4 @@ export const supportsSendBeacon = !!globalScope.navigator?.sendBeacon;
44
46
  * according to the browser's local clock.
45
47
  * @type {number}
46
48
  */
47
- export const originTime = Math.floor(Date.now() - performance.now());
49
+ export const originTime = Date.now() - now();
@@ -10,7 +10,7 @@ var denyList = [];
10
10
  * @returns {boolean} `true` if request does not match any entries of {@link denyList|deny list}; else `false`
11
11
  */
12
12
  export function shouldCollectEvent(params) {
13
- if (hasUndefinedHostname(params)) return false;
13
+ if (!params || hasUndefinedHostname(params)) return false;
14
14
  if (denyList.length === 0) return true;
15
15
  for (var i = 0; i < denyList.length; i++) {
16
16
  var parsed = denyList[i];
@@ -3,7 +3,7 @@ import { warn } from '../util/console';
3
3
  import { stringify } from '../util/stringify';
4
4
  import { ee } from '../event-emitter/contextual-ee';
5
5
  import { Timer } from '../timer/timer';
6
- import { isBrowserScope } from '../constants/runtime';
6
+ import { isBrowserScope, isIE } from '../constants/runtime';
7
7
  import { DEFAULT_EXPIRES_MS, DEFAULT_INACTIVE_MS, MODE, PREFIX, SESSION_EVENTS, SESSION_EVENT_TYPES } from './constants';
8
8
  import { InteractionTimer } from '../timer/interaction-timer';
9
9
  import { wrapEvents } from '../wrap';
@@ -53,7 +53,13 @@ export class SessionEntity {
53
53
  this.ee = ee.get(agentIdentifier);
54
54
  wrapEvents(this.ee);
55
55
  this.setup(opts);
56
- if (isBrowserScope) {
56
+
57
+ /**
58
+ * Do not emit session storage events for IE11, because IE11 is unable to determine
59
+ * if the event was spawned on the current page or an adjacent page, and the behavior tied
60
+ * to storage events is critical to apply only to cross-tab behavior
61
+ * */
62
+ if (isBrowserScope && !isIE) {
57
63
  windowAddEventListener('storage', event => {
58
64
  if (event.key === this.lookupKey) {
59
65
  const obj = typeof event.newValue === 'string' ? JSON.parse(event.newValue) : event.newValue;
@@ -57,7 +57,7 @@ export class TimeKeeper {
57
57
  throw new Error('Missing date header on rum response.');
58
58
  }
59
59
  const medianRumOffset = (endTime - startTime) / 2;
60
- const serverOffset = Math.floor(startTime + medianRumOffset);
60
+ const serverOffset = startTime + medianRumOffset;
61
61
 
62
62
  // Corrected page origin time
63
63
  this.#correctedOriginTime = Math.floor(Date.parse(responseDateHeader) - serverOffset);
@@ -78,7 +78,7 @@ export class TimeKeeper {
78
78
  * @returns {number} Corrected unix/epoch timestamp
79
79
  */
80
80
  convertRelativeTimestamp(relativeTime) {
81
- return this.#correctedOriginTime + relativeTime;
81
+ return Math.floor(this.#correctedOriginTime + relativeTime);
82
82
  }
83
83
 
84
84
  /**
@@ -0,0 +1,48 @@
1
+ /*
2
+ * Copyright 2020 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+ /**
6
+ * @file Wraps native timeout and interval methods for instrumentation.
7
+ * This module is used by: jserrors, spa.
8
+ */
9
+
10
+ import { ee as baseEE, contextId } from '../event-emitter/contextual-ee';
11
+ import { EventContext } from '../event-emitter/event-context';
12
+ import { createWrapperWithEmitter as wfn } from './wrap-function';
13
+
14
+ /**
15
+ * Wraps a supplied function and adds emitter events under the `-wrap-logger-` prefix
16
+ * @param {Object} sharedEE - The shared event emitter on which a new scoped event emitter will be based.
17
+ * @param {Object} parent - The parent object housing the logger function
18
+ * @param {string} loggerFn - The name of the function in the parent object to wrap
19
+ * @returns {Object} Scoped event emitter with a debug ID of `logger`.
20
+ */
21
+ // eslint-disable-next-line
22
+ export function wrapLogger(sharedEE, parent, loggerFn, context) {
23
+ const ee = scopedEE(sharedEE);
24
+ const wrapFn = wfn(ee);
25
+
26
+ /**
27
+ * This section contains the context that will be shared across all invoked calls of the wrapped function,
28
+ * which will be used to decorate the log data later at agg time
29
+ */
30
+ const ctx = new EventContext(contextId);
31
+ ctx.level = context.level;
32
+ ctx.customAttributes = context.customAttributes;
33
+
34
+ /** observe calls to <loggerFn> and emit events prefixed with `wrap-logger-` */
35
+ wrapFn.inPlace(parent, [loggerFn], 'wrap-logger-', ctx);
36
+ return ee;
37
+ }
38
+
39
+ /**
40
+ * Returns an event emitter scoped specifically for the `logger` context. This scoping is a remnant from when all the
41
+ * features shared the same group in the event, to isolate events between features. It will likely be revisited.
42
+ * @param {Object} sharedEE - Optional event emitter on which to base the scoped emitter.
43
+ * Uses `ee` on the global scope if undefined).
44
+ * @returns {Object} Scoped event emitter with a debug ID of 'logger'.
45
+ */
46
+ export function scopedEE(sharedEE) {
47
+ return (sharedEE || baseEE).get('logger');
48
+ }
@@ -0,0 +1,95 @@
1
+ import { getConfigurationValue, getInfo, getRuntime } from '../../../common/config/config';
2
+ import { handle } from '../../../common/event-emitter/handle';
3
+ import { registerHandler } from '../../../common/event-emitter/register-handler';
4
+ import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler';
5
+ import { warn } from '../../../common/util/console';
6
+ import { stringify } from '../../../common/util/stringify';
7
+ import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants';
8
+ import { AggregateBase } from '../../utils/aggregate-base';
9
+ import { FEATURE_NAME, LOGGING_EVENT_EMITTER_CHANNEL, LOGGING_IGNORED, MAX_PAYLOAD_SIZE } from '../constants';
10
+ import { Log } from '../shared/log';
11
+ export class Aggregate extends AggregateBase {
12
+ static featureName = FEATURE_NAME;
13
+ #agentRuntime;
14
+ #agentInfo;
15
+ constructor(agentIdentifier, aggregator) {
16
+ super(agentIdentifier, aggregator, FEATURE_NAME);
17
+
18
+ /** held logs before sending */
19
+ this.bufferedLogs = [];
20
+ /** held logs during sending, for retries */
21
+ this.outgoingLogs = [];
22
+ /** the estimated bytes of log data waiting to be sent -- triggers a harvest if adding a new log will exceed limit */
23
+ this.estimatedBytes = 0;
24
+ this.#agentRuntime = getRuntime(this.agentIdentifier);
25
+ this.#agentInfo = getInfo(this.agentIdentifier);
26
+ this.harvestTimeSeconds = getConfigurationValue(this.agentIdentifier, 'logging.harvestTimeSeconds');
27
+ this.waitForFlags([]).then(() => {
28
+ this.scheduler = new HarvestScheduler('browser/logs', {
29
+ onFinished: this.onHarvestFinished.bind(this),
30
+ retryDelay: this.harvestTimeSeconds,
31
+ getPayload: this.prepareHarvest.bind(this),
32
+ raw: true
33
+ }, this);
34
+ /** harvest immediately once started to purge pre-load logs collected */
35
+ this.scheduler.startTimer(this.harvestTimeSeconds, 0);
36
+ /** emitted by instrument class (wrapped loggers) or the api methods directly */
37
+ registerHandler(LOGGING_EVENT_EMITTER_CHANNEL, this.handleLog.bind(this), this.featureName, this.ee);
38
+ this.drain();
39
+ });
40
+ }
41
+ handleLog(timestamp, message, attributes, level) {
42
+ if (this.blocked) return;
43
+ const log = new Log(this.#agentRuntime.timeKeeper.convertRelativeTimestamp(timestamp), message, attributes, level);
44
+ const logBytes = log.message.length + stringify(log.attributes).length + log.level.length + 10; // timestamp == 10 chars
45
+ if (logBytes > MAX_PAYLOAD_SIZE) {
46
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ['Logging/Harvest/Failed/Seen', logBytes]);
47
+ return warn(LOGGING_IGNORED + '> ' + MAX_PAYLOAD_SIZE + ' bytes', log.message.slice(0, 25) + '...');
48
+ }
49
+ if (this.estimatedBytes + logBytes >= MAX_PAYLOAD_SIZE) {
50
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ['Logging/Harvest/Early/Seen', this.estimatedBytes + logBytes]);
51
+ this.scheduler.runHarvest({});
52
+ }
53
+ this.estimatedBytes += logBytes;
54
+ this.bufferedLogs.push(log);
55
+ }
56
+ prepareHarvest() {
57
+ if (this.blocked || !(this.bufferedLogs.length || this.outgoingLogs.length)) return;
58
+ /** populate outgoing array while also clearing main buffer */
59
+ this.outgoingLogs.push(...this.bufferedLogs.splice(0));
60
+ this.estimatedBytes = 0;
61
+ /** see https://source.datanerd.us/agents/rum-specs/blob/main/browser/Log for logging spec */
62
+ return {
63
+ qs: {
64
+ browser_monitoring_key: this.#agentInfo.licenseKey
65
+ },
66
+ body: [{
67
+ common: {
68
+ /** Attributes in the `common` section are added to `all` logs generated in the payload */
69
+ attributes: {
70
+ 'entity.guid': this.#agentRuntime.appMetadata?.agents?.[0]?.entityGuid,
71
+ // browser entity guid as provided from RUM response
72
+ session: this.#agentRuntime?.session?.state.value || '0',
73
+ // The session ID that we generate and keep across page loads
74
+ hasReplay: this.#agentRuntime?.session?.state.sessionReplayMode === 1,
75
+ // True if a session replay recording is running
76
+ hasTrace: this.#agentRuntime?.session?.state.sessionTraceMode === 1,
77
+ // True if a session trace recording is running
78
+ ptid: this.#agentRuntime.ptid,
79
+ // page trace id
80
+ appId: this.#agentInfo.applicationID,
81
+ // Application ID from info object,
82
+ standalone: Boolean(this.#agentInfo.sa),
83
+ // copy paste (true) vs APM (false)
84
+ agentVersion: this.#agentRuntime.version // browser agent version
85
+ }
86
+ },
87
+ /** logs section contains individual unique log entries */
88
+ logs: this.outgoingLogs
89
+ }]
90
+ };
91
+ }
92
+ onHarvestFinished(result) {
93
+ if (!result.retry) this.outgoingLogs = [];
94
+ }
95
+ }
@@ -0,0 +1,14 @@
1
+ import { FEATURE_NAMES } from '../../loaders/features/features';
2
+ export const LOG_LEVELS = {
3
+ ERROR: 'ERROR',
4
+ WARN: 'WARN',
5
+ INFO: 'INFO',
6
+ DEBUG: 'DEBUG',
7
+ TRACE: 'TRACE'
8
+ };
9
+ export const LOGGING_EVENT_EMITTER_CHANNEL = 'log';
10
+ export const FEATURE_NAME = FEATURE_NAMES.logging;
11
+ export const MAX_PAYLOAD_SIZE = 1000000;
12
+ export const LOGGING_FAILURE_MESSAGE = 'failed to wrap logger: ';
13
+ export const LOGGING_LEVEL_FAILURE_MESSAGE = 'invalid log level: ';
14
+ export const LOGGING_IGNORED = 'ignored log: ';
@@ -0,0 +1 @@
1
+ export { Instrument as Logging } from './instrument/index';
@@ -0,0 +1,21 @@
1
+ import { InstrumentBase } from '../../utils/instrument-base';
2
+ import { FEATURE_NAME } from '../constants';
3
+ import { bufferLog } from '../shared/utils';
4
+ export class Instrument extends InstrumentBase {
5
+ static featureName = FEATURE_NAME;
6
+ constructor(agentIdentifier, aggregator) {
7
+ let auto = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
8
+ super(agentIdentifier, aggregator, FEATURE_NAME, auto);
9
+ const instanceEE = this.ee;
10
+ /** emitted by wrap-logger function */
11
+ this.ee.on('wrap-logger-end', function handleLog(_ref) {
12
+ let [message] = _ref;
13
+ const {
14
+ level,
15
+ customAttributes
16
+ } = this;
17
+ bufferLog(instanceEE, message, customAttributes, level);
18
+ });
19
+ this.importAggregator();
20
+ }
21
+ }
@@ -0,0 +1,32 @@
1
+ import { globalScope } from '../../../common/constants/runtime';
2
+ import { cleanURL } from '../../../common/url/clean-url';
3
+ import { LOG_LEVELS } from '../constants';
4
+ export class Log {
5
+ /** @type {long} the unix timestamp of the log event */
6
+ timestamp;
7
+ /** @type {string} the log message */
8
+ message;
9
+ /** @type {object} the object of attributes to be parsed by logging ingest into top-level properties */
10
+ attributes;
11
+ /** @type {'ERROR'|'TRACE'|'DEBUG'|'INFO'|'WARN'} the log type of the log */
12
+ level;
13
+
14
+ /**
15
+ * @param {number} timestamp - Unix timestamp
16
+ * @param {string} message - message string
17
+ * @param {object} attributes - other log event attributes
18
+ * @param {enum} level - Log level
19
+ */
20
+ constructor(timestamp, message) {
21
+ let attributes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
22
+ let level = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : LOG_LEVELS.INFO;
23
+ /** @type {long} */
24
+ this.timestamp = timestamp;
25
+ this.message = message;
26
+ this.attributes = {
27
+ ...attributes,
28
+ pageUrl: cleanURL('' + globalScope.location)
29
+ };
30
+ this.level = level.toUpperCase();
31
+ }
32
+ }
@@ -0,0 +1,44 @@
1
+ import { handle } from '../../../common/event-emitter/handle';
2
+ import { now } from '../../../common/timing/now';
3
+ import { warn } from '../../../common/util/console';
4
+ import { stringify } from '../../../common/util/stringify';
5
+ import { FEATURE_NAMES } from '../../../loaders/features/features';
6
+ import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants';
7
+ import { LOGGING_EVENT_EMITTER_CHANNEL, LOG_LEVELS } from '../constants';
8
+
9
+ /**
10
+ * @param {ContextualEE} ee - The contextual ee tied to the instance
11
+ * @param {string} message - the log message string
12
+ * @param {{[key: string]: *}} customAttributes - The log's custom attributes if any
13
+ * @param {enum} level - the log level enum
14
+ */
15
+ export function bufferLog(ee, message) {
16
+ let customAttributes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
17
+ let level = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : LOG_LEVELS.INFO;
18
+ try {
19
+ if (typeof message !== 'string') {
20
+ const stringified = stringify(message);
21
+ /**
22
+ * Error instances convert to `{}` when stringified
23
+ * Symbol converts to '' when stringified
24
+ * other cases tbd
25
+ * */
26
+ if (!!stringified && stringified !== '{}') message = stringified;else message = String(message);
27
+ }
28
+ } catch (err) {
29
+ warn('could not cast log message to string', message);
30
+ return;
31
+ }
32
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ["API/logging/".concat(level.toLowerCase(), "/called")], undefined, FEATURE_NAMES.metrics, ee);
33
+ handle(LOGGING_EVENT_EMITTER_CHANNEL, [now(), message, customAttributes, level], undefined, FEATURE_NAMES.logging, ee);
34
+ }
35
+
36
+ /**
37
+ * Checks if a supplied log level is acceptable for use in generating a log event
38
+ * @param {string} level
39
+ * @returns {boolean}
40
+ */
41
+ export function isValidLogLevel(level) {
42
+ if (typeof level !== 'string') return false;
43
+ return Object.values(LOG_LEVELS).some(logLevel => logLevel.toUpperCase() === level.toUpperCase());
44
+ }
@@ -42,7 +42,6 @@ export class Aggregate extends AggregateBase {
42
42
  if (getConfigurationValue(this.agentIdentifier, 'page_view_timing.long_task') === true) longTask.subscribe(this.#handleVitalMetric);
43
43
  registerHandler('docHidden', msTimestamp => this.endCurrentSession(msTimestamp), this.featureName, this.ee);
44
44
  registerHandler('winPagehide', msTimestamp => this.recordPageUnload(msTimestamp), this.featureName, this.ee);
45
- const initialHarvestSeconds = getConfigurationValue(this.agentIdentifier, 'page_view_timing.initialHarvestSeconds') || 10;
46
45
  const harvestTimeSeconds = getConfigurationValue(this.agentIdentifier, 'page_view_timing.harvestTimeSeconds') || 30;
47
46
  this.waitForFlags([]).then(() => {
48
47
  /* It's important that CWV api, like "onLCP", is called before the **scheduler** is initialized. The reason is because they listen to the same
@@ -79,7 +78,7 @@ export class Aggregate extends AggregateBase {
79
78
  return _this.prepareHarvest(...arguments);
80
79
  }
81
80
  }, this);
82
- scheduler.startTimer(harvestTimeSeconds, initialHarvestSeconds);
81
+ scheduler.startTimer(harvestTimeSeconds);
83
82
  this.drain();
84
83
  });
85
84
  }
@@ -298,7 +298,7 @@ export class Aggregate extends AggregateBase {
298
298
  this.scheduler.opts.gzip = false;
299
299
  }
300
300
  if (len > MAX_PAYLOAD_SIZE) {
301
- this.abort(ABORT_REASONS.TOO_BIG);
301
+ this.abort(ABORT_REASONS.TOO_BIG, len);
302
302
  return;
303
303
  }
304
304
  // TODO -- Gracefully handle the buffer for retries.
@@ -415,8 +415,9 @@ export class Aggregate extends AggregateBase {
415
415
  /** Abort the feature, once aborted it will not resume */
416
416
  abort() {
417
417
  let reason = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
418
+ let data = arguments.length > 1 ? arguments[1] : undefined;
418
419
  warn("SR aborted -- ".concat(reason.message));
419
- handle(SUPPORTABILITY_METRIC_CHANNEL, ["SessionReplay/Abort/".concat(reason.sm)], undefined, FEATURE_NAMES.metrics, this.ee);
420
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ["SessionReplay/Abort/".concat(reason.sm), data], undefined, FEATURE_NAMES.metrics, this.ee);
420
421
  this.blocked = true;
421
422
  this.mode = MODE.OFF;
422
423
  this.recorder?.stopRecording?.();
@@ -42,22 +42,30 @@ export class Aggregate extends AggregateBase {
42
42
  this.entitled ??= stEntitled;
43
43
  if (this.blocked || !this.entitled) return deregisterDrain(this.agentIdentifier, this.featureName);
44
44
  if (!this.initialized) {
45
+ this.initialized = true;
46
+ /** Store session identifiers at initialization time to be cross-checked later at harvest time for session changes that are subject to race conditions */
47
+ this.ptid = this.agentRuntime.ptid;
48
+ this.sessionId = this.agentRuntime.session?.state.value;
45
49
  // 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.
46
50
  this.ee.on(SESSION_EVENTS.RESET, () => {
47
- this.abort();
51
+ if (this.blocked) return;
52
+ this.abort(1);
48
53
  });
49
54
  // The SessionEntity can have updates (locally or across tabs for SR mode changes), (across tabs for ST mode changes).
50
55
  // Those updates should be sync'd here to ensure this page also honors the mode after initialization
51
56
  this.ee.on(SESSION_EVENTS.UPDATE, (eventType, sessionState) => {
57
+ if (this.blocked) return;
52
58
  // this will only have an effect if ST is NOT already in full mode
53
59
  if (this.mode !== MODE.FULL && (sessionState.sessionReplayMode === MODE.FULL || sessionState.sessionTraceMode === MODE.FULL)) this.switchToFull();
60
+ // if another page's session entity has expired, or another page has transitioned to off and this one hasn't... we can just abort straight away here
61
+ if (this.sessionId !== sessionState.value || eventType === 'cross-tab' && this.scheduler?.started && sessionState.sessionTraceMode === MODE.OFF) this.abort(2);
54
62
  });
55
63
  }
56
64
 
57
65
  /** ST/SR sampling flow in BCS - https://drive.google.com/file/d/19hwt2oft-8Hh4RrjpLqEXfpP_9wYBLcq/view?usp=sharing */
58
66
  /** ST will run in the mode provided by BCS if the session IS NEW. If not... it will use the state of the session entity to determine what mode to run in */
59
67
  if (!this.agentRuntime.session.isNew && !ignoreSession) this.mode = this.agentRuntime.session.state.sessionTraceMode;else this.mode = stMode;
60
- this.initialized = true;
68
+
61
69
  /** If the mode is off, we do not want to hold up draining for other features, so we deregister the feature for now.
62
70
  * If it drains later (due to a mode change), data and handlers will instantly drain instead of waiting for the registry. */
63
71
  if (this.mode === MODE.OFF) return deregisterDrain(this.agentIdentifier, this.featureName);
@@ -122,15 +130,15 @@ export class Aggregate extends AggregateBase {
122
130
  let options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
123
131
  this.traceStorage.prevStoredEvents.clear(); // release references to past events for GC
124
132
  if (!this.timeKeeper?.ready) return; // this should likely never happen, but just to be safe, we should never harvest if we cant correct time
125
- if (this.mode === MODE.OFF && this.traceStorage.nodeCount === 0) return;
126
- if (this.mode === MODE.ERROR) return; // Trace in this mode should never be harvesting, even on unload
127
-
133
+ if (this.blocked || this.mode !== MODE.FULL || this.traceStorage.nodeCount === 0) return;
134
+ if (this.sessionId !== this.agentRuntime.session?.state.value || this.ptid !== this.agentRuntime.ptid) return this.abort(3); // if something unexpected happened and we somehow still got to the point of harvesting after a session identifier changed, we should force-exit instead of harvesting
128
135
  /** Get the ST nodes from the traceStorage buffer. This also returns helpful metadata about the payload. */
129
136
  const {
130
137
  stns,
131
138
  earliestTimeStamp,
132
139
  latestTimeStamp
133
140
  } = this.traceStorage.takeSTNs();
141
+ if (!stns) return; // there are no trace nodes
134
142
  if (options.retry) {
135
143
  this.sentTrace = stns;
136
144
  }
@@ -179,8 +187,8 @@ export class Aggregate extends AggregateBase {
179
187
  ...(hasReplay && {
180
188
  hasReplay
181
189
  }),
182
- ptid: "".concat(this.agentRuntime.ptid),
183
- session: "".concat(this.agentRuntime.session?.state.value),
190
+ ptid: "".concat(this.ptid),
191
+ session: "".concat(this.sessionId),
184
192
  // customer-defined data should go last so that if it exceeds the query param padding limit it will be truncated instead of important attrs
185
193
  ...(endUserId && {
186
194
  'enduser.id': endUserId
@@ -222,7 +230,7 @@ export class Aggregate extends AggregateBase {
222
230
  }
223
231
 
224
232
  /** Stop running for the remainder of the page lifecycle */
225
- abort() {
233
+ abort(reason) {
226
234
  this.blocked = true;
227
235
  this.mode = MODE.OFF;
228
236
  this.agentRuntime.session.write({
@@ -272,7 +272,7 @@ export class TraceStorage {
272
272
  // Ajax (FEATURE) events--XML & fetches--pipes into ST here.
273
273
  storeXhrAgg(type, name, params, metrics) {
274
274
  if (type !== 'xhr') return;
275
- this.storeSTN(new TraceNode('Ajax', metrics.time, metrics.time + metrics.duration, "".concat(params.status, " ").concat(params.method, ": ").concat(params.host).concat(params.pathname)));
275
+ this.storeSTN(new TraceNode('Ajax', metrics.time, metrics.time + metrics.duration, "".concat(params.status, " ").concat(params.method, ": ").concat(params.host).concat(params.pathname), 'ajax'));
276
276
  }
277
277
  restoreNode(name, listOfSTNodes) {
278
278
  if (this.nodeCount >= MAX_NODES_PER_HARVEST) return;
@@ -1,10 +1,9 @@
1
- import { now } from '../../../common/timing/now';
2
1
  let nodesSeen = 0;
3
2
  export class BelNode {
4
3
  belType;
5
4
  /** List of other BelNode derivatives. Each children should be of a subclass that implements its own 'serialize' function. */
6
5
  children = [];
7
- start = now();
6
+ start;
8
7
  end;
9
8
  callbackEnd = 0;
10
9
  callbackDuration = 0;
@@ -61,7 +61,7 @@ export class Aggregate extends AggregateBase {
61
61
  });
62
62
 
63
63
  // By default, a complete UI driven interaction requires event -> URL change -> DOM mod in that exact order.
64
- registerHandler('newUIEvent', event => this.startUIInteraction(event.type, event.timeStamp, event.target), this.featureName, this.ee);
64
+ registerHandler('newUIEvent', event => this.startUIInteraction(event.type, Math.floor(event.timeStamp), event.target), this.featureName, this.ee);
65
65
  registerHandler('newURL', (timestamp, url) => this.interactionInProgress?.updateHistory(timestamp, url), this.featureName, this.ee);
66
66
  registerHandler('newDom', timestamp => {
67
67
  this.interactionInProgress?.updateDom(timestamp);
@@ -2,6 +2,7 @@ import { getInfo } from '../../../common/config/config';
2
2
  import { globalScope, initialLocation } from '../../../common/constants/runtime';
3
3
  import { generateUuid } from '../../../common/ids/unique-id';
4
4
  import { addCustomAttributes, getAddStringContext, nullable, numeric } from '../../../common/serialize/bel-serializer';
5
+ import { now } from '../../../common/timing/now';
5
6
  import { cleanURL } from '../../../common/url/clean-url';
6
7
  import { NODE_TYPE, INTERACTION_STATUS, INTERACTION_TYPE, API_TRIGGER_NAME } from '../constants';
7
8
  import { BelNode } from './bel-node';
@@ -39,11 +40,11 @@ export class Interaction extends BelNode {
39
40
  if (this.trigger === API_TRIGGER_NAME) this.createdByApi = true;
40
41
  }
41
42
  updateDom(timestamp) {
42
- this.domTimestamp = timestamp || performance.now(); // default timestamp should be precise for accurate isActiveDuring calculations
43
+ this.domTimestamp = timestamp || now(); // default timestamp should be precise for accurate isActiveDuring calculations
43
44
  }
44
45
  updateHistory(timestamp, newUrl) {
45
46
  this.newURL = newUrl || '' + globalScope?.location;
46
- this.historyTimestamp = timestamp || performance.now();
47
+ this.historyTimestamp = timestamp || now();
47
48
  }
48
49
  seenHistoryAndDomChange() {
49
50
  return this.historyTimestamp > 0 && this.domTimestamp > this.historyTimestamp; // URL must change before DOM does
@@ -113,9 +114,9 @@ export class Interaction extends BelNode {
113
114
  // IMPORTANT: The order in which addString is called matters and correlates to the order in which string shows up in the harvest payload. Do not re-order the following code.
114
115
  const fields = [numeric(this.belType), 0,
115
116
  // this will be overwritten below with number of attached nodes
116
- numeric(Math.floor(this.start - firstStartTimeOfPayload)),
117
+ numeric(this.start - firstStartTimeOfPayload),
117
118
  // relative to first node
118
- numeric(Math.floor(this.end - this.start)),
119
+ numeric(this.end - this.start),
119
120
  // end -- relative to start
120
121
  numeric(this.callbackEnd),
121
122
  // cbEnd -- relative to start; not used by BrowserInteraction events
@@ -536,7 +536,7 @@ export class Aggregate extends AggregateBase {
536
536
  var interaction = this.ixn;
537
537
  var node = activeNodeFor(interaction);
538
538
  setCurrentNode(null);
539
- node.child('customEnd', timestamp).finish(timestamp);
539
+ node.child('customEnd', timestamp)?.finish(timestamp);
540
540
  interaction.finish();
541
541
  }, this.featureName, baseEE);
542
542
  register(INTERACTION_API + 'ignore', function (t) {
@@ -18,6 +18,8 @@ export function lazyFeatureLoader(featureName, featurePart) {
18
18
  return import( /* webpackChunkName: "ajax-aggregate" */'../ajax/aggregate');
19
19
  case FEATURE_NAMES.jserrors:
20
20
  return import( /* webpackChunkName: "jserrors-aggregate" */'../jserrors/aggregate');
21
+ case FEATURE_NAMES.logging:
22
+ return import( /* webpackChunkName: "logging-aggregate" */'../logging/aggregate');
21
23
  case FEATURE_NAMES.metrics:
22
24
  return import( /* webpackChunkName: "metrics-aggregate" */'../metrics/aggregate');
23
25
  case FEATURE_NAMES.pageAction: