@newrelic/browser-agent 1.260.1 → 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 (140) hide show
  1. package/CHANGELOG.md +19 -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/deny-list/deny-list.js +1 -1
  12. package/dist/cjs/common/harvest/harvest-scheduler.js +1 -1
  13. package/dist/cjs/common/harvest/harvest.js +1 -1
  14. package/dist/cjs/common/session/session-entity.js +7 -1
  15. package/dist/cjs/common/wrap/wrap-logger.js +54 -0
  16. package/dist/cjs/features/ajax/aggregate/index.js +1 -1
  17. package/dist/cjs/features/logging/aggregate/index.js +102 -0
  18. package/dist/cjs/features/logging/constants.js +20 -0
  19. package/dist/cjs/features/logging/index.js +12 -0
  20. package/dist/cjs/features/logging/instrument/index.js +28 -0
  21. package/dist/cjs/features/logging/shared/log.js +39 -0
  22. package/dist/cjs/features/logging/shared/utils.js +50 -0
  23. package/dist/cjs/features/page_view_event/aggregate/index.js +1 -1
  24. package/dist/cjs/features/page_view_event/instrument/index.js +1 -1
  25. package/dist/cjs/features/page_view_timing/aggregate/index.js +1 -2
  26. package/dist/cjs/features/session_replay/aggregate/index.js +4 -3
  27. package/dist/cjs/features/session_replay/instrument/index.js +1 -1
  28. package/dist/cjs/features/session_trace/aggregate/index.js +15 -8
  29. package/dist/cjs/features/session_trace/instrument/index.js +1 -1
  30. package/dist/cjs/features/spa/aggregate/index.js +2 -2
  31. package/dist/cjs/features/spa/instrument/index.js +1 -1
  32. package/dist/cjs/features/utils/instrument-base.js +1 -1
  33. package/dist/cjs/features/utils/lazy-feature-loader.js +3 -1
  34. package/dist/cjs/loaders/agent-base.js +23 -2
  35. package/dist/cjs/loaders/api/api-methods.js +1 -1
  36. package/dist/cjs/loaders/api/api.js +29 -2
  37. package/dist/cjs/loaders/features/features.js +7 -5
  38. package/dist/cjs/loaders/micro-agent.js +1 -1
  39. package/dist/esm/cdn/experimental.js +2 -1
  40. package/dist/esm/cdn/polyfills/pro.js +2 -1
  41. package/dist/esm/cdn/polyfills/spa.js +2 -1
  42. package/dist/esm/cdn/pro.js +2 -1
  43. package/dist/esm/cdn/spa.js +2 -1
  44. package/dist/esm/common/config/state/init.js +30 -23
  45. package/dist/esm/common/constants/env.cdn.js +1 -1
  46. package/dist/esm/common/constants/env.npm.js +1 -1
  47. package/dist/esm/common/deny-list/deny-list.js +1 -1
  48. package/dist/esm/common/session/session-entity.js +8 -2
  49. package/dist/esm/common/wrap/wrap-logger.js +48 -0
  50. package/dist/esm/features/logging/aggregate/index.js +95 -0
  51. package/dist/esm/features/logging/constants.js +14 -0
  52. package/dist/esm/features/logging/index.js +1 -0
  53. package/dist/esm/features/logging/instrument/index.js +21 -0
  54. package/dist/esm/features/logging/shared/log.js +32 -0
  55. package/dist/esm/features/logging/shared/utils.js +44 -0
  56. package/dist/esm/features/page_view_timing/aggregate/index.js +1 -2
  57. package/dist/esm/features/session_replay/aggregate/index.js +3 -2
  58. package/dist/esm/features/session_trace/aggregate/index.js +15 -8
  59. package/dist/esm/features/spa/aggregate/index.js +1 -1
  60. package/dist/esm/features/utils/lazy-feature-loader.js +2 -0
  61. package/dist/esm/loaders/agent-base.js +23 -2
  62. package/dist/esm/loaders/api/api-methods.js +1 -1
  63. package/dist/esm/loaders/api/api.js +28 -1
  64. package/dist/esm/loaders/features/features.js +7 -5
  65. package/dist/types/common/config/state/init.d.ts.map +1 -1
  66. package/dist/types/common/drain/drain.d.ts.map +1 -1
  67. package/dist/types/common/harvest/harvest-scheduler.d.ts.map +1 -1
  68. package/dist/types/common/harvest/harvest.d.ts +5 -5
  69. package/dist/types/common/harvest/types.d.ts +2 -2
  70. package/dist/types/common/harvest/types.d.ts.map +1 -1
  71. package/dist/types/common/ids/id.d.ts.map +1 -1
  72. package/dist/types/common/ids/unique-id.d.ts.map +1 -1
  73. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  74. package/dist/types/common/util/console.d.ts.map +1 -1
  75. package/dist/types/common/util/data-size.d.ts.map +1 -1
  76. package/dist/types/common/util/feature-flags.d.ts.map +1 -1
  77. package/dist/types/common/util/get-or-set.d.ts.map +1 -1
  78. package/dist/types/common/util/invoke.d.ts.map +1 -1
  79. package/dist/types/common/util/stringify.d.ts.map +1 -1
  80. package/dist/types/common/util/submit-data.d.ts.map +1 -1
  81. package/dist/types/common/util/type-check.d.ts.map +1 -1
  82. package/dist/types/common/wrap/wrap-logger.d.ts +17 -0
  83. package/dist/types/common/wrap/wrap-logger.d.ts.map +1 -0
  84. package/dist/types/features/jserrors/aggregate/compute-stack-trace.d.ts.map +1 -1
  85. package/dist/types/features/jserrors/aggregate/index.d.ts +1 -1
  86. package/dist/types/features/logging/aggregate/index.d.ts +40 -0
  87. package/dist/types/features/logging/aggregate/index.d.ts.map +1 -0
  88. package/dist/types/features/logging/constants.d.ts +14 -0
  89. package/dist/types/features/logging/constants.d.ts.map +1 -0
  90. package/dist/types/features/logging/index.d.ts +2 -0
  91. package/dist/types/features/logging/index.d.ts.map +1 -0
  92. package/dist/types/features/logging/instrument/index.d.ts +6 -0
  93. package/dist/types/features/logging/instrument/index.d.ts.map +1 -0
  94. package/dist/types/features/logging/shared/log.d.ts +18 -0
  95. package/dist/types/features/logging/shared/log.d.ts.map +1 -0
  96. package/dist/types/features/logging/shared/utils.d.ts +16 -0
  97. package/dist/types/features/logging/shared/utils.d.ts.map +1 -0
  98. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  99. package/dist/types/features/session_replay/aggregate/index.d.ts +1 -1
  100. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  101. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  102. package/dist/types/features/session_trace/aggregate/index.d.ts +7 -4
  103. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  104. package/dist/types/features/utils/feature-base.d.ts +1 -1
  105. package/dist/types/features/utils/feature-base.d.ts.map +1 -1
  106. package/dist/types/features/utils/instrument-base.d.ts +2 -2
  107. package/dist/types/features/utils/lazy-feature-loader.d.ts.map +1 -1
  108. package/dist/types/loaders/agent-base.d.ts +25 -4
  109. package/dist/types/loaders/agent-base.d.ts.map +1 -1
  110. package/dist/types/loaders/api/api.d.ts +8 -0
  111. package/dist/types/loaders/api/api.d.ts.map +1 -1
  112. package/dist/types/loaders/api/interaction-types.d.ts.map +1 -1
  113. package/dist/types/loaders/features/features.d.ts +1 -0
  114. package/dist/types/loaders/features/features.d.ts.map +1 -1
  115. package/dist/types/loaders/micro-agent.d.ts.map +1 -1
  116. package/package.json +15 -28
  117. package/src/cdn/experimental.js +2 -0
  118. package/src/cdn/polyfills/pro.js +3 -1
  119. package/src/cdn/polyfills/spa.js +2 -0
  120. package/src/cdn/pro.js +3 -1
  121. package/src/cdn/spa.js +2 -0
  122. package/src/common/config/state/init.js +17 -15
  123. package/src/common/deny-list/deny-list.js +1 -1
  124. package/src/common/session/session-entity.js +7 -2
  125. package/src/common/wrap/wrap-logger.js +49 -0
  126. package/src/features/logging/aggregate/index.js +101 -0
  127. package/src/features/logging/constants.js +19 -0
  128. package/src/features/logging/index.js +1 -0
  129. package/src/features/logging/instrument/index.js +18 -0
  130. package/src/features/logging/shared/log.js +28 -0
  131. package/src/features/logging/shared/utils.js +43 -0
  132. package/src/features/page_view_timing/aggregate/index.js +1 -2
  133. package/src/features/session_replay/aggregate/index.js +3 -3
  134. package/src/features/session_trace/aggregate/index.js +14 -8
  135. package/src/features/spa/aggregate/index.js +1 -1
  136. package/src/features/utils/lazy-feature-loader.js +2 -0
  137. package/src/loaders/agent-base.js +23 -2
  138. package/src/loaders/api/api-methods.js +1 -1
  139. package/src/loaders/api/api.js +19 -1
  140. 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'
