newrelic 9.13.0 → 9.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/NEWS.md CHANGED
@@ -1,3 +1,7 @@
1
+ ### v9.14.0 (2023-03-23)
2
+
3
+ * Added new API function called `setErrorGroupCallback`, which provides a way for you to customize the `error.group.name` attribute of errors that are captured by the agent. This attribute controls how the Errors Inbox functionality groups similar errors together. To learn more about this function, please refer to our [example app](https://github.com/newrelic/newrelic-node-examples).
4
+
1
5
  ### v9.13.0 (2023-03-20)
2
6
 
3
7
  * Updated http instrumentation to no longer remove the `x-new-relic-disable-dt` header when using AWS SDK v3. This was done to prevent the "The request signature we calculated does not match the signature you provided. Check your key and signing method." error from AWS SDK.
package/api.js CHANGED
@@ -1739,4 +1739,51 @@ function _filterAttributes(attributes, name) {
1739
1739
  return filteredAttributes
1740
1740
  }
1741
1741
 
1742
+ /**
1743
+ * Function for adding a custom callback to generate Error Group names, which
1744
+ * will be used by the Errors Inbox to group similar errors together via the `error.group.name`
1745
+ * agent attribute.
1746
+ *
1747
+ * Provided functions must return a string, and receive an object as an argument,
1748
+ * which contains information related to the Error that occurred, and has the
1749
+ * following format:
1750
+ *
1751
+ * ```
1752
+ * {
1753
+ * customAttributes: object,
1754
+ * 'request.uri': string,
1755
+ * 'http.statusCode': string,
1756
+ * 'http.method': string,
1757
+ * error: Error,
1758
+ * 'error.expected': boolean
1759
+ * }
1760
+ * ```
1761
+ *
1762
+ * Calling this function multiple times will replace previously defined functions
1763
+ *
1764
+ * @param {Function} callback - callback function to generate `error.group.name` attribute
1765
+ * @example
1766
+ * function myCallback(metadata) {
1767
+ * if (metadata['http.statusCode'] === '400') {
1768
+ * return 'Bad User Input'
1769
+ * }
1770
+ * }
1771
+ * newrelic.setErrorGroupCallback(myCallback)
1772
+ */
1773
+ API.prototype.setErrorGroupCallback = function setErrorGroupCallback(callback) {
1774
+ const metric = this.agent.metrics.getOrCreateMetric(
1775
+ NAMES.SUPPORTABILITY.API + '/setErrorGroupCallback'
1776
+ )
1777
+ metric.incrementCallCount()
1778
+
1779
+ if (!this.shim.isFunction(callback) || this.shim.isPromise(callback)) {
1780
+ logger.warn(
1781
+ 'Error Group callback must be a synchronous function, Error Group attribute will not be added'
1782
+ )
1783
+ return
1784
+ }
1785
+
1786
+ this.agent.errors.errorGroupCallback = callback
1787
+ }
1788
+
1742
1789
  module.exports = API
@@ -36,6 +36,8 @@ class ErrorCollector {
36
36
  this.seenStringsByTransaction = Object.create(null)
37
37
 
38
38
  this.traceAggregator.on('starting error_data data send.', this._onSendErrorTrace.bind(this))
39
+
40
+ this.errorGroupCallback = null
39
41
  }
40
42
 
41
43
  _onSendErrorTrace() {
@@ -330,6 +332,10 @@ class ErrorCollector {
330
332
  return false
331
333
  }
332
334
 
335
+ if (this.errorGroupCallback) {
336
+ exception.errorGroupCallback = this.errorGroupCallback
337
+ }
338
+
333
339
  const errorTrace = createError(transaction, exception, this.config)
334
340
  this._maybeRecordErrorMetrics(errorTrace, transaction)
335
341
 
@@ -5,6 +5,7 @@
5
5
 
6
6
  'use strict'
7
7
 
8
+ const logger = require('../logger').child({ component: 'errors_lib' })
8
9
  const DESTINATIONS = require('../config/attribute-filter').DESTINATIONS
9
10
  const props = require('../util/properties')
10
11
  const urltils = require('../util/urltils')
@@ -25,6 +26,7 @@ class Exception {
25
26
  this.customAttributes = customAttributes || {}
26
27
  this.agentAttributes = agentAttributes || {}
27
28
  this._expected = expected
29
+ this.errorGroupCallback = null
28
30
  }
29
31
 
30
32
  getErrorDetails(config) {
@@ -107,9 +109,44 @@ function createError(transaction, exception, config) {
107
109
  params.intrinsics[ERROR_EXPECTED_PATH] =
108
110
  exception._expected || errorHelper.isExpected(type, message, transaction, config, urltils)
109
111
 
112
+ maybeAddAgentAttributes(params, exception)
113
+
110
114
  return [0, name, message, type, params]
111
115
  }
112
116
 
117
+ function isValidErrorGroupOutput(output) {
118
+ return (typeof output === 'string' || output instanceof String) && output !== ''
119
+ }
120
+
121
+ function maybeAddAgentAttributes(attributes, exception) {
122
+ if (exception.errorGroupCallback) {
123
+ const callbackInput = {
124
+ 'error': exception.error,
125
+ 'customAttributes': Object.assign({}, attributes.userAttributes),
126
+ 'request.uri': attributes.agentAttributes['request.uri'],
127
+ 'http.statusCode': attributes.agentAttributes['http.statusCode'],
128
+ 'http.method': attributes.agentAttributes['request.method'],
129
+ 'error.expected': attributes.intrinsics[ERROR_EXPECTED_PATH]
130
+ }
131
+
132
+ try {
133
+ const callbackOutput = exception.errorGroupCallback(callbackInput)
134
+
135
+ if (!isValidErrorGroupOutput(callbackOutput)) {
136
+ logger.warn('Function provided via setErrorGroupCallback return value malformed')
137
+ return
138
+ }
139
+
140
+ attributes.agentAttributes['error.group.name'] = callbackOutput
141
+ } catch (err) {
142
+ logger.warn(
143
+ err,
144
+ 'Function provided via setErrorGroupCallback failed, not generating `error.group.name`'
145
+ )
146
+ }
147
+ }
148
+ }
149
+
113
150
  function maybeAddUserAttributes(userAttributes, exception, config) {
114
151
  const customAttributes = exception.customAttributes
115
152
  if (!config.high_security && config.api.custom_attributes_enabled && customAttributes) {
@@ -5,8 +5,6 @@
5
5
 
6
6
  'use strict'
7
7
 
8
- /* eslint sonarjs/cognitive-complexity: ["error", 17] -- TODO: https://issues.newrelic.com/browse/NEWRELIC-5252 */
9
-
10
8
  const logger = require('../../logger').child({ component: 'tx_segment_normalizer' })
11
9
 
12
10
  module.exports = TxSegmentNormalizer
@@ -17,7 +15,7 @@ function TxSegmentNormalizer() {
17
15
 
18
16
  /**
19
17
  * This normalize method is wicked. The best bet is to read the spec:
20
- * https://newrelic.atlassian.net/wiki/pages/viewpage.action?spaceKey=eng&title=Language+agent+transaction+segment+terms+rules
18
+ * https://source.datanerd.us/agents/agent-specs/blob/main/Metric-Name-Rules-PORTED.md
21
19
  *
22
20
  * A copy paste of the rules that were followed:
23
21
  * 1. Find the first rule where the prefix key matches the prefix of the
@@ -31,11 +29,28 @@ function TxSegmentNormalizer() {
31
29
  * 6. Join together the modified segments with slashes, and re-prepend the prefix.
32
30
  *
33
31
  * @param {string} path - The transaction metric path to normalize.
34
- * @returns {NormalizationResults} - The results of normalizing the given path.
32
+ * @returns {{ignore: boolean, matched: boolean, value: string}} - The results of normalizing the given path.
35
33
  */
36
34
  TxSegmentNormalizer.prototype.normalize = function normalize(path) {
37
35
  let currentTerm
38
36
  let prefix
37
+ let result
38
+ let prev
39
+
40
+ const normalizeParts = (part, idx, parts) => {
41
+ if (part === '' && idx + 1 === parts.length) {
42
+ return // if this is the last one, don't iterate
43
+ }
44
+ if (currentTerm.terms.indexOf(part) === -1) {
45
+ if (prev === '*') {
46
+ return
47
+ }
48
+ result.push((prev = '*'))
49
+ } else {
50
+ result.push((prev = part))
51
+ }
52
+ }
53
+
39
54
  for (let i = 0; i < this.terms.length; i++) {
40
55
  currentTerm = this.terms[i]
41
56
  prefix = currentTerm.prefix
@@ -44,26 +59,10 @@ TxSegmentNormalizer.prototype.normalize = function normalize(path) {
44
59
  }
45
60
  const fragment = path.slice(prefix.length)
46
61
  const parts = fragment.split('/')
47
- const result = []
48
- let prev
49
- let segment
50
-
51
- for (let j = 0; j < parts.length; j++) {
52
- segment = parts[j]
62
+ result = []
53
63
 
54
- if (segment === '' && j + 1 === parts.length) {
55
- break
56
- }
64
+ parts.forEach(normalizeParts)
57
65
 
58
- if (currentTerm.terms.indexOf(segment) === -1) {
59
- if (prev === '*') {
60
- continue
61
- }
62
- result.push((prev = '*'))
63
- } else {
64
- result.push((prev = segment))
65
- }
66
- }
67
66
  logger.trace('Normalizing %s because of rule: %s', path, currentTerm)
68
67
  return {
69
68
  matched: true, // To match MetricNormalizer
package/lib/shimmer.js CHANGED
@@ -352,33 +352,15 @@ const shimmer = (module.exports = {
352
352
  shimmer.unwrapMethod(Module, 'Module', '_load')
353
353
  },
354
354
 
355
- bootstrapInstrumentation: function bootstrapInstrumentation(agent) {
356
- // Instrument global.
357
- const globalShim = new shims.Shim(agent, 'globals', 'globals')
358
- applyDebugState(globalShim, global)
359
- const globalsFilepath = path.join(__dirname, 'instrumentation', 'core', 'globals.js')
360
- _firstPartyInstrumentation(agent, globalsFilepath, globalShim, global, 'globals')
361
-
362
- // Instrument each of the core modules.
363
- Object.keys(CORE_INSTRUMENTATION).forEach(function forEachCore(mojule) {
364
- const core = CORE_INSTRUMENTATION[mojule]
365
- const filePath = path.join(__dirname, 'instrumentation', 'core', core.file)
366
- let uninstrumented = null
367
-
368
- try {
369
- uninstrumented = require(mojule)
370
- } catch (err) {
371
- logger.trace('Could not load core module %s got error %s', mojule, err)
372
- }
373
-
374
- const shim = shims.createShimFromType(core.type, agent, mojule, mojule)
375
- applyDebugState(shim, core)
376
- _firstPartyInstrumentation(agent, filePath, shim, uninstrumented, mojule)
377
- })
378
-
379
- // Register all the first-party instrumentations.
380
- Object.keys(INSTRUMENTATIONS).forEach(function forEachInstrumentation(moduleName) {
381
- const instrInfo = INSTRUMENTATIONS[moduleName]
355
+ /**
356
+ * Registers all instrumentation for "first-party" libraries.
357
+ *
358
+ * This is all 3rd party libs with the exception of the domain library in Node.js core
359
+ *
360
+ * @param {object} agent NR agent
361
+ */
362
+ registerFirstPartyInstrumentation(agent) {
363
+ for (const [moduleName, instrInfo] of Object.entries(INSTRUMENTATIONS)) {
382
364
  if (instrInfo.module) {
383
365
  // Because external instrumentations can change independent of
384
366
  // the agent core, we don't want breakages in them to entirely
@@ -401,7 +383,7 @@ const shimmer = (module.exports = {
401
383
  onRequire: _firstPartyInstrumentation.bind(null, agent, fileName)
402
384
  })
403
385
  }
404
- })
386
+ }
405
387
 
406
388
  // Even though domain is a core module we add it as a registered
407
389
  // instrumentation to be lazy-loaded because we do not want to cause domain
@@ -414,6 +396,40 @@ const shimmer = (module.exports = {
414
396
  })
415
397
  },
416
398
 
399
+ /**
400
+ * Registers all instrumentation for Node.js core libraries.
401
+ *
402
+ * @param {object} agent NR agent
403
+ */
404
+ registerCoreInstrumentation(agent) {
405
+ // Instrument global.
406
+ const globalShim = new shims.Shim(agent, 'globals', 'globals')
407
+ applyDebugState(globalShim, global)
408
+ const globalsFilepath = path.join(__dirname, 'instrumentation', 'core', 'globals.js')
409
+ _firstPartyInstrumentation(agent, globalsFilepath, globalShim, global, 'globals')
410
+
411
+ // Instrument each of the core modules.
412
+ for (const [mojule, core] of Object.entries(CORE_INSTRUMENTATION)) {
413
+ const filePath = path.join(__dirname, 'instrumentation', 'core', core.file)
414
+ let uninstrumented = null
415
+
416
+ try {
417
+ uninstrumented = require(mojule)
418
+ } catch (err) {
419
+ logger.trace('Could not load core module %s got error %s', mojule, err)
420
+ }
421
+
422
+ const shim = shims.createShimFromType(core.type, agent, mojule, mojule)
423
+ applyDebugState(shim, core)
424
+ _firstPartyInstrumentation(agent, filePath, shim, uninstrumented, mojule)
425
+ }
426
+ },
427
+
428
+ bootstrapInstrumentation: function bootstrapInstrumentation(agent) {
429
+ shimmer.registerCoreInstrumentation(agent)
430
+ shimmer.registerFirstPartyInstrumentation(agent)
431
+ },
432
+
417
433
  registerInstrumentation: function registerInstrumentation(opts) {
418
434
  if (!hasValidRegisterOptions(opts)) {
419
435
  return
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "9.13.0",
3
+ "version": "9.14.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [