@newrelic/browser-agent 1.305.0 → 1.306.0-rc.1

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 (46) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cjs/common/constants/env.cdn.js +1 -1
  3. package/dist/cjs/common/constants/env.npm.js +1 -1
  4. package/dist/cjs/common/constants/runtime.js +1 -1
  5. package/dist/cjs/common/deny-list/deny-list.js +22 -36
  6. package/dist/cjs/common/session/session-entity.js +1 -0
  7. package/dist/cjs/common/vitals/time-to-first-byte.js +1 -0
  8. package/dist/cjs/common/wrap/wrap-logger.js +3 -1
  9. package/dist/cjs/features/logging/aggregate/index.js +42 -27
  10. package/dist/cjs/features/logging/instrument/index.js +3 -2
  11. package/dist/cjs/features/logging/shared/utils.js +3 -2
  12. package/dist/cjs/loaders/api/log.js +1 -1
  13. package/dist/cjs/loaders/api/wrapLogger.js +1 -1
  14. package/dist/esm/common/constants/env.cdn.js +1 -1
  15. package/dist/esm/common/constants/env.npm.js +1 -1
  16. package/dist/esm/common/constants/runtime.js +1 -1
  17. package/dist/esm/common/deny-list/deny-list.js +22 -36
  18. package/dist/esm/common/session/session-entity.js +1 -0
  19. package/dist/esm/common/vitals/time-to-first-byte.js +1 -0
  20. package/dist/esm/common/wrap/wrap-logger.js +3 -1
  21. package/dist/esm/features/logging/aggregate/index.js +42 -27
  22. package/dist/esm/features/logging/instrument/index.js +3 -2
  23. package/dist/esm/features/logging/shared/utils.js +3 -2
  24. package/dist/esm/loaders/api/log.js +1 -1
  25. package/dist/esm/loaders/api/wrapLogger.js +1 -1
  26. package/dist/types/common/constants/runtime.d.ts +1 -1
  27. package/dist/types/common/constants/runtime.d.ts.map +1 -1
  28. package/dist/types/common/deny-list/deny-list.d.ts.map +1 -1
  29. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  30. package/dist/types/common/wrap/wrap-logger.d.ts +2 -1
  31. package/dist/types/common/wrap/wrap-logger.d.ts.map +1 -1
  32. package/dist/types/features/logging/aggregate/index.d.ts +6 -5
  33. package/dist/types/features/logging/aggregate/index.d.ts.map +1 -1
  34. package/dist/types/features/logging/shared/utils.d.ts +2 -1
  35. package/dist/types/features/logging/shared/utils.d.ts.map +1 -1
  36. package/package.json +3 -2
  37. package/src/common/constants/runtime.js +1 -1
  38. package/src/common/deny-list/deny-list.js +25 -40
  39. package/src/common/session/session-entity.js +1 -0
  40. package/src/common/vitals/time-to-first-byte.js +1 -0
  41. package/src/common/wrap/wrap-logger.js +3 -1
  42. package/src/features/logging/aggregate/index.js +39 -30
  43. package/src/features/logging/instrument/index.js +2 -2
  44. package/src/features/logging/shared/utils.js +3 -2
  45. package/src/loaders/api/log.js +1 -1
  46. package/src/loaders/api/wrapLogger.js +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,19 @@
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.306.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.305.0...v1.306.0) (2025-12-16)
7
+
8
+
9
+ ### Features
10
+
11
+ * Control log API through separate RUM flag ([#1467](https://github.com/newrelic/newrelic-browser-agent/issues/1467)) ([f9f1639](https://github.com/newrelic/newrelic-browser-agent/commit/f9f1639504a32b0f24baa117d287c47e209cc818))
12
+ * Ensure responseStart is valid before relying on onTTFB ([#1634](https://github.com/newrelic/newrelic-browser-agent/issues/1634)) ([cd83f60](https://github.com/newrelic/newrelic-browser-agent/commit/cd83f605087a79cbc4817ddfb82d48c18eae8474))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * Remove linebreak syntax from webpack output ([#1649](https://github.com/newrelic/newrelic-browser-agent/issues/1649)) ([f741585](https://github.com/newrelic/newrelic-browser-agent/commit/f74158516ec251ced26a56f65c145a48b938e79f))
18
+
6
19
  ## [1.305.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.304.0...v1.305.0) (2025-12-10)
7
20
 
8
21
 
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.RRWEB_PACKAGE_NAME = exports.D
17
17
  /**
18
18
  * Exposes the version of the agent
19
19
  */
20
- const VERSION = exports.VERSION = "1.305.0";
20
+ const VERSION = exports.VERSION = "1.306.0-rc.1";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.RRWEB_PACKAGE_NAME = exports.D
17
17
  /**
18
18
  * Exposes the version of the agent
19
19
  */
20
- const VERSION = exports.VERSION = "1.305.0";
20
+ const VERSION = exports.VERSION = "1.306.0-rc.1";
21
21
 
22
22
  /**
23
23
  * Exposes the build type of the agent
@@ -54,5 +54,5 @@ const ffVersion = exports.ffVersion = (() => {
54
54
  * @type {number}
55
55
  */
56
56
  const originTime = exports.originTime = Date.now() - (0, _now.now)();
57
- const supportsNavTimingL2 = () => typeof PerformanceNavigationTiming !== 'undefined' && globalScope?.performance?.getEntriesByType('navigation')?.length > 0;
57
+ const supportsNavTimingL2 = () => typeof PerformanceNavigationTiming !== 'undefined' && globalScope?.performance?.getEntriesByType('navigation')?.[0]?.responseStart;
58
58
  exports.supportsNavTimingL2 = supportsNavTimingL2;
@@ -26,12 +26,12 @@ var denyList = [];
26
26
  function shouldCollectEvent(params) {
27
27
  if (!params || hasUndefinedHostname(params)) return false;
28
28
  if (denyList.length === 0) return true;
29
+
30
+ // short circuit if deny list contains just a wildcard
31
+ if (denyList[0].hostname === '*') return false;
29
32
  for (var i = 0; i < denyList.length; i++) {
30
33
  var parsed = denyList[i];
31
- if (parsed.hostname === '*') {
32
- return false;
33
- }
34
- if (domainMatchesPattern(parsed.hostname, params.hostname) && comparePath(parsed.pathname, params.pathname)) {
34
+ if (parsed.hostname.test(params.hostname) && parsed.pathname.test(params.pathname)) {
35
35
  return false;
36
36
  }
37
37
  }
@@ -54,6 +54,13 @@ function setDenyList(denyListConfig) {
54
54
  let url = denyListConfig[i];
55
55
  if (!url) continue; // ignore bad values like undefined or empty strings
56
56
 
57
+ // short circuit if deny list entry is just a wildcard
58
+ if (url === '*') {
59
+ denyList = [{
60
+ hostname: '*'
61
+ }];
62
+ return;
63
+ }
57
64
  if (url.indexOf('http://') === 0) {
58
65
  url = url.substring(7);
59
66
  } else if (url.indexOf('https://') === 0) {
@@ -66,45 +73,24 @@ function setDenyList(denyListConfig) {
66
73
  pathname = url.substring(firstSlash);
67
74
  } else {
68
75
  host = url;
69
- pathname = '';
76
+ pathname = '*';
70
77
  }
71
78
  let [hostname] = host.split(':');
72
79
  denyList.push({
73
- hostname,
74
- pathname
80
+ hostname: convertToRegularExpression(hostname),
81
+ pathname: convertToRegularExpression(pathname, true)
75
82
  });
76
83
  }
77
84
  }
78
- /**
79
- * Returns true if the right side of `domain` (end of string) matches `pattern`.
80
- * @param {string} pattern - a string to be matched against the end of `domain` string
81
- * @param {string} domain - a domain string with no protocol or path (e.g., app1.example.com)
82
- * @returns {boolean} `true` if domain matches pattern; else `false`
83
- */
84
- function domainMatchesPattern(pattern, domain) {
85
- if (pattern.length > domain.length) {
86
- return false;
87
- }
88
- return domain.indexOf(pattern) === domain.length - pattern.length;
89
- }
90
85
 
91
86
  /**
92
- * Returns true if a URL path matches a pattern string, disregarding leading slashes.
93
- * @param {string} pattern - a string to compare with path (e.g., api/v1)
94
- * @param {string} path - a string representing a URL path (e.g., /api/v1)
95
- * @returns {boolean} `true` if path and pattern are an exact string match (except for leading slashes); else `false`
87
+ * Converts a deny list filter string into a regular expression object with wildcard support
88
+ * @param {string} filter - deny list filter to convert
89
+ * @param {boolean} [isPathname=false] - indicates if the filter is a pathname
90
+ * @returns {RegExp} - regular expression object built from the input string
96
91
  */
97
- function comparePath(pattern, path) {
98
- if (pattern.indexOf('/') === 0) {
99
- pattern = pattern.substring(1);
100
- }
101
- if (path.indexOf('/') === 0) {
102
- path = path.substring(1);
103
- }
104
-
105
- // No path in pattern means match all paths.
106
- if (pattern === '') {
107
- return true;
108
- }
109
- return pattern === path;
92
+ function convertToRegularExpression(filter, isPathname = false) {
93
+ const newFilter = filter.replace(/[.+?^${}()|[\]\\]/g, m => '\\' + m) // use a replacer function to not break apm injection
94
+ .replace(/\*/g, '.*?'); // use lazy matching instead of greedy
95
+ return new RegExp((isPathname ? '^' : '') + newFilter + '$');
110
96
  }
@@ -36,6 +36,7 @@ const model = {
36
36
  sessionTraceMode: _constants.MODE.OFF,
37
37
  traceHarvestStarted: false,
38
38
  loggingMode: _constants3.LOGGING_MODE.OFF,
39
+ logApiMode: _constants3.LOGGING_MODE.OFF,
39
40
  serverTimeDiff: null,
40
41
  // set by TimeKeeper; "undefined" value will not be stringified and stored but "null" will
41
42
  custom: {},
@@ -21,6 +21,7 @@ const timeToFirstByte = exports.timeToFirstByte = new _vitalMetric.VitalMetric(_
21
21
  * - in browsers that do not support PerformanceNavigationTiming API
22
22
  * - in an iOS browser
23
23
  * - cross-origin iframes specifically in firefox and safari
24
+ * - onTTFB relies on a truthy `responseStart` value, should ensure that exists before relying on it (seen to be falsy in certain Electron.js cases for instance)
24
25
  */
25
26
  if (_runtime.isBrowserScope && (0, _runtime.supportsNavTimingL2)() && !_runtime.isiOS && window === window.parent) {
26
27
  (0, _attribution.onTTFB)(({
@@ -26,10 +26,11 @@ const contextMap = new Map();
26
26
  * @param {Object} sharedEE - The shared event emitter on which a new scoped event emitter will be based.
27
27
  * @param {Object} parent - The parent object housing the logger function
28
28
  * @param {string} loggerFn - The name of the function in the parent object to wrap
29
+ * @param {boolean} [autoCaptured=true] - True if log was captured from auto wrapping. False if it was captured from the API manual usage.
29
30
  * @returns {Object} Scoped event emitter with a debug ID of `logger`.
30
31
  */
31
32
  // eslint-disable-next-line
32
- function wrapLogger(sharedEE, parent, loggerFn, context) {
33
+ function wrapLogger(sharedEE, parent, loggerFn, context, autoCaptured = true) {
33
34
  if (!(typeof parent === 'object' && !!parent && typeof loggerFn === 'string' && !!loggerFn && typeof parent[loggerFn] === 'function')) return (0, _console.warn)(29);
34
35
  const ee = scopedEE(sharedEE);
35
36
  const wrapFn = (0, _wrapFunction.createWrapperWithEmitter)(ee);
@@ -41,6 +42,7 @@ function wrapLogger(sharedEE, parent, loggerFn, context) {
41
42
  const ctx = new _eventContext.EventContext(_contextualEe.contextId);
42
43
  ctx.level = context.level;
43
44
  ctx.customAttributes = context.customAttributes;
45
+ ctx.autoCaptured = autoCaptured;
44
46
  const contextLookupKey = parent[loggerFn]?.[_wrapFunction.flag] || parent[loggerFn];
45
47
  contextMap.set(contextLookupKey, ctx);
46
48
 
@@ -21,11 +21,19 @@ var _mfe = require("../../../common/util/mfe");
21
21
  * SPDX-License-Identifier: Apache-2.0
22
22
  */
23
23
 
24
+ const LOGGING_EVENT = 'Logging/Event/';
24
25
  class Aggregate extends _aggregateBase.AggregateBase {
25
26
  static featureName = _constants.FEATURE_NAME;
26
27
  constructor(agentRef) {
27
28
  super(agentRef, _constants.FEATURE_NAME);
28
- this.isSessionTrackingEnabled = (0, _featureGates.canEnableSessionTracking)(agentRef.init) && agentRef.runtime.session;
29
+ const updateLocalLoggingMode = (auto, api) => {
30
+ this.loggingMode = {
31
+ auto,
32
+ api
33
+ };
34
+ // In agent v1.290.0 & under, the logApiMode prop did not yet exist, so need to account for old session state being in-use.
35
+ if (api === undefined) this.loggingMode.api = auto;
36
+ };
29
37
 
30
38
  /** set up agg-level behaviors specific to this feature */
31
39
  this.harvestOpts.raw = true;
@@ -37,20 +45,25 @@ class Aggregate extends _aggregateBase.AggregateBase {
37
45
  });
38
46
  this.ee.on(_constants2.SESSION_EVENTS.UPDATE, (type, data) => {
39
47
  if (this.blocked || type !== _constants2.SESSION_EVENT_TYPES.CROSS_TAB) return;
40
- if (this.loggingMode !== _constants.LOGGING_MODE.OFF && data.loggingMode === _constants.LOGGING_MODE.OFF) this.abort(_constants3.ABORT_REASONS.CROSS_TAB);else this.loggingMode = data.loggingMode;
48
+ // In agent v1.290.0 & under, the logApiMode prop did not yet exist, so need to account for old session state being in-use with just loggingMode off == feature off.
49
+ if (data.loggingMode === _constants.LOGGING_MODE.OFF && (!data.logApiMode || data.logApiMode === _constants.LOGGING_MODE.OFF)) this.abort(_constants3.ABORT_REASONS.CROSS_TAB);else updateLocalLoggingMode(data.loggingMode, data.logApiMode);
41
50
  });
42
- this.waitForFlags(['log']).then(([loggingMode]) => {
43
- const session = this.agentRef.runtime.session ?? {};
44
- if (this.loggingMode === _constants.LOGGING_MODE.OFF || session.isNew && loggingMode === _constants.LOGGING_MODE.OFF) {
51
+ this.waitForFlags(['log', 'logapi']).then(([auto, api]) => {
52
+ if (this.blocked) return; // means abort already happened before this, likely from session reset or update; abort would've set mode off + deregistered drain
53
+
54
+ this.loggingMode ??= {
55
+ auto,
56
+ api
57
+ }; // likewise, don't want to overwrite the mode if it was set already
58
+ const session = this.agentRef.runtime.session;
59
+ if ((0, _featureGates.canEnableSessionTracking)(agentRef.init) && session) {
60
+ if (session.isNew) this.#syncWithSessionManager();else updateLocalLoggingMode(session.state.loggingMode, session.state.logApiMode);
61
+ }
62
+ if (this.loggingMode.auto === _constants.LOGGING_MODE.OFF && this.loggingMode.api === _constants.LOGGING_MODE.OFF) {
45
63
  this.blocked = true;
46
64
  this.deregisterDrain();
47
65
  return;
48
66
  }
49
- if (session.isNew || !this.isSessionTrackingEnabled) {
50
- this.updateLoggingMode(loggingMode);
51
- } else {
52
- this.loggingMode = session.state.loggingMode;
53
- }
54
67
 
55
68
  /** emitted by instrument class (wrapped loggers) or the api methods directly */
56
69
  (0, _registerHandler.registerHandler)(_constants.LOGGING_EVENT_EMITTER_CHANNEL, this.handleLog.bind(this), this.featureName, this.ee);
@@ -59,14 +72,11 @@ class Aggregate extends _aggregateBase.AggregateBase {
59
72
  agentRef.runtime.harvester.triggerHarvestFor(this);
60
73
  });
61
74
  }
62
- updateLoggingMode(loggingMode) {
63
- this.loggingMode = loggingMode;
64
- this.syncWithSessionManager({
65
- loggingMode: this.loggingMode
66
- });
67
- }
68
- handleLog(timestamp, message, attributes = {}, level = _constants.LOG_LEVELS.INFO, target) {
69
- if (this.blocked || !this.loggingMode) return;
75
+ handleLog(timestamp, message, attributes = {}, level = _constants.LOG_LEVELS.INFO, autoCaptured, target) {
76
+ if (this.blocked) return;
77
+ // Check respective logging mode depending on whether this log is from auto wrapped instrumentation or manual API that it's not turned off.
78
+ const modeForThisLog = autoCaptured ? this.loggingMode.auto : this.loggingMode.api;
79
+ if (!modeForThisLog) return;
70
80
  if (!attributes || typeof attributes !== 'object') attributes = {};
71
81
  attributes = {
72
82
  ...attributes,
@@ -75,8 +85,8 @@ class Aggregate extends _aggregateBase.AggregateBase {
75
85
  };
76
86
  if (typeof level === 'string') level = level.toUpperCase();
77
87
  if (!(0, _utils.isValidLogLevel)(level)) return (0, _console.warn)(30, level);
78
- if (this.loggingMode < (_constants.LOGGING_MODE[level] || Infinity)) {
79
- this.reportSupportabilityMetric('Logging/Event/Dropped/Sampling');
88
+ if (modeForThisLog < (_constants.LOGGING_MODE[level] || Infinity)) {
89
+ this.reportSupportabilityMetric(LOGGING_EVENT + 'Dropped/Sampling');
80
90
  return;
81
91
  }
82
92
  try {
@@ -91,12 +101,12 @@ class Aggregate extends _aggregateBase.AggregateBase {
91
101
  }
92
102
  } catch (err) {
93
103
  (0, _console.warn)(16, message);
94
- this.reportSupportabilityMetric('Logging/Event/Dropped/Casting');
104
+ this.reportSupportabilityMetric(LOGGING_EVENT + 'Dropped/Casting');
95
105
  return;
96
106
  }
97
107
  if (typeof message !== 'string' || !message) return (0, _console.warn)(32);
98
108
  const log = new _log.Log(Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp)), message, attributes, level);
99
- this.events.add(log);
109
+ if (this.events.add(log)) this.reportSupportabilityMetric(LOGGING_EVENT + (autoCaptured ? 'Auto' : 'API') + '/Added');
100
110
  }
101
111
  serializer(eventBuffer) {
102
112
  const sessionEntity = this.agentRef.runtime.session;
@@ -146,13 +156,18 @@ class Aggregate extends _aggregateBase.AggregateBase {
146
156
  this.events.clear();
147
157
  this.events.clearSave();
148
158
  }
149
- this.updateLoggingMode(_constants.LOGGING_MODE.OFF);
159
+ this.loggingMode = {
160
+ auto: _constants.LOGGING_MODE.OFF,
161
+ api: _constants.LOGGING_MODE.OFF
162
+ };
163
+ this.#syncWithSessionManager();
150
164
  this.deregisterDrain();
151
165
  }
152
- syncWithSessionManager(state = {}) {
153
- if (this.isSessionTrackingEnabled) {
154
- this.agentRef.runtime.session.write(state);
155
- }
166
+ #syncWithSessionManager() {
167
+ this.agentRef.runtime.session?.write({
168
+ loggingMode: this.loggingMode.auto,
169
+ logApiMode: this.loggingMode.api
170
+ });
156
171
  }
157
172
  }
158
173
  exports.Aggregate = Aggregate;
@@ -39,9 +39,10 @@ class Instrument extends _instrumentBase.InstrumentBase {
39
39
  this.ee.on('wrap-logger-end', function handleLog([message]) {
40
40
  const {
41
41
  level,
42
- customAttributes
42
+ customAttributes,
43
+ autoCaptured
43
44
  } = this;
44
- (0, _utils.bufferLog)(instanceEE, message, customAttributes, level);
45
+ (0, _utils.bufferLog)(instanceEE, message, customAttributes, level, autoCaptured);
45
46
  });
46
47
  this.importAggregator(agentRef, () => Promise.resolve().then(() => _interopRequireWildcard(require(/* webpackChunkName: "logging-aggregate" */'../aggregate'))));
47
48
  }
@@ -20,11 +20,12 @@ var _constants2 = require("../constants");
20
20
  * @param {string} message - the log message string
21
21
  * @param {{[key: string]: *}} customAttributes - The log's custom attributes if any
22
22
  * @param {enum} level - the log level enum
23
+ * @param {boolean} [autoCaptured=true] - True if log was captured from auto wrapping. False if it was captured from the API manual usage.
23
24
  * @param {object=} target - the optional target provided by an api call
24
25
  */
25
- function bufferLog(ee, message, customAttributes = {}, level = _constants2.LOG_LEVELS.INFO, target, timestamp = (0, _now.now)()) {
26
+ function bufferLog(ee, message, customAttributes = {}, level = _constants2.LOG_LEVELS.INFO, autoCaptured = true, target, timestamp = (0, _now.now)()) {
26
27
  (0, _handle.handle)(_constants.SUPPORTABILITY_METRIC_CHANNEL, ["API/logging/".concat(level.toLowerCase(), "/called")], undefined, _features.FEATURE_NAMES.metrics, ee);
27
- (0, _handle.handle)(_constants2.LOGGING_EVENT_EMITTER_CHANNEL, [timestamp, message, customAttributes, level, target], undefined, _features.FEATURE_NAMES.logging, ee);
28
+ (0, _handle.handle)(_constants2.LOGGING_EVENT_EMITTER_CHANNEL, [timestamp, message, customAttributes, level, autoCaptured, target], undefined, _features.FEATURE_NAMES.logging, ee);
28
29
  }
29
30
 
30
31
  /**
@@ -22,5 +22,5 @@ function log(message, {
22
22
  customAttributes = {},
23
23
  level = _constants.LOG_LEVELS.INFO
24
24
  } = {}, agentRef, target, timestamp = (0, _now.now)()) {
25
- (0, _utils.bufferLog)(agentRef.ee, message, customAttributes, level, target, timestamp);
25
+ (0, _utils.bufferLog)(agentRef.ee, message, customAttributes, level, false, target, timestamp);
26
26
  }
@@ -21,6 +21,6 @@ function setupWrapLoggerAPI(agent) {
21
21
  (0, _wrapLogger.wrapLogger)(agent.ee, parent, functionName, {
22
22
  customAttributes,
23
23
  level
24
- });
24
+ }, false);
25
25
  }, agent);
26
26
  }
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.305.0";
14
+ export const VERSION = "1.306.0-rc.1";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.305.0";
14
+ export const VERSION = "1.306.0-rc.1";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -49,4 +49,4 @@ export const ffVersion = (() => {
49
49
  * @type {number}
50
50
  */
51
51
  export const originTime = Date.now() - now();
52
- export const supportsNavTimingL2 = () => typeof PerformanceNavigationTiming !== 'undefined' && globalScope?.performance?.getEntriesByType('navigation')?.length > 0;
52
+ export const supportsNavTimingL2 = () => typeof PerformanceNavigationTiming !== 'undefined' && globalScope?.performance?.getEntriesByType('navigation')?.[0]?.responseStart;
@@ -18,12 +18,12 @@ var denyList = [];
18
18
  export function shouldCollectEvent(params) {
19
19
  if (!params || hasUndefinedHostname(params)) return false;
20
20
  if (denyList.length === 0) return true;
21
+
22
+ // short circuit if deny list contains just a wildcard
23
+ if (denyList[0].hostname === '*') return false;
21
24
  for (var i = 0; i < denyList.length; i++) {
22
25
  var parsed = denyList[i];
23
- if (parsed.hostname === '*') {
24
- return false;
25
- }
26
- if (domainMatchesPattern(parsed.hostname, params.hostname) && comparePath(parsed.pathname, params.pathname)) {
26
+ if (parsed.hostname.test(params.hostname) && parsed.pathname.test(params.pathname)) {
27
27
  return false;
28
28
  }
29
29
  }
@@ -46,6 +46,13 @@ export function setDenyList(denyListConfig) {
46
46
  let url = denyListConfig[i];
47
47
  if (!url) continue; // ignore bad values like undefined or empty strings
48
48
 
49
+ // short circuit if deny list entry is just a wildcard
50
+ if (url === '*') {
51
+ denyList = [{
52
+ hostname: '*'
53
+ }];
54
+ return;
55
+ }
49
56
  if (url.indexOf('http://') === 0) {
50
57
  url = url.substring(7);
51
58
  } else if (url.indexOf('https://') === 0) {
@@ -58,45 +65,24 @@ export function setDenyList(denyListConfig) {
58
65
  pathname = url.substring(firstSlash);
59
66
  } else {
60
67
  host = url;
61
- pathname = '';
68
+ pathname = '*';
62
69
  }
63
70
  let [hostname] = host.split(':');
64
71
  denyList.push({
65
- hostname,
66
- pathname
72
+ hostname: convertToRegularExpression(hostname),
73
+ pathname: convertToRegularExpression(pathname, true)
67
74
  });
68
75
  }
69
76
  }
70
- /**
71
- * Returns true if the right side of `domain` (end of string) matches `pattern`.
72
- * @param {string} pattern - a string to be matched against the end of `domain` string
73
- * @param {string} domain - a domain string with no protocol or path (e.g., app1.example.com)
74
- * @returns {boolean} `true` if domain matches pattern; else `false`
75
- */
76
- function domainMatchesPattern(pattern, domain) {
77
- if (pattern.length > domain.length) {
78
- return false;
79
- }
80
- return domain.indexOf(pattern) === domain.length - pattern.length;
81
- }
82
77
 
83
78
  /**
84
- * Returns true if a URL path matches a pattern string, disregarding leading slashes.
85
- * @param {string} pattern - a string to compare with path (e.g., api/v1)
86
- * @param {string} path - a string representing a URL path (e.g., /api/v1)
87
- * @returns {boolean} `true` if path and pattern are an exact string match (except for leading slashes); else `false`
79
+ * Converts a deny list filter string into a regular expression object with wildcard support
80
+ * @param {string} filter - deny list filter to convert
81
+ * @param {boolean} [isPathname=false] - indicates if the filter is a pathname
82
+ * @returns {RegExp} - regular expression object built from the input string
88
83
  */
89
- function comparePath(pattern, path) {
90
- if (pattern.indexOf('/') === 0) {
91
- pattern = pattern.substring(1);
92
- }
93
- if (path.indexOf('/') === 0) {
94
- path = path.substring(1);
95
- }
96
-
97
- // No path in pattern means match all paths.
98
- if (pattern === '') {
99
- return true;
100
- }
101
- return pattern === path;
84
+ function convertToRegularExpression(filter, isPathname = false) {
85
+ const newFilter = filter.replace(/[.+?^${}()|[\]\\]/g, m => '\\' + m) // use a replacer function to not break apm injection
86
+ .replace(/\*/g, '.*?'); // use lazy matching instead of greedy
87
+ return new RegExp((isPathname ? '^' : '') + newFilter + '$');
102
88
  }
@@ -30,6 +30,7 @@ const model = {
30
30
  sessionTraceMode: MODE.OFF,
31
31
  traceHarvestStarted: false,
32
32
  loggingMode: LOGGING_MODE.OFF,
33
+ logApiMode: LOGGING_MODE.OFF,
33
34
  serverTimeDiff: null,
34
35
  // set by TimeKeeper; "undefined" value will not be stringified and stored but "null" will
35
36
  custom: {},
@@ -14,6 +14,7 @@ export const timeToFirstByte = new VitalMetric(VITAL_NAMES.TIME_TO_FIRST_BYTE);
14
14
  * - in browsers that do not support PerformanceNavigationTiming API
15
15
  * - in an iOS browser
16
16
  * - cross-origin iframes specifically in firefox and safari
17
+ * - onTTFB relies on a truthy `responseStart` value, should ensure that exists before relying on it (seen to be falsy in certain Electron.js cases for instance)
17
18
  */
18
19
  if (isBrowserScope && supportsNavTimingL2() && !isiOS && window === window.parent) {
19
20
  onTTFB(({
@@ -19,10 +19,11 @@ const contextMap = new Map();
19
19
  * @param {Object} sharedEE - The shared event emitter on which a new scoped event emitter will be based.
20
20
  * @param {Object} parent - The parent object housing the logger function
21
21
  * @param {string} loggerFn - The name of the function in the parent object to wrap
22
+ * @param {boolean} [autoCaptured=true] - True if log was captured from auto wrapping. False if it was captured from the API manual usage.
22
23
  * @returns {Object} Scoped event emitter with a debug ID of `logger`.
23
24
  */
24
25
  // eslint-disable-next-line
25
- export function wrapLogger(sharedEE, parent, loggerFn, context) {
26
+ export function wrapLogger(sharedEE, parent, loggerFn, context, autoCaptured = true) {
26
27
  if (!(typeof parent === 'object' && !!parent && typeof loggerFn === 'string' && !!loggerFn && typeof parent[loggerFn] === 'function')) return warn(29);
27
28
  const ee = scopedEE(sharedEE);
28
29
  const wrapFn = wfn(ee);
@@ -34,6 +35,7 @@ export function wrapLogger(sharedEE, parent, loggerFn, context) {
34
35
  const ctx = new EventContext(contextId);
35
36
  ctx.level = context.level;
36
37
  ctx.customAttributes = context.customAttributes;
38
+ ctx.autoCaptured = autoCaptured;
37
39
  const contextLookupKey = parent[loggerFn]?.[flag] || parent[loggerFn];
38
40
  contextMap.set(contextLookupKey, ctx);
39
41
 
@@ -14,11 +14,19 @@ import { SESSION_EVENT_TYPES, SESSION_EVENTS } from '../../../common/session/con
14
14
  import { ABORT_REASONS } from '../../session_replay/constants';
15
15
  import { canEnableSessionTracking } from '../../utils/feature-gates';
16
16
  import { getVersion2Attributes } from '../../../common/util/mfe';
17
+ const LOGGING_EVENT = 'Logging/Event/';
17
18
  export class Aggregate extends AggregateBase {
18
19
  static featureName = FEATURE_NAME;
19
20
  constructor(agentRef) {
20
21
  super(agentRef, FEATURE_NAME);
21
- this.isSessionTrackingEnabled = canEnableSessionTracking(agentRef.init) && agentRef.runtime.session;
22
+ const updateLocalLoggingMode = (auto, api) => {
23
+ this.loggingMode = {
24
+ auto,
25
+ api
26
+ };
27
+ // In agent v1.290.0 & under, the logApiMode prop did not yet exist, so need to account for old session state being in-use.
28
+ if (api === undefined) this.loggingMode.api = auto;
29
+ };
22
30
 
23
31
  /** set up agg-level behaviors specific to this feature */
24
32
  this.harvestOpts.raw = true;
@@ -30,20 +38,25 @@ export class Aggregate extends AggregateBase {
30
38
  });
31
39
  this.ee.on(SESSION_EVENTS.UPDATE, (type, data) => {
32
40
  if (this.blocked || type !== SESSION_EVENT_TYPES.CROSS_TAB) return;
33
- if (this.loggingMode !== LOGGING_MODE.OFF && data.loggingMode === LOGGING_MODE.OFF) this.abort(ABORT_REASONS.CROSS_TAB);else this.loggingMode = data.loggingMode;
41
+ // In agent v1.290.0 & under, the logApiMode prop did not yet exist, so need to account for old session state being in-use with just loggingMode off == feature off.
42
+ if (data.loggingMode === LOGGING_MODE.OFF && (!data.logApiMode || data.logApiMode === LOGGING_MODE.OFF)) this.abort(ABORT_REASONS.CROSS_TAB);else updateLocalLoggingMode(data.loggingMode, data.logApiMode);
34
43
  });
35
- this.waitForFlags(['log']).then(([loggingMode]) => {
36
- const session = this.agentRef.runtime.session ?? {};
37
- if (this.loggingMode === LOGGING_MODE.OFF || session.isNew && loggingMode === LOGGING_MODE.OFF) {
44
+ this.waitForFlags(['log', 'logapi']).then(([auto, api]) => {
45
+ if (this.blocked) return; // means abort already happened before this, likely from session reset or update; abort would've set mode off + deregistered drain
46
+
47
+ this.loggingMode ??= {
48
+ auto,
49
+ api
50
+ }; // likewise, don't want to overwrite the mode if it was set already
51
+ const session = this.agentRef.runtime.session;
52
+ if (canEnableSessionTracking(agentRef.init) && session) {
53
+ if (session.isNew) this.#syncWithSessionManager();else updateLocalLoggingMode(session.state.loggingMode, session.state.logApiMode);
54
+ }
55
+ if (this.loggingMode.auto === LOGGING_MODE.OFF && this.loggingMode.api === LOGGING_MODE.OFF) {
38
56
  this.blocked = true;
39
57
  this.deregisterDrain();
40
58
  return;
41
59
  }
42
- if (session.isNew || !this.isSessionTrackingEnabled) {
43
- this.updateLoggingMode(loggingMode);
44
- } else {
45
- this.loggingMode = session.state.loggingMode;
46
- }
47
60
 
48
61
  /** emitted by instrument class (wrapped loggers) or the api methods directly */
49
62
  registerHandler(LOGGING_EVENT_EMITTER_CHANNEL, this.handleLog.bind(this), this.featureName, this.ee);
@@ -52,14 +65,11 @@ export class Aggregate extends AggregateBase {
52
65
  agentRef.runtime.harvester.triggerHarvestFor(this);
53
66
  });
54
67
  }
55
- updateLoggingMode(loggingMode) {
56
- this.loggingMode = loggingMode;
57
- this.syncWithSessionManager({
58
- loggingMode: this.loggingMode
59
- });
60
- }
61
- handleLog(timestamp, message, attributes = {}, level = LOG_LEVELS.INFO, target) {
62
- if (this.blocked || !this.loggingMode) return;
68
+ handleLog(timestamp, message, attributes = {}, level = LOG_LEVELS.INFO, autoCaptured, target) {
69
+ if (this.blocked) return;
70
+ // Check respective logging mode depending on whether this log is from auto wrapped instrumentation or manual API that it's not turned off.
71
+ const modeForThisLog = autoCaptured ? this.loggingMode.auto : this.loggingMode.api;
72
+ if (!modeForThisLog) return;
63
73
  if (!attributes || typeof attributes !== 'object') attributes = {};
64
74
  attributes = {
65
75
  ...attributes,
@@ -68,8 +78,8 @@ export class Aggregate extends AggregateBase {
68
78
  };
69
79
  if (typeof level === 'string') level = level.toUpperCase();
70
80
  if (!isValidLogLevel(level)) return warn(30, level);
71
- if (this.loggingMode < (LOGGING_MODE[level] || Infinity)) {
72
- this.reportSupportabilityMetric('Logging/Event/Dropped/Sampling');
81
+ if (modeForThisLog < (LOGGING_MODE[level] || Infinity)) {
82
+ this.reportSupportabilityMetric(LOGGING_EVENT + 'Dropped/Sampling');
73
83
  return;
74
84
  }
75
85
  try {
@@ -84,12 +94,12 @@ export class Aggregate extends AggregateBase {
84
94
  }
85
95
  } catch (err) {
86
96
  warn(16, message);
87
- this.reportSupportabilityMetric('Logging/Event/Dropped/Casting');
97
+ this.reportSupportabilityMetric(LOGGING_EVENT + 'Dropped/Casting');
88
98
  return;
89
99
  }
90
100
  if (typeof message !== 'string' || !message) return warn(32);
91
101
  const log = new Log(Math.floor(this.agentRef.runtime.timeKeeper.correctRelativeTimestamp(timestamp)), message, attributes, level);
92
- this.events.add(log);
102
+ if (this.events.add(log)) this.reportSupportabilityMetric(LOGGING_EVENT + (autoCaptured ? 'Auto' : 'API') + '/Added');
93
103
  }
94
104
  serializer(eventBuffer) {
95
105
  const sessionEntity = this.agentRef.runtime.session;
@@ -139,12 +149,17 @@ export class Aggregate extends AggregateBase {
139
149
  this.events.clear();
140
150
  this.events.clearSave();
141
151
  }
142
- this.updateLoggingMode(LOGGING_MODE.OFF);
152
+ this.loggingMode = {
153
+ auto: LOGGING_MODE.OFF,
154
+ api: LOGGING_MODE.OFF
155
+ };
156
+ this.#syncWithSessionManager();
143
157
  this.deregisterDrain();
144
158
  }
145
- syncWithSessionManager(state = {}) {
146
- if (this.isSessionTrackingEnabled) {
147
- this.agentRef.runtime.session.write(state);
148
- }
159
+ #syncWithSessionManager() {
160
+ this.agentRef.runtime.session?.write({
161
+ loggingMode: this.loggingMode.auto,
162
+ logApiMode: this.loggingMode.api
163
+ });
149
164
  }
150
165
  }
@@ -33,9 +33,10 @@ export class Instrument extends InstrumentBase {
33
33
  this.ee.on('wrap-logger-end', function handleLog([message]) {
34
34
  const {
35
35
  level,
36
- customAttributes
36
+ customAttributes,
37
+ autoCaptured
37
38
  } = this;
38
- bufferLog(instanceEE, message, customAttributes, level);
39
+ bufferLog(instanceEE, message, customAttributes, level, autoCaptured);
39
40
  });
40
41
  this.importAggregator(agentRef, () => import(/* webpackChunkName: "logging-aggregate" */'../aggregate'));
41
42
  }
@@ -13,11 +13,12 @@ import { LOGGING_EVENT_EMITTER_CHANNEL, LOG_LEVELS } from '../constants';
13
13
  * @param {string} message - the log message string
14
14
  * @param {{[key: string]: *}} customAttributes - The log's custom attributes if any
15
15
  * @param {enum} level - the log level enum
16
+ * @param {boolean} [autoCaptured=true] - True if log was captured from auto wrapping. False if it was captured from the API manual usage.
16
17
  * @param {object=} target - the optional target provided by an api call
17
18
  */
18
- export function bufferLog(ee, message, customAttributes = {}, level = LOG_LEVELS.INFO, target, timestamp = now()) {
19
+ export function bufferLog(ee, message, customAttributes = {}, level = LOG_LEVELS.INFO, autoCaptured = true, target, timestamp = now()) {
19
20
  handle(SUPPORTABILITY_METRIC_CHANNEL, ["API/logging/".concat(level.toLowerCase(), "/called")], undefined, FEATURE_NAMES.metrics, ee);
20
- handle(LOGGING_EVENT_EMITTER_CHANNEL, [timestamp, message, customAttributes, level, target], undefined, FEATURE_NAMES.logging, ee);
21
+ handle(LOGGING_EVENT_EMITTER_CHANNEL, [timestamp, message, customAttributes, level, autoCaptured, target], undefined, FEATURE_NAMES.logging, ee);
21
22
  }
22
23
 
23
24
  /**
@@ -15,5 +15,5 @@ export function log(message, {
15
15
  customAttributes = {},
16
16
  level = LOG_LEVELS.INFO
17
17
  } = {}, agentRef, target, timestamp = now()) {
18
- bufferLog(agentRef.ee, message, customAttributes, level, target, timestamp);
18
+ bufferLog(agentRef.ee, message, customAttributes, level, false, target, timestamp);
19
19
  }
@@ -14,6 +14,6 @@ export function setupWrapLoggerAPI(agent) {
14
14
  wrapLogger(agent.ee, parent, functionName, {
15
15
  customAttributes,
16
16
  level
17
- });
17
+ }, false);
18
18
  }, agent);
19
19
  }
@@ -26,5 +26,5 @@ export const ffVersion: number;
26
26
  * @type {number}
27
27
  */
28
28
  export const originTime: number;
29
- export function supportsNavTimingL2(): boolean;
29
+ export function supportsNavTimingL2(): any;
30
30
  //# sourceMappingURL=runtime.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../../src/common/constants/runtime.js"],"names":[],"mappings":"AAcA;;GAEG;AACH,qCAEqB;AAErB;;GAEG;AACH,oCAeK;AAEL,oDAUI;AAEJ,oDAA6F;AAE7F,sCAA2F;AAE3F,qCAAyD;AAEzD,4BAA8E;AAE9E;;;;;;GAMG;AACH,iCAAwE;AAExE,+BAOI;AAEJ;;;;GAIG;AACH,yBAFU,MAAM,CAE4B;AAErC,+CAA4J"}
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../../src/common/constants/runtime.js"],"names":[],"mappings":"AAcA;;GAEG;AACH,qCAEqB;AAErB;;GAEG;AACH,oCAeK;AAEL,oDAUI;AAEJ,oDAA6F;AAE7F,sCAA2F;AAE3F,qCAAyD;AAEzD,4BAA8E;AAE9E;;;;;;GAMG;AACH,iCAAwE;AAExE,+BAOI;AAEJ;;;;GAIG;AACH,yBAFU,MAAM,CAE4B;AAErC,2CAAoK"}
@@ -1 +1 @@
1
- {"version":3,"file":"deny-list.d.ts","sourceRoot":"","sources":["../../../../src/common/deny-list/deny-list.js"],"names":[],"mappings":"AAYA;;;;GAIG;AACH,2CAHW,MAAM,GACJ,OAAO,CAqBnB;AAED,2DAEC;AAED;;;GAGG;AACH,4CAFW,MAAM,EAAE,QAgClB"}
1
+ {"version":3,"file":"deny-list.d.ts","sourceRoot":"","sources":["../../../../src/common/deny-list/deny-list.js"],"names":[],"mappings":"AAYA;;;;GAIG;AACH,2CAHW,MAAM,GACJ,OAAO,CAoBnB;AAED,2DAEC;AAED;;;GAGG;AACH,4CAFW,MAAM,EAAE,QAyClB"}
@@ -1 +1 @@
1
- {"version":3,"file":"session-entity.d.ts","sourceRoot":"","sources":["../../../../src/common/session/session-entity.js"],"names":[],"mappings":"AAsCA;IACE;;;;;OAKG;IACH,uBA+BC;IAzBC,qBAAsC;IACtC,aAAsB;IACtB,UAAe;IAGf,SAAc;IAEd,QAAiC;IAoBnC;;;;;aAgFC;IApEC,8BAA0B;IAC1B,+BAA4B;IAe1B,gCAOqC;IAUrC,4CAkBsC;IAexC,iCAAuB;IAKzB,wBAEC;IAED,sBAEC;IAED;;;OAGG;IACH,QAFa,MAAM,CA6BlB;IAED;;;;;;OAMG;IACH,YAHW,MAAM,GACJ,MAAM,CAkBlB;IAED,gBA4BC;IAED;;OAEG;IACH,gBAIC;IAED;;;OAGG;IACH,qBAHW,MAAM,GACJ,OAAO,CAInB;IAED;;;OAGG;IACH,gBAHW,MAAM,GACJ,OAAO,CAKnB;IAED,yDAUC;IAED,6DAIC;IAED;;;OAGG;IACH,6BAHW,MAAM,GACJ,MAAM,CAIlB;IAED,gDAaC;IAHG,YAAuD;CAI5D;sBA3TqB,gBAAgB;iCAGL,4BAA4B"}
1
+ {"version":3,"file":"session-entity.d.ts","sourceRoot":"","sources":["../../../../src/common/session/session-entity.js"],"names":[],"mappings":"AAuCA;IACE;;;;;OAKG;IACH,uBA+BC;IAzBC,qBAAsC;IACtC,aAAsB;IACtB,UAAe;IAGf,SAAc;IAEd,QAAiC;IAoBnC;;;;;aAgFC;IApEC,8BAA0B;IAC1B,+BAA4B;IAe1B,gCAOqC;IAUrC,4CAkBsC;IAexC,iCAAuB;IAKzB,wBAEC;IAED,sBAEC;IAED;;;OAGG;IACH,QAFa,MAAM,CA6BlB;IAED;;;;;;OAMG;IACH,YAHW,MAAM,GACJ,MAAM,CAkBlB;IAED,gBA4BC;IAED;;OAEG;IACH,gBAIC;IAED;;;OAGG;IACH,qBAHW,MAAM,GACJ,OAAO,CAInB;IAED;;;OAGG;IACH,gBAHW,MAAM,GACJ,OAAO,CAKnB;IAED,yDAUC;IAED,6DAIC;IAED;;;OAGG;IACH,6BAHW,MAAM,GACJ,MAAM,CAIlB;IAED,gDAaC;IAHG,YAAuD;CAI5D;sBA5TqB,gBAAgB;iCAGL,4BAA4B"}
@@ -3,9 +3,10 @@
3
3
  * @param {Object} sharedEE - The shared event emitter on which a new scoped event emitter will be based.
4
4
  * @param {Object} parent - The parent object housing the logger function
5
5
  * @param {string} loggerFn - The name of the function in the parent object to wrap
6
+ * @param {boolean} [autoCaptured=true] - True if log was captured from auto wrapping. False if it was captured from the API manual usage.
6
7
  * @returns {Object} Scoped event emitter with a debug ID of `logger`.
7
8
  */
8
- export function wrapLogger(sharedEE: Object, parent: Object, loggerFn: string, context: any): Object;
9
+ export function wrapLogger(sharedEE: Object, parent: Object, loggerFn: string, context: any, autoCaptured?: boolean): Object;
9
10
  /**
10
11
  * Returns an event emitter scoped specifically for the `logger` context. This scoping is a remnant from when all the
11
12
  * features shared the same group in the event, to isolate events between features. It will likely be revisited.
@@ -1 +1 @@
1
- {"version":3,"file":"wrap-logger.d.ts","sourceRoot":"","sources":["../../../../src/common/wrap/wrap-logger.js"],"names":[],"mappings":"AAiBA;;;;;;GAMG;AAEH,qCANW,MAAM,UACN,MAAM,YACN,MAAM,iBACJ,MAAM,CAuBlB;AAED;;;;;;GAMG;AACH,mCAJW,MAAM,GAEJ,MAAM,CAIlB"}
1
+ {"version":3,"file":"wrap-logger.d.ts","sourceRoot":"","sources":["../../../../src/common/wrap/wrap-logger.js"],"names":[],"mappings":"AAiBA;;;;;;;GAOG;AAEH,qCAPW,MAAM,UACN,MAAM,YACN,MAAM,+BACN,OAAO,GACL,MAAM,CAwBlB;AAED;;;;;;GAMG;AACH,mCAJW,MAAM,GAEJ,MAAM,CAIlB"}
@@ -1,10 +1,11 @@
1
1
  export class Aggregate extends AggregateBase {
2
2
  static featureName: string;
3
3
  constructor(agentRef: any);
4
- isSessionTrackingEnabled: any;
5
- loggingMode: any;
6
- updateLoggingMode(loggingMode: any): void;
7
- handleLog(timestamp: any, message: any, attributes: {} | undefined, level: string | undefined, target: any): void;
4
+ loggingMode: {
5
+ auto: any;
6
+ api: any;
7
+ };
8
+ handleLog(timestamp: any, message: any, attributes: {} | undefined, level: string | undefined, autoCaptured: any, target: any): void;
8
9
  serializer(eventBuffer: any): {
9
10
  common: {
10
11
  /** Attributes in the `common` section are added to `all` logs generated in the payload */
@@ -18,7 +19,7 @@ export class Aggregate extends AggregateBase {
18
19
  };
19
20
  /** Abort the feature, once aborted it will not resume */
20
21
  abort(reason?: {}): void;
21
- syncWithSessionManager(state?: {}): void;
22
+ #private;
22
23
  }
23
24
  import { AggregateBase } from '../../utils/aggregate-base';
24
25
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/logging/aggregate/index.js"],"names":[],"mappings":"AAiBA;IACE,2BAAiC;IACjC,2BAsCC;IApCC,8BAAmG;IAc5F,iBAAmC;IAwB5C,0CAKC;IAED,kHA4CC;IAED;;YAIM,0FAA0F;;;QAqB5F,0DAA0D;;QAM7D;IAED;;MAEC;IAED,yDAAyD;IACzD,yBASC;IAED,yCAIC;CACF;8BA/J6B,4BAA4B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/logging/aggregate/index.js"],"names":[],"mappings":"AAmBA;IACE,2BAAiC;IACjC,2BA4CC;IAzCG;;;MAAgC;IA2CpC,qIA+CC;IAED;;YAIM,0FAA0F;;;QAqB5F,0DAA0D;;QAM7D;IAED;;MAEC;IAED,yDAAyD;IACzD,yBAaC;;CAQF;8BAxK6B,4BAA4B"}
@@ -3,11 +3,12 @@
3
3
  * @param {string} message - the log message string
4
4
  * @param {{[key: string]: *}} customAttributes - The log's custom attributes if any
5
5
  * @param {enum} level - the log level enum
6
+ * @param {boolean} [autoCaptured=true] - True if log was captured from auto wrapping. False if it was captured from the API manual usage.
6
7
  * @param {object=} target - the optional target provided by an api call
7
8
  */
8
9
  export function bufferLog(ee: ContextualEE, message: string, customAttributes?: {
9
10
  [key: string]: any;
10
- }, level?: enum, target?: object | undefined, timestamp?: number): void;
11
+ }, level?: enum, autoCaptured?: boolean, target?: object | undefined, timestamp?: number): void;
11
12
  /**
12
13
  * Checks if a supplied log level is acceptable for use in generating a log event
13
14
  * @param {string} level -- must be cast to uppercase before running this test
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../../src/features/logging/shared/utils.js"],"names":[],"mappings":"AAUA;;;;;;KAMK;AACL,8BANa,YAAY,WACZ,MAAM,qBACN;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAC,CAAA;CAAC,UAClB,IAAI,WACJ,MAAM,YAAC,4BAKnB;AAED;;;;GAIG;AACH,uCAHW,MAAM,GACJ,OAAO,CAKnB"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../../src/features/logging/shared/utils.js"],"names":[],"mappings":"AAUA;;;;;;;KAOK;AACL,8BAPa,YAAY,WACZ,MAAM,qBACN;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,GAAC,CAAA;CAAC,UAClB,IAAI,iBACJ,OAAO,WACP,MAAM,YAAC,4BAKnB;AAED;;;;GAIG;AACH,uCAHW,MAAM,GACJ,OAAO,CAKnB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@newrelic/browser-agent",
3
- "version": "1.305.0",
3
+ "version": "1.306.0-rc.1",
4
4
  "private": false,
5
5
  "author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
6
6
  "description": "New Relic Browser Agent",
@@ -195,6 +195,7 @@
195
195
  "build:npm": "npm run npm:build:esm && npm run npm:build:cjs && npm run npm:build:types && npm run npm:pack",
196
196
  "cdn:build": "npm run cdn:build:prod",
197
197
  "cdn:build:local": "npm run cdn:webpack",
198
+ "cdn:build:local-external": "npm run cdn:webpack -- --env mode=local-external",
198
199
  "cdn:build:prod": "npm run cdn:webpack -- --env mode=prod",
199
200
  "cdn:build:dev": "npm run cdn:webpack -- --env mode=dev",
200
201
  "cdn:build:experiment": "npm run cdn:webpack -- --env mode=experiment",
@@ -291,4 +292,4 @@
291
292
  "README.md",
292
293
  "CHANGELOG.md"
293
294
  ]
294
- }
295
+ }
@@ -84,4 +84,4 @@ export const ffVersion = (() => {
84
84
  */
85
85
  export const originTime = Date.now() - now()
86
86
 
87
- export const supportsNavTimingL2 = () => typeof PerformanceNavigationTiming !== 'undefined' && globalScope?.performance?.getEntriesByType('navigation')?.length > 0
87
+ export const supportsNavTimingL2 = () => typeof PerformanceNavigationTiming !== 'undefined' && globalScope?.performance?.getEntriesByType('navigation')?.[0]?.responseStart
@@ -20,15 +20,14 @@ export function shouldCollectEvent (params) {
20
20
 
21
21
  if (denyList.length === 0) return true
22
22
 
23
+ // short circuit if deny list contains just a wildcard
24
+ if (denyList[0].hostname === '*') return false
25
+
23
26
  for (var i = 0; i < denyList.length; i++) {
24
27
  var parsed = denyList[i]
25
28
 
26
- if (parsed.hostname === '*') {
27
- return false
28
- }
29
-
30
- if (domainMatchesPattern(parsed.hostname, params.hostname) &&
31
- comparePath(parsed.pathname, params.pathname)) {
29
+ if (parsed.hostname.test(params.hostname) &&
30
+ parsed.pathname.test(params.pathname)) {
32
31
  return false
33
32
  }
34
33
  }
@@ -55,6 +54,12 @@ export function setDenyList (denyListConfig) {
55
54
  let url = denyListConfig[i]
56
55
  if (!url) continue // ignore bad values like undefined or empty strings
57
56
 
57
+ // short circuit if deny list entry is just a wildcard
58
+ if (url === '*') {
59
+ denyList = [{ hostname: '*' }]
60
+ return
61
+ }
62
+
58
63
  if (url.indexOf('http://') === 0) {
59
64
  url = url.substring(7)
60
65
  } else if (url.indexOf('https://') === 0) {
@@ -68,46 +73,26 @@ export function setDenyList (denyListConfig) {
68
73
  pathname = url.substring(firstSlash)
69
74
  } else {
70
75
  host = url
71
- pathname = ''
76
+ pathname = '*'
72
77
  }
73
78
  let [hostname] = host.split(':')
74
79
 
75
- denyList.push({ hostname, pathname })
80
+ denyList.push({
81
+ hostname: convertToRegularExpression(hostname),
82
+ pathname: convertToRegularExpression(pathname, true)
83
+ })
76
84
  }
77
85
  }
78
- /**
79
- * Returns true if the right side of `domain` (end of string) matches `pattern`.
80
- * @param {string} pattern - a string to be matched against the end of `domain` string
81
- * @param {string} domain - a domain string with no protocol or path (e.g., app1.example.com)
82
- * @returns {boolean} `true` if domain matches pattern; else `false`
83
- */
84
- function domainMatchesPattern (pattern, domain) {
85
- if (pattern.length > domain.length) {
86
- return false
87
- }
88
-
89
- return domain.indexOf(pattern) === (domain.length - pattern.length)
90
- }
91
86
 
92
87
  /**
93
- * Returns true if a URL path matches a pattern string, disregarding leading slashes.
94
- * @param {string} pattern - a string to compare with path (e.g., api/v1)
95
- * @param {string} path - a string representing a URL path (e.g., /api/v1)
96
- * @returns {boolean} `true` if path and pattern are an exact string match (except for leading slashes); else `false`
88
+ * Converts a deny list filter string into a regular expression object with wildcard support
89
+ * @param {string} filter - deny list filter to convert
90
+ * @param {boolean} [isPathname=false] - indicates if the filter is a pathname
91
+ * @returns {RegExp} - regular expression object built from the input string
97
92
  */
98
- function comparePath (pattern, path) {
99
- if (pattern.indexOf('/') === 0) {
100
- pattern = pattern.substring(1)
101
- }
102
-
103
- if (path.indexOf('/') === 0) {
104
- path = path.substring(1)
105
- }
106
-
107
- // No path in pattern means match all paths.
108
- if (pattern === '') {
109
- return true
110
- }
111
-
112
- return pattern === path
93
+ function convertToRegularExpression (filter, isPathname = false) {
94
+ const newFilter = filter
95
+ .replace(/[.+?^${}()|[\]\\]/g, (m) => '\\' + m) // use a replacer function to not break apm injection
96
+ .replace(/\*/g, '.*?') // use lazy matching instead of greedy
97
+ return new RegExp((isPathname ? '^' : '') + newFilter + '$')
113
98
  }
@@ -30,6 +30,7 @@ const model = {
30
30
  sessionTraceMode: MODE.OFF,
31
31
  traceHarvestStarted: false,
32
32
  loggingMode: LOGGING_MODE.OFF,
33
+ logApiMode: LOGGING_MODE.OFF,
33
34
  serverTimeDiff: null, // set by TimeKeeper; "undefined" value will not be stringified and stored but "null" will
34
35
  custom: {},
35
36
  numOfResets: 0,
@@ -15,6 +15,7 @@ export const timeToFirstByte = new VitalMetric(VITAL_NAMES.TIME_TO_FIRST_BYTE)
15
15
  * - in browsers that do not support PerformanceNavigationTiming API
16
16
  * - in an iOS browser
17
17
  * - cross-origin iframes specifically in firefox and safari
18
+ * - onTTFB relies on a truthy `responseStart` value, should ensure that exists before relying on it (seen to be falsy in certain Electron.js cases for instance)
18
19
  */
19
20
  if (isBrowserScope && supportsNavTimingL2() && !isiOS && window === window.parent) {
20
21
  onTTFB(({ value, attribution }) => {
@@ -20,10 +20,11 @@ const contextMap = new Map()
20
20
  * @param {Object} sharedEE - The shared event emitter on which a new scoped event emitter will be based.
21
21
  * @param {Object} parent - The parent object housing the logger function
22
22
  * @param {string} loggerFn - The name of the function in the parent object to wrap
23
+ * @param {boolean} [autoCaptured=true] - True if log was captured from auto wrapping. False if it was captured from the API manual usage.
23
24
  * @returns {Object} Scoped event emitter with a debug ID of `logger`.
24
25
  */
25
26
  // eslint-disable-next-line
26
- export function wrapLogger(sharedEE, parent, loggerFn, context) {
27
+ export function wrapLogger(sharedEE, parent, loggerFn, context, autoCaptured = true) {
27
28
  if (!(typeof parent === 'object' && !!parent && typeof loggerFn === 'string' && !!loggerFn && typeof parent[loggerFn] === 'function')) return warn(29)
28
29
  const ee = scopedEE(sharedEE)
29
30
  const wrapFn = wfn(ee)
@@ -35,6 +36,7 @@ export function wrapLogger(sharedEE, parent, loggerFn, context) {
35
36
  const ctx = new EventContext(contextId)
36
37
  ctx.level = context.level
37
38
  ctx.customAttributes = context.customAttributes
39
+ ctx.autoCaptured = autoCaptured
38
40
 
39
41
  const contextLookupKey = parent[loggerFn]?.[flag] || parent[loggerFn]
40
42
  contextMap.set(contextLookupKey, ctx)
@@ -15,11 +15,17 @@ import { ABORT_REASONS } from '../../session_replay/constants'
15
15
  import { canEnableSessionTracking } from '../../utils/feature-gates'
16
16
  import { getVersion2Attributes } from '../../../common/util/mfe'
17
17
 
18
+ const LOGGING_EVENT = 'Logging/Event/'
19
+
18
20
  export class Aggregate extends AggregateBase {
19
21
  static featureName = FEATURE_NAME
20
22
  constructor (agentRef) {
21
23
  super(agentRef, FEATURE_NAME)
22
- this.isSessionTrackingEnabled = canEnableSessionTracking(agentRef.init) && agentRef.runtime.session
24
+ const updateLocalLoggingMode = (auto, api) => {
25
+ this.loggingMode = { auto, api }
26
+ // In agent v1.290.0 & under, the logApiMode prop did not yet exist, so need to account for old session state being in-use.
27
+ if (api === undefined) this.loggingMode.api = auto
28
+ }
23
29
 
24
30
  /** set up agg-level behaviors specific to this feature */
25
31
  this.harvestOpts.raw = true
@@ -29,25 +35,27 @@ export class Aggregate extends AggregateBase {
29
35
  this.ee.on(SESSION_EVENTS.RESET, () => {
30
36
  this.abort(ABORT_REASONS.RESET)
31
37
  })
32
-
33
38
  this.ee.on(SESSION_EVENTS.UPDATE, (type, data) => {
34
39
  if (this.blocked || type !== SESSION_EVENT_TYPES.CROSS_TAB) return
35
- if (this.loggingMode !== LOGGING_MODE.OFF && data.loggingMode === LOGGING_MODE.OFF) this.abort(ABORT_REASONS.CROSS_TAB)
36
- else this.loggingMode = data.loggingMode
40
+ // In agent v1.290.0 & under, the logApiMode prop did not yet exist, so need to account for old session state being in-use with just loggingMode off == feature off.
41
+ if (data.loggingMode === LOGGING_MODE.OFF && (!data.logApiMode || data.logApiMode === LOGGING_MODE.OFF)) this.abort(ABORT_REASONS.CROSS_TAB)
42
+ else updateLocalLoggingMode(data.loggingMode, data.logApiMode)
37
43
  })
38
44
 
39
- this.waitForFlags(['log']).then(([loggingMode]) => {
40
- const session = this.agentRef.runtime.session ?? {}
41
- if (this.loggingMode === LOGGING_MODE.OFF || (session.isNew && loggingMode === LOGGING_MODE.OFF)) {
45
+ this.waitForFlags(['log', 'logapi']).then(([auto, api]) => {
46
+ if (this.blocked) return // means abort already happened before this, likely from session reset or update; abort would've set mode off + deregistered drain
47
+
48
+ this.loggingMode ??= { auto, api } // likewise, don't want to overwrite the mode if it was set already
49
+ const session = this.agentRef.runtime.session
50
+ if (canEnableSessionTracking(agentRef.init) && session) {
51
+ if (session.isNew) this.#syncWithSessionManager()
52
+ else updateLocalLoggingMode(session.state.loggingMode, session.state.logApiMode)
53
+ }
54
+ if (this.loggingMode.auto === LOGGING_MODE.OFF && this.loggingMode.api === LOGGING_MODE.OFF) {
42
55
  this.blocked = true
43
56
  this.deregisterDrain()
44
57
  return
45
58
  }
46
- if (session.isNew || !this.isSessionTrackingEnabled) {
47
- this.updateLoggingMode(loggingMode)
48
- } else {
49
- this.loggingMode = session.state.loggingMode
50
- }
51
59
 
52
60
  /** emitted by instrument class (wrapped loggers) or the api methods directly */
53
61
  registerHandler(LOGGING_EVENT_EMITTER_CHANNEL, this.handleLog.bind(this), this.featureName, this.ee)
@@ -57,15 +65,11 @@ export class Aggregate extends AggregateBase {
57
65
  })
58
66
  }
59
67
 
60
- updateLoggingMode (loggingMode) {
61
- this.loggingMode = loggingMode
62
- this.syncWithSessionManager({
63
- loggingMode: this.loggingMode
64
- })
65
- }
66
-
67
- handleLog (timestamp, message, attributes = {}, level = LOG_LEVELS.INFO, target) {
68
- if (this.blocked || !this.loggingMode) return
68
+ handleLog (timestamp, message, attributes = {}, level = LOG_LEVELS.INFO, autoCaptured, target) {
69
+ if (this.blocked) return
70
+ // Check respective logging mode depending on whether this log is from auto wrapped instrumentation or manual API that it's not turned off.
71
+ const modeForThisLog = autoCaptured ? this.loggingMode.auto : this.loggingMode.api
72
+ if (!modeForThisLog) return
69
73
 
70
74
  if (!attributes || typeof attributes !== 'object') attributes = {}
71
75
 
@@ -77,8 +81,8 @@ export class Aggregate extends AggregateBase {
77
81
 
78
82
  if (typeof level === 'string') level = level.toUpperCase()
79
83
  if (!isValidLogLevel(level)) return warn(30, level)
80
- if (this.loggingMode < (LOGGING_MODE[level] || Infinity)) {
81
- this.reportSupportabilityMetric('Logging/Event/Dropped/Sampling')
84
+ if (modeForThisLog < (LOGGING_MODE[level] || Infinity)) {
85
+ this.reportSupportabilityMetric(LOGGING_EVENT + 'Dropped/Sampling')
82
86
  return
83
87
  }
84
88
 
@@ -95,7 +99,7 @@ export class Aggregate extends AggregateBase {
95
99
  }
96
100
  } catch (err) {
97
101
  warn(16, message)
98
- this.reportSupportabilityMetric('Logging/Event/Dropped/Casting')
102
+ this.reportSupportabilityMetric(LOGGING_EVENT + 'Dropped/Casting')
99
103
  return
100
104
  }
101
105
  if (typeof message !== 'string' || !message) return warn(32)
@@ -107,7 +111,7 @@ export class Aggregate extends AggregateBase {
107
111
  level
108
112
  )
109
113
 
110
- this.events.add(log)
114
+ if (this.events.add(log)) this.reportSupportabilityMetric(LOGGING_EVENT + (autoCaptured ? 'Auto' : 'API') + '/Added')
111
115
  }
112
116
 
113
117
  serializer (eventBuffer) {
@@ -155,13 +159,18 @@ export class Aggregate extends AggregateBase {
155
159
  this.events.clear()
156
160
  this.events.clearSave()
157
161
  }
158
- this.updateLoggingMode(LOGGING_MODE.OFF)
162
+ this.loggingMode = {
163
+ auto: LOGGING_MODE.OFF,
164
+ api: LOGGING_MODE.OFF
165
+ }
166
+ this.#syncWithSessionManager()
159
167
  this.deregisterDrain()
160
168
  }
161
169
 
162
- syncWithSessionManager (state = {}) {
163
- if (this.isSessionTrackingEnabled) {
164
- this.agentRef.runtime.session.write(state)
165
- }
170
+ #syncWithSessionManager () {
171
+ this.agentRef.runtime.session?.write({
172
+ loggingMode: this.loggingMode.auto,
173
+ logApiMode: this.loggingMode.api
174
+ })
166
175
  }
167
176
  }
@@ -32,8 +32,8 @@ export class Instrument extends InstrumentBase {
32
32
 
33
33
  /** emitted by wrap-logger function */
34
34
  this.ee.on('wrap-logger-end', function handleLog ([message]) {
35
- const { level, customAttributes } = this
36
- bufferLog(instanceEE, message, customAttributes, level)
35
+ const { level, customAttributes, autoCaptured } = this
36
+ bufferLog(instanceEE, message, customAttributes, level, autoCaptured)
37
37
  })
38
38
  this.importAggregator(agentRef, () => import(/* webpackChunkName: "logging-aggregate" */ '../aggregate'))
39
39
  }
@@ -13,11 +13,12 @@ import { LOGGING_EVENT_EMITTER_CHANNEL, LOG_LEVELS } from '../constants'
13
13
  * @param {string} message - the log message string
14
14
  * @param {{[key: string]: *}} customAttributes - The log's custom attributes if any
15
15
  * @param {enum} level - the log level enum
16
+ * @param {boolean} [autoCaptured=true] - True if log was captured from auto wrapping. False if it was captured from the API manual usage.
16
17
  * @param {object=} target - the optional target provided by an api call
17
18
  */
18
- export function bufferLog (ee, message, customAttributes = {}, level = LOG_LEVELS.INFO, target, timestamp = now()) {
19
+ export function bufferLog (ee, message, customAttributes = {}, level = LOG_LEVELS.INFO, autoCaptured = true, target, timestamp = now()) {
19
20
  handle(SUPPORTABILITY_METRIC_CHANNEL, [`API/logging/${level.toLowerCase()}/called`], undefined, FEATURE_NAMES.metrics, ee)
20
- handle(LOGGING_EVENT_EMITTER_CHANNEL, [timestamp, message, customAttributes, level, target], undefined, FEATURE_NAMES.logging, ee)
21
+ handle(LOGGING_EVENT_EMITTER_CHANNEL, [timestamp, message, customAttributes, level, autoCaptured, target], undefined, FEATURE_NAMES.logging, ee)
21
22
  }
22
23
 
23
24
  /**
@@ -14,5 +14,5 @@ export function setupLogAPI (agent) {
14
14
  }
15
15
 
16
16
  export function log (message, { customAttributes = {}, level = LOG_LEVELS.INFO } = {}, agentRef, target, timestamp = now()) {
17
- bufferLog(agentRef.ee, message, customAttributes, level, target, timestamp)
17
+ bufferLog(agentRef.ee, message, customAttributes, level, false, target, timestamp)
18
18
  }
@@ -9,6 +9,6 @@ import { setupAPI } from './sharedHandlers'
9
9
 
10
10
  export function setupWrapLoggerAPI (agent) {
11
11
  setupAPI(WRAP_LOGGER, (parent, functionName, { customAttributes = {}, level = LOG_LEVELS.INFO } = {}) => {
12
- wrapLogger(agent.ee, parent, functionName, { customAttributes, level })
12
+ wrapLogger(agent.ee, parent, functionName, { customAttributes, level }, false)
13
13
  }, agent)
14
14
  }