@@ -31,12 +32,6 @@ const model = () => {
31
32
  }
32
33
  }
33
34
  return {
34
- feature_flags: [],
35
- proxy: {
36
- assets: undefined, // if this value is set, it will be used to overwrite the webpack asset path used to fetch assets
37
- beacon: undefined // likewise for the url to which we send analytics
38
- },
39
- privacy: { cookies_enabled: true }, // *cli - per discussion, default should be true
40
35
  ajax: { deny_list: undefined, block_internal: true, enabled: true, harvestTimeSeconds: 10, autoStart: true },
41
36
  distributed_tracing: {
42
37
  enabled: undefined,
@@ -45,19 +40,24 @@ const model = () => {
45
40
  cors_use_tracecontext_headers: undefined,
46
41
  allowed_origins: undefined
47
42
  },
48
- session: {
49
- expiresMs: DEFAULT_EXPIRES_MS,
50
- inactiveMs: DEFAULT_INACTIVE_MS
51
- },
52
- ssl: undefined,
53
- obfuscate: undefined,
43
+ feature_flags: [],
44
+ harvest: { tooManyRequestsDelay: 60 },
54
45
  jserrors: { enabled: true, harvestTimeSeconds: 10, autoStart: true },
46
+ logging: { enabled: true, harvestTimeSeconds: 10, autoStart: true, level: LOG_LEVELS.INFO },
55
47
  metrics: { enabled: true, autoStart: true },
48
+ obfuscate: undefined,
56
49
  page_action: { enabled: true, harvestTimeSeconds: 30, autoStart: true },
57
50
  page_view_event: { enabled: true, autoStart: true },
58
51
  page_view_timing: { enabled: true, harvestTimeSeconds: 30, long_task: false, autoStart: true },
59
- session_trace: { enabled: true, harvestTimeSeconds: 10, autoStart: true },
60
- harvest: { tooManyRequestsDelay: 60 },
52
+ privacy: { cookies_enabled: true }, // *cli - per discussion, default should be true
53
+ proxy: {
54
+ assets: undefined, // if this value is set, it will be used to overwrite the webpack asset path used to fetch assets
55
+ beacon: undefined // likewise for the url to which we send analytics
56
+ },
57
+ session: {
58
+ expiresMs: DEFAULT_EXPIRES_MS,
59
+ inactiveMs: DEFAULT_INACTIVE_MS
60
+ },
61
61
  session_replay: {
62
62
  // feature settings
63
63
  autoStart: true,
@@ -100,8 +100,10 @@ const model = () => {
100
100
  else warn('An invalid session_replay.mask_input_option was provided and will not be used', val)
101
101
  }
102
102
  },
103
+ session_trace: { enabled: true, harvestTimeSeconds: 10, autoStart: true },
104
+ soft_navigations: { enabled: true, harvestTimeSeconds: 10, autoStart: true },
103
105
  spa: { enabled: true, harvestTimeSeconds: 10, autoStart: true },
104
- soft_navigations: { enabled: true, harvestTimeSeconds: 10, autoStart: true }
106
+ ssl: undefined
105
107
  }
106
108
  }
107
109
 
@@ -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
 
15
15
  if (denyList.length === 0) return true
16
16
 
@@ -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'
@@ -52,7 +52,12 @@ export class SessionEntity {
52
52
  wrapEvents(this.ee)
53
53
  this.setup(opts)
54
54
 
55
- if (isBrowserScope) {
55
+ /**
56
+ * Do not emit session storage events for IE11, because IE11 is unable to determine
57
+ * if the event was spawned on the current page or an adjacent page, and the behavior tied
58
+ * to storage events is critical to apply only to cross-tab behavior
59
+ * */
60
+ if (isBrowserScope && !isIE) {
56
61
  windowAddEventListener('storage', (event) => {
57
62
  if (event.key === this.lookupKey) {
58
63
  const obj = typeof event.newValue === 'string' ? JSON.parse(event.newValue) : event.newValue
@@ -0,0 +1,49 @@
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
+
37
+ return ee
38
+ }
39
+
40
+ /**
41
+ * Returns an event emitter scoped specifically for the `logger` context. This scoping is a remnant from when all the
42
+ * features shared the same group in the event, to isolate events between features. It will likely be revisited.
43
+ * @param {Object} sharedEE - Optional event emitter on which to base the scoped emitter.
44
+ * Uses `ee` on the global scope if undefined).
45
+ * @returns {Object} Scoped event emitter with a debug ID of 'logger'.
46
+ */
47
+ export function scopedEE (sharedEE) {
48
+ return (sharedEE || baseEE).get('logger')
49
+ }
@@ -0,0 +1,101 @@
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
+
12
+ export class Aggregate extends AggregateBase {
13
+ static featureName = FEATURE_NAME
14
+ #agentRuntime
15
+ #agentInfo
16
+ constructor (agentIdentifier, aggregator) {
17
+ super(agentIdentifier, aggregator, FEATURE_NAME)
18
+
19
+ /** held logs before sending */
20
+ this.bufferedLogs = []
21
+ /** held logs during sending, for retries */
22
+ this.outgoingLogs = []
23
+ /** the estimated bytes of log data waiting to be sent -- triggers a harvest if adding a new log will exceed limit */
24
+ this.estimatedBytes = 0
25
+
26
+ this.#agentRuntime = getRuntime(this.agentIdentifier)
27
+ this.#agentInfo = getInfo(this.agentIdentifier)
28
+
29
+ this.harvestTimeSeconds = getConfigurationValue(this.agentIdentifier, 'logging.harvestTimeSeconds')
30
+
31
+ this.waitForFlags([]).then(() => {
32
+ this.scheduler = new HarvestScheduler('browser/logs', {
33
+ onFinished: this.onHarvestFinished.bind(this),
34
+ retryDelay: this.harvestTimeSeconds,
35
+ getPayload: this.prepareHarvest.bind(this),
36
+ raw: true
37
+ }, this)
38
+ /** harvest immediately once started to purge pre-load logs collected */
39
+ this.scheduler.startTimer(this.harvestTimeSeconds, 0)
40
+ /** emitted by instrument class (wrapped loggers) or the api methods directly */
41
+ registerHandler(LOGGING_EVENT_EMITTER_CHANNEL, this.handleLog.bind(this), this.featureName, this.ee)
42
+ this.drain()
43
+ })
44
+ }
45
+
46
+ handleLog (timestamp, message, attributes, level) {
47
+ if (this.blocked) return
48
+ const log = new Log(
49
+ this.#agentRuntime.timeKeeper.convertRelativeTimestamp(timestamp),
50
+ message,
51
+ attributes,
52
+ level
53
+ )
54
+ const logBytes = log.message.length + stringify(log.attributes).length + log.level.length + 10 // timestamp == 10 chars
55
+ if (logBytes > MAX_PAYLOAD_SIZE) {
56
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ['Logging/Harvest/Failed/Seen', logBytes])
57
+ return warn(LOGGING_IGNORED + '> ' + MAX_PAYLOAD_SIZE + ' bytes', log.message.slice(0, 25) + '...')
58
+ }
59
+
60
+ if (this.estimatedBytes + logBytes >= MAX_PAYLOAD_SIZE) {
61
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ['Logging/Harvest/Early/Seen', this.estimatedBytes + logBytes])
62
+ this.scheduler.runHarvest({})
63
+ }
64
+ this.estimatedBytes += logBytes
65
+ this.bufferedLogs.push(log)
66
+ }
67
+
68
+ prepareHarvest () {
69
+ if (this.blocked || !(this.bufferedLogs.length || this.outgoingLogs.length)) return
70
+ /** populate outgoing array while also clearing main buffer */
71
+ this.outgoingLogs.push(...this.bufferedLogs.splice(0))
72
+ this.estimatedBytes = 0
73
+ /** see https://source.datanerd.us/agents/rum-specs/blob/main/browser/Log for logging spec */
74
+ return {
75
+ qs: {
76
+ browser_monitoring_key: this.#agentInfo.licenseKey
77
+ },
78
+ body: [{
79
+ common: {
80
+ /** Attributes in the `common` section are added to `all` logs generated in the payload */
81
+ attributes: {
82
+ 'entity.guid': this.#agentRuntime.appMetadata?.agents?.[0]?.entityGuid, // browser entity guid as provided from RUM response
83
+ session: this.#agentRuntime?.session?.state.value || '0', // The session ID that we generate and keep across page loads
84
+ hasReplay: this.#agentRuntime?.session?.state.sessionReplayMode === 1, // True if a session replay recording is running
85
+ hasTrace: this.#agentRuntime?.session?.state.sessionTraceMode === 1, // True if a session trace recording is running
86
+ ptid: this.#agentRuntime.ptid, // page trace id
87
+ appId: this.#agentInfo.applicationID, // Application ID from info object,
88
+ standalone: Boolean(this.#agentInfo.sa), // copy paste (true) vs APM (false)
89
+ agentVersion: this.#agentRuntime.version // browser agent version
90
+ }
91
+ },
92
+ /** logs section contains individual unique log entries */
93
+ logs: this.outgoingLogs
94
+ }]
95
+ }
96
+ }
97
+
98
+ onHarvestFinished (result) {
99
+ if (!result.retry) this.outgoingLogs = []
100
+ }
101
+ }
@@ -0,0 +1,19 @@
1
+ import { FEATURE_NAMES } from '../../loaders/features/features'
2
+
3
+ export const LOG_LEVELS = {
4
+ ERROR: 'ERROR',
5
+ WARN: 'WARN',
6
+ INFO: 'INFO',
7
+ DEBUG: 'DEBUG',
8
+ TRACE: 'TRACE'
9
+ }
10
+
11
+ export const LOGGING_EVENT_EMITTER_CHANNEL = 'log'
12
+
13
+ export const FEATURE_NAME = FEATURE_NAMES.logging
14
+
15
+ export const MAX_PAYLOAD_SIZE = 1000000
16
+
17
+ export const LOGGING_FAILURE_MESSAGE = 'failed to wrap logger: '
18
+ export const LOGGING_LEVEL_FAILURE_MESSAGE = 'invalid log level: '
19
+ export const LOGGING_IGNORED = 'ignored log: '
@@ -0,0 +1 @@
1
+ export { Instrument as Logging } from './instrument/index'
@@ -0,0 +1,18 @@
1
+ import { InstrumentBase } from '../../utils/instrument-base'
2
+ import { FEATURE_NAME } from '../constants'
3
+ import { bufferLog } from '../shared/utils'
4
+
5
+ export class Instrument extends InstrumentBase {
6
+ static featureName = FEATURE_NAME
7
+ constructor (agentIdentifier, aggregator, auto = true) {
8
+ super(agentIdentifier, aggregator, FEATURE_NAME, auto)
9
+
10
+ const instanceEE = this.ee
11
+ /** emitted by wrap-logger function */
12
+ this.ee.on('wrap-logger-end', function handleLog ([message]) {
13
+ const { level, customAttributes } = this
14
+ bufferLog(instanceEE, message, customAttributes, level)
15
+ })
16
+ this.importAggregator()
17
+ }
18
+ }
@@ -0,0 +1,28 @@
1
+ import { globalScope } from '../../../common/constants/runtime'
2
+ import { cleanURL } from '../../../common/url/clean-url'
3
+ import { LOG_LEVELS } from '../constants'
4
+
5
+ export class Log {
6
+ /** @type {long} the unix timestamp of the log event */
7
+ timestamp
8
+ /** @type {string} the log message */
9
+ message
10
+ /** @type {object} the object of attributes to be parsed by logging ingest into top-level properties */
11
+ attributes
12
+ /** @type {'ERROR'|'TRACE'|'DEBUG'|'INFO'|'WARN'} the log type of the log */
13
+ level
14
+
15
+ /**
16
+ * @param {number} timestamp - Unix timestamp
17
+ * @param {string} message - message string
18
+ * @param {object} attributes - other log event attributes
19
+ * @param {enum} level - Log level
20
+ */
21
+ constructor (timestamp, message, attributes = {}, level = LOG_LEVELS.INFO) {
22
+ /** @type {long} */
23
+ this.timestamp = timestamp
24
+ this.message = message
25
+ this.attributes = { ...attributes, pageUrl: cleanURL('' + globalScope.location) }
26
+ this.level = level.toUpperCase()
27
+ }
28
+ }
@@ -0,0 +1,43 @@
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, customAttributes = {}, level = LOG_LEVELS.INFO) {
16
+ try {
17
+ if (typeof message !== 'string') {
18
+ const stringified = stringify(message)
19
+ /**
20
+ * Error instances convert to `{}` when stringified
21
+ * Symbol converts to '' when stringified
22
+ * other cases tbd
23
+ * */
24
+ if (!!stringified && stringified !== '{}') message = stringified
25
+ else message = String(message)
26
+ }
27
+ } catch (err) {
28
+ warn('could not cast log message to string', message)
29
+ return
30
+ }
31
+ handle(SUPPORTABILITY_METRIC_CHANNEL, [`API/logging/${level.toLowerCase()}/called`], undefined, FEATURE_NAMES.metrics, ee)
32
+ handle(LOGGING_EVENT_EMITTER_CHANNEL, [now(), message, customAttributes, level], undefined, FEATURE_NAMES.logging, ee)
33
+ }
34
+
35
+ /**
36
+ * Checks if a supplied log level is acceptable for use in generating a log event
37
+ * @param {string} level
38
+ * @returns {boolean}
39
+ */
40
+ export function isValidLogLevel (level) {
41
+ if (typeof level !== 'string') return false
42
+ return Object.values(LOG_LEVELS).some(logLevel => logLevel.toUpperCase() === level.toUpperCase())
43
+ }
@@ -42,7 +42,6 @@ export class Aggregate extends AggregateBase {
42
42
  registerHandler('docHidden', msTimestamp => this.endCurrentSession(msTimestamp), this.featureName, this.ee)
43
43
  registerHandler('winPagehide', msTimestamp => this.recordPageUnload(msTimestamp), this.featureName, this.ee)
44
44
 
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
 
48
47
  this.waitForFlags(([])).then(() => {
@@ -69,7 +68,7 @@ export class Aggregate extends AggregateBase {
69
68
  onFinished: (...args) => this.onHarvestFinished(...args),
70
69
  getPayload: (...args) => this.prepareHarvest(...args)
71
70
  }, this)
72
- scheduler.startTimer(harvestTimeSeconds, initialHarvestSeconds)
71
+ scheduler.startTimer(harvestTimeSeconds)
73
72
 
74
73
  this.drain()
75
74
  })
@@ -282,7 +282,7 @@ export class Aggregate extends AggregateBase {
282
282
  }
283
283
 
284
284
  if (len > MAX_PAYLOAD_SIZE) {
285
- this.abort(ABORT_REASONS.TOO_BIG)
285
+ this.abort(ABORT_REASONS.TOO_BIG, len)
286
286
  return
287
287
  }
288
288
  // TODO -- Gracefully handle the buffer for retries.
@@ -393,9 +393,9 @@ export class Aggregate extends AggregateBase {
393
393
  }
394
394
 
395
395
  /** Abort the feature, once aborted it will not resume */
396
- abort (reason = {}) {
396
+ abort (reason = {}, data) {
397
397
  warn(`SR aborted -- ${reason.message}`)
398
- handle(SUPPORTABILITY_METRIC_CHANNEL, [`SessionReplay/Abort/${reason.sm}`], undefined, FEATURE_NAMES.metrics, this.ee)
398
+ handle(SUPPORTABILITY_METRIC_CHANNEL, [`SessionReplay/Abort/${reason.sm}`, data], undefined, FEATURE_NAMES.metrics, this.ee)
399
399
  this.blocked = true
400
400
  this.mode = MODE.OFF
401
401
  this.recorder?.stopRecording?.()
@@ -42,15 +42,23 @@ export class Aggregate extends AggregateBase {
42
42
  if (this.blocked || !this.entitled) return deregisterDrain(this.agentIdentifier, this.featureName)
43
43
 
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
 
@@ -59,7 +67,6 @@ export class Aggregate extends AggregateBase {
59
67
  if (!this.agentRuntime.session.isNew && !ignoreSession) this.mode = this.agentRuntime.session.state.sessionTraceMode
60
68
  else this.mode = stMode
61
69
 
62
- this.initialized = true
63
70
  /** If the mode is off, we do not want to hold up draining for other features, so we deregister the feature for now.
64
71
  * If it drains later (due to a mode change), data and handlers will instantly drain instead of waiting for the registry. */
65
72
  if (this.mode === MODE.OFF) return deregisterDrain(this.agentIdentifier, this.featureName)
@@ -111,9 +118,8 @@ export class Aggregate extends AggregateBase {
111
118
  prepareHarvest (options = {}) {
112
119
  this.traceStorage.prevStoredEvents.clear() // release references to past events for GC
113
120
  if (!this.timeKeeper?.ready) return // this should likely never happen, but just to be safe, we should never harvest if we cant correct time
114
- if (this.mode === MODE.OFF && this.traceStorage.nodeCount === 0) return
115
- if (this.mode === MODE.ERROR) return // Trace in this mode should never be harvesting, even on unload
116
-
121
+ if (this.blocked || this.mode !== MODE.FULL || this.traceStorage.nodeCount === 0) return
122
+ 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
117
123
  /** Get the ST nodes from the traceStorage buffer. This also returns helpful metadata about the payload. */
118
124
  const { stns, earliestTimeStamp, latestTimeStamp } = this.traceStorage.takeSTNs()
119
125
  if (!stns) return // there are no trace nodes
@@ -160,8 +166,8 @@ export class Aggregate extends AggregateBase {
160
166
  agentVersion: this.agentRuntime.version,
161
167
  ...(firstSessionHarvest && { firstSessionHarvest }),
162
168
  ...(hasReplay && { hasReplay }),
163
- ptid: `${this.agentRuntime.ptid}`,
164
- session: `${this.agentRuntime.session?.state.value}`,
169
+ ptid: `${this.ptid}`,
170
+ session: `${this.sessionId}`,
165
171
  // customer-defined data should go last so that if it exceeds the query param padding limit it will be truncated instead of important attrs
166
172
  ...(endUserId && { 'enduser.id': endUserId })
167
173
  // The Query Param is being arbitrarily limited in length here. It is also applied when estimating the size of the payload in getPayloadSize()
@@ -195,7 +201,7 @@ export class Aggregate extends AggregateBase {
195
201
  }
196
202
 
197
203
  /** Stop running for the remainder of the page lifecycle */
198
- abort () {
204
+ abort (reason) {
199
205
  this.blocked = true
200
206
  this.mode = MODE.OFF
201
207
  this.agentRuntime.session.write({ sessionTraceMode: this.mode })
@@ -574,7 +574,7 @@ export class Aggregate extends AggregateBase {
574
574
  var interaction = this.ixn
575
575
  var node = activeNodeFor(interaction)
576
576
  setCurrentNode(null)
577
- node.child('customEnd', timestamp).finish(timestamp)
577
+ node.child('customEnd', timestamp)?.finish(timestamp)
578
578
  interaction.finish()
579
579
  }, this.featureName, baseEE)
580
580
 
@@ -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:
@@ -53,8 +53,8 @@ export class AgentBase {
53
53
  * Adds a user-defined attribute name and value to subsequent events on the page.
54
54
  * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/setcustomattribute/}
55
55
  * @param {string} name Name of the attribute. Appears as column in the PageView event. It will also appear as a column in the PageAction event if you are using it.
56
- * @param {string|number|null} value Value of the attribute. Appears as the value in the named attribute column in the PageView event. It will appear as a column in the PageAction event if you are using it. Custom attribute values cannot be complex objects, only simple types such as Strings and Integers.
57
- * @param {boolean} [persist] Default false. f set to true, the name-value pair will also be set into the browser's storage API. Then on the following instrumented pages that load within the same session, the pair will be re-applied as a custom attribute.
56
+ * @param {string|number|boolean|null} value Value of the attribute. Appears as the value in the named attribute column in the PageView event. It will appear as a column in the PageAction event if you are using it. Custom attribute values cannot be complex objects, only simple types such as Strings, Integers and Booleans. Passing a null value unsets any existing attribute of the same name.
57
+ * @param {boolean} [persist] Default false. If set to true, the name-value pair will also be set into the browser's storage API. Then on the following instrumented pages that load within the same session, the pair will be re-applied as a custom attribute.
58
58
  */
59
59
  setCustomAttribute (name, value, persist) {
60
60
  return this.#callMethod('setCustomAttribute', name, value, persist)
@@ -181,4 +181,25 @@ export class AgentBase {
181
181
  interaction () {
182
182
  return this.#callMethod('interaction')
183
183
  }
184
+
185
+ /**
186
+ * Capture a single log.
187
+ * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/loginfo/}
188
+ * @param {string} message String to be captured as log message
189
+ * @param {{customAttributes?: object, level?: 'ERROR'|'TRACE'|'DEBUG'|'INFO'|'WARN'}} [options] customAttributes defaults to `{}` if not assigned, level defaults to `info` if not assigned.
190
+ */
191
+ log (message, options) {
192
+ return this.#callMethod('logInfo', message, options)
193
+ }
194
+
195
+ /**
196
+ * Wrap a logger function to capture a log each time the function is invoked with the message and arguments passed
197
+ * {@link https://docs.newrelic.com/docs/browser/new-relic-browser/browser-apis/wraplogger/}
198
+ * @param {object} parent The parent object containing the logger method
199
+ * @param {string} functionName The property name of the function in the parent object to be wrapped
200
+ * @param {{customAttributes?: object, level?: 'ERROR'|'TRACE'|'DEBUG'|'INFO'|'WARN'}} [options] customAttributes defaults to `{}` if not assigned, level defaults to `info` if not assigned.
201
+ */
202
+ wrapLogger (parent, functionName, options) {
203
+ return this.#callMethod('wrapLogger', parent, functionName, options)
204
+ }
184
205
  }
@@ -4,7 +4,7 @@ export const apiMethods = [
4
4
  'setErrorHandler', 'finished', 'addToTrace', 'addRelease',
5
5
  'addPageAction', 'setCurrentRouteName', 'setPageViewName', 'setCustomAttribute',
6
6
  'interaction', 'noticeError', 'setUserId', 'setApplicationVersion', 'start',
7
- SR_EVENT_EMITTER_TYPES.RECORD, SR_EVENT_EMITTER_TYPES.PAUSE
7
+ SR_EVENT_EMITTER_TYPES.RECORD, SR_EVENT_EMITTER_TYPES.PAUSE, 'log', 'wrapLogger'
8
8
  ]
9
9
 
10
10
  export const asyncApiMethods = [
@@ -16,6 +16,9 @@ import { apiMethods, asyncApiMethods } from './api-methods'
16
16
  import { SR_EVENT_EMITTER_TYPES } from '../../features/session_replay/constants'
17
17
  import { now } from '../../common/timing/now'
18
18
  import { MODE } from '../../common/session/constants'
19
+ import { LOGGING_FAILURE_MESSAGE, LOGGING_IGNORED, LOGGING_LEVEL_FAILURE_MESSAGE, LOG_LEVELS, MAX_PAYLOAD_SIZE } from '../../features/logging/constants'
20
+ import { bufferLog, isValidLogLevel } from '../../features/logging/shared/utils'
21
+ import { wrapLogger } from '../../common/wrap/wrap-logger'
19
22
 
20
23
  export function setTopLevelCallers () {
21
24
  const nr = gosCDN()
@@ -51,6 +54,21 @@ export function setAPI (agentIdentifier, forceDrain, runSoftNavOverSpa = false)
51
54
  var prefix = 'api-'
52
55
  var spaPrefix = prefix + 'ixn-'
53
56
 
57
+ apiInterface.log = function (message, { customAttributes = {}, level = LOG_LEVELS.INFO } = {}) {
58
+ if (!customAttributes || typeof customAttributes !== 'object') customAttributes = {}
59
+ if (typeof message !== 'string' || !message) return warn(LOGGING_IGNORED + 'invalid message')
60
+ if (!isValidLogLevel(level)) return warn(LOGGING_LEVEL_FAILURE_MESSAGE + level, LOG_LEVELS)
61
+ if (message.length > MAX_PAYLOAD_SIZE) return warn(LOGGING_IGNORED + '> ' + MAX_PAYLOAD_SIZE + ' bytes: ', message.slice(0, 25) + '...')
62
+ bufferLog(instanceEE, message, customAttributes, level.toUpperCase())
63
+ }
64
+
65
+ apiInterface.wrapLogger = (parent, functionName, { customAttributes = {}, level = LOG_LEVELS.INFO } = {}) => {
66
+ if (!customAttributes || typeof customAttributes !== 'object') customAttributes = {}
67
+ if (!(typeof parent === 'object' && !!parent && typeof functionName === 'string' && !!functionName && typeof parent[functionName] === 'function' && typeof customAttributes === 'object')) return warn(LOGGING_FAILURE_MESSAGE + 'invalid argument(s)')
68
+ if (!isValidLogLevel(level)) return warn(LOGGING_FAILURE_MESSAGE + LOGGING_LEVEL_FAILURE_MESSAGE + level, LOG_LEVELS)
69
+ wrapLogger(instanceEE, parent, functionName, { customAttributes, level: level.toUpperCase() })
70
+ }
71
+
54
72
  // Setup stub functions that queue calls for later processing.
55
73
  asyncApiMethods.forEach(fnName => { apiInterface[fnName] = apiCall(prefix, fnName, true, 'api') })
56
74
 
@@ -66,7 +84,7 @@ export function setAPI (agentIdentifier, forceDrain, runSoftNavOverSpa = false)
66
84
  /**
67
85
  * Attach the key-value attribute onto agent payloads. All browser events in NR will be affected.
68
86
  * @param {string} key
69
- * @param {string|number|null} value - null indicates the key should be removed or erased
87
+ * @param {string|number|boolean|null} value - null indicates the key should be removed or erased
70
88
  * @param {string} apiName
71
89
  * @param {boolean} addToBrowserStorage - whether this attribute should be stored in browser storage API and retrieved by the next agent context or initialization
72
90
  * @returns @see apiCall
@@ -1,6 +1,7 @@
1
1
  export const FEATURE_NAMES = {
2
2
  ajax: 'ajax',
3
3
  jserrors: 'jserrors',
4
+ logging: 'logging',
4
5
  metrics: 'metrics',
5
6
  pageAction: 'page_action',
6
7
  pageViewEvent: 'page_view_event',
@@ -20,10 +21,11 @@ export const featurePriority = {
20
21
  [FEATURE_NAMES.pageViewTiming]: 2,
21
22
  [FEATURE_NAMES.metrics]: 3,
22
23
  [FEATURE_NAMES.jserrors]: 4,
23
- [FEATURE_NAMES.ajax]: 5,
24
- [FEATURE_NAMES.sessionTrace]: 6,
25
- [FEATURE_NAMES.pageAction]: 7,
26
- [FEATURE_NAMES.spa]: 8,
24
+ [FEATURE_NAMES.spa]: 5,
25
+ [FEATURE_NAMES.ajax]: 6,
26
+ [FEATURE_NAMES.sessionTrace]: 7,
27
+ [FEATURE_NAMES.pageAction]: 8,
27
28
  [FEATURE_NAMES.softNav]: 9,
28
- [FEATURE_NAMES.sessionReplay]: 10
29
+ [FEATURE_NAMES.sessionReplay]: 10,
30
+ [FEATURE_NAMES.logging]: 11
29
31
  }