newrelic 6.4.2 → 6.5.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.
@@ -1,4 +1,4 @@
1
- {
1
+ module.exports = {
2
2
  "env": {
3
3
  "es6": true,
4
4
  "node": true,
@@ -7,9 +7,7 @@
7
7
  "parserOptions": {
8
8
  "ecmaVersion": 6
9
9
  },
10
- "plugins": [
11
- "mocha-no-only"
12
- ],
10
+ "ignorePatterns": ["invalid-json/"],
13
11
  "rules": {
14
12
  "indent": ["warn", 2, {"SwitchCase": 1}],
15
13
  "brace-style": "error",
@@ -27,7 +25,6 @@
27
25
  "max-len": ["error", 100, { "ignoreUrls": true }],
28
26
  "max-nested-callbacks": ["error", 3],
29
27
  "max-params": ["error", 5],
30
- "mocha-no-only/mocha-no-only": ["error"],
31
28
  "new-cap": "error",
32
29
  "no-console": "warn",
33
30
  "no-debugger": "error",
package/.travis.yml CHANGED
@@ -18,7 +18,6 @@ env:
18
18
  - NR_NODE_VERSION=12 SUITE=unit
19
19
  - NR_NODE_VERSION=12 SUITE=integration
20
20
  - NR_NODE_VERSION=12 SUITE=lint
21
- - NR_NODE_VERSION=12 SUITE=security
22
21
  - NR_NODE_VERSION=12 SUITE=versioned
23
22
  services:
24
23
  - memcached
package/NEWS.md CHANGED
@@ -1,3 +1,27 @@
1
+ ### 6.5.0 (2020-03-18):
2
+
3
+ * Added error attributes to spans.
4
+ * The public api method `noticeError()` now attaches exception details to the currently executing
5
+ span. Spans with error details are now highlighted red in the Distributed Tracing UI. Also, the
6
+ attributes `error.class` and `error.message` are added to the span. If multiple errors are
7
+ recorded for a single span, only the final error's attributes will be added to the span.
8
+
9
+ * Added ID of the span in which an error occurred to the corresponding transaction error event.
10
+
11
+ * Added new public API methods `addCustomSpanAttribute` and `addCustomSpanAttributes` to add
12
+ attributes to the currently executing span.
13
+
14
+ * Added new attributes to http outbound spans: `http.statusCode` and `http.statusText`.
15
+
16
+ * Updated W3C Trace Context "Known Issues and Workaround" notes with latest accurate consideration.
17
+
18
+ * Converted unit tests to run via `tap`. Removes `mocha` dependency.
19
+
20
+ * Fixed route naming when hapi's `pre` route handlers share functions.
21
+
22
+ * Fixed `child_process` instrumentation so that handlers can be effectively removed
23
+ when attached via `.once()` or manually removed via `removeListener()`.
24
+
1
25
  ### 6.4.2 (2020-02-27):
2
26
 
3
27
  * Support new http[s] get/request function signature in Node v10+
@@ -92,7 +116,9 @@
92
116
 
93
117
  Known Issues and Workarounds
94
118
 
95
- * If a .NET agent is initiating traces as the root service, do not upgrade your downstream Node New Relic agents to this agent release.
119
+ * If a .NET agent is initiating distributed traces as the root service, you must update
120
+ that .NET agent to version `8.24` or later before upgrading your downstream Node
121
+ New Relic agents to this agent release.
96
122
 
97
123
  * Pins Node 12 version to `v12.15` to avoid breakages with `v12.16.0` until cause(s)
98
124
  resolved.
package/api.js CHANGED
@@ -340,6 +340,77 @@ API.prototype.addCustomAttributes = function addCustomAttributes(atts) {
340
340
  }
341
341
  }
342
342
 
343
+ /**
344
+ * Add custom span attributes in an object to the current segment/span.
345
+ *
346
+ * See documentation for newrelic.addCustomSpanAttribute for more information.
347
+ *
348
+ * An example of setting a custom span attribute:
349
+ *
350
+ * newrelic.addCustomSpanAttribute({test: 'value', test2: 'value2'})
351
+ *
352
+ * @param {object} [atts]
353
+ * @param {string} [atts.KEY] The name you want displayed in the RPM UI.API.
354
+ * @param {string} [atts.KEY.VALUE] The value you want displayed. Must be serializable.
355
+ */
356
+ API.prototype.addCustomSpanAttributes = function addCustomSpanAttributes(atts) {
357
+ const metric = this.agent.metrics.getOrCreateMetric(
358
+ NAMES.SUPPORTABILITY.API + '/addCustomSpanAttributes'
359
+ )
360
+ metric.incrementCallCount()
361
+
362
+ for (let key in atts) {
363
+ if (properties.hasOwn(atts, key)) {
364
+ this.addCustomSpanAttribute(key, atts[key])
365
+ }
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Add a custom span attribute to the current transaction. Some attributes
371
+ * are reserved (see CUSTOM_DENYLIST for the current, very short list), and
372
+ * as with most API methods, this must be called in the context of an
373
+ * active segment/span. Most recently set value wins.
374
+ *
375
+ * @param {string} key The key you want displayed in the RPM UI.
376
+ * @param {string} value The value you want displayed. Must be serializable.
377
+ */
378
+ API.prototype.addCustomSpanAttribute = function addCustomSpanAttribute(key, value) {
379
+ const metric = this.agent.metrics.getOrCreateMetric(
380
+ NAMES.SUPPORTABILITY.API + '/addCustomSpanAttribute'
381
+ )
382
+ metric.incrementCallCount()
383
+
384
+ // If high security mode is on, custom attributes are disabled.
385
+ if (this.agent.config.high_security) {
386
+ logger.warnOnce(
387
+ 'Custom span attributes',
388
+ 'Custom span attributes are disabled by high security mode.'
389
+ )
390
+ return false
391
+ } else if (!this.agent.config.api.custom_attributes_enabled) {
392
+ logger.debug(
393
+ 'Config.api.custom_attributes_enabled set to false, not collecting value'
394
+ )
395
+ return false
396
+ }
397
+
398
+ const segment = this.agent.tracer.getSegment()
399
+
400
+ if (!segment) {
401
+ return logger.debug(
402
+ 'Could not add attribute %s. No available span/segment.',
403
+ key
404
+ )
405
+ }
406
+
407
+ if (CUSTOM_DENYLIST.has(key)) {
408
+ return logger.warn('Not overwriting value of NR-only attribute %s.', key)
409
+ }
410
+
411
+ segment.addCustomSpanAttribute(key, value)
412
+ }
413
+
343
414
  API.prototype.setIgnoreTransaction = util.deprecate(
344
415
  setIgnoreTransaction, [
345
416
  'API#setIgnoreTransaction is being deprecated!',
@@ -415,7 +486,6 @@ API.prototype.noticeError = function noticeError(error, customAttributes) {
415
486
  if (typeof error === 'string') {
416
487
  error = new Error(error)
417
488
  }
418
- const transaction = this.agent.tracer.getTransaction()
419
489
 
420
490
  // Filter all object type valued attributes out
421
491
  let filteredAttributes = customAttributes
@@ -423,6 +493,7 @@ API.prototype.noticeError = function noticeError(error, customAttributes) {
423
493
  filteredAttributes = _filterAttributes(customAttributes, 'noticeError')
424
494
  }
425
495
 
496
+ const transaction = this.agent.tracer.getTransaction()
426
497
  this.agent.errors.addUserError(transaction, error, filteredAttributes)
427
498
  }
428
499
 
package/lib/attributes.js CHANGED
@@ -6,6 +6,8 @@ const isValidType = require('./util/attribute-types')
6
6
  const byteUtils = require('./util/byte-limit')
7
7
  const properties = require('./util/properties')
8
8
 
9
+ const MAXIMUM_CUSTOM_ATTRIBUTES = 64
10
+
9
11
  /**
10
12
  * @class
11
13
  * @private
@@ -23,6 +25,7 @@ class Attributes {
23
25
  this.filter = makeFilter(scope)
24
26
  this.limit = limit
25
27
  this.attributes = Object.create(null)
28
+ this.attributeCount = 0
26
29
  }
27
30
 
28
31
  /**
@@ -57,7 +60,6 @@ class Attributes {
57
60
  */
58
61
  get(dest) {
59
62
  const attrs = Object.create(null)
60
- let attrCount = 0
61
63
  for (let key in this.attributes) { // eslint-disable-line guard-for-in
62
64
  const attr = this.attributes[key]
63
65
  if (!(attr.destinations & dest)) {
@@ -67,10 +69,6 @@ class Attributes {
67
69
  attrs[key] = typeof attr.value === 'string' && !attr.truncateExempt
68
70
  ? byteUtils.truncate(attr.value, 255)
69
71
  : attr.value
70
-
71
- if (++attrCount >= this.limit) {
72
- break
73
- }
74
72
  }
75
73
 
76
74
  return attrs
@@ -103,6 +101,13 @@ class Attributes {
103
101
  * @param {boolean} [truncateExempt=false] - Flag marking value exempt from truncation
104
102
  */
105
103
  addAttribute(destinations, key, value, truncateExempt = false) {
104
+ if (this.attributeCount + 1 > this.limit) {
105
+ return logger.debug(
106
+ `Maximum number of custom attributes have been added.
107
+ Dropping attribute ${key} with ${value} type.`
108
+ )
109
+ }
110
+
106
111
  if (!isValidType(value)) {
107
112
  return logger.debug(
108
113
  'Not adding attribute %s with %s value type. This is expected for undefined' +
@@ -123,6 +128,7 @@ class Attributes {
123
128
  // Only set the attribute if at least one destination passed
124
129
  const validDestinations = this.filter(destinations, key)
125
130
  if (validDestinations) {
131
+ this.attributeCount = this.attributeCount + 1
126
132
  this._set(validDestinations, key, value, truncateExempt)
127
133
  }
128
134
  }
@@ -159,4 +165,7 @@ function makeFilter(scope) {
159
165
  }
160
166
  }
161
167
 
162
- module.exports = Attributes
168
+ module.exports = {
169
+ Attributes: Attributes,
170
+ MAXIMUM_CUSTOM_ATTRIBUTES: MAXIMUM_CUSTOM_ATTRIBUTES
171
+ }
@@ -1458,8 +1458,12 @@ Config.prototype.applyLasp = function applyLasp(agent, policies) {
1458
1458
  }
1459
1459
  var valueName = splitConfigName[splitConfigName.length - 1]
1460
1460
  var localVal = settingBlock[valueName]
1461
+
1462
+ // Indexes into "allowed values" based on "enabled" setting
1463
+ // to retreive proper mapping.
1461
1464
  var policyValues = localMapping.allowedValues
1462
1465
  var policyValue = policyValues[policy.enabled ? 1 : 0]
1466
+
1463
1467
  // get the most secure setting between local config and the policy
1464
1468
  var finalValue = settingBlock[valueName] = config._getMostSecure(
1465
1469
  name,
@@ -1,12 +1,25 @@
1
1
  'use strict'
2
2
 
3
+ // TODO: would likely be easier to understand if the allowedValues mapping
4
+ // just took the raw enabled/disabled and translated. This is not a hot path.
5
+
6
+ /**
7
+ * path: Full nested path for the local configuration item.
8
+ * allowedValues:
9
+ * Array of valid config values to map for incoming enabled/disabled.
10
+ * policy.enabled: false uses index 0, policy.enabled: true uses index 1.
11
+ * filter: Allows for calculating most secure setting to use
12
+ * applyAdditionalSettings: Applies additional settings that are required
13
+ * when the policy is disabled.
14
+ * clearData: Clears the relevant agent collection.
15
+ */
3
16
  const LASP_MAP = {
4
17
  // LASP key
5
18
  record_sql: {
6
19
  // full path to corresponding config key
7
20
  path: 'transaction_tracer.record_sql',
8
21
  // Mapping from policy enabled status to usable config value
9
- // first element is policy is off, second is policy is on
22
+ // policy.enabled: false === off, policy.enabled: true === 'obfuscated'
10
23
  allowedValues: ['off', 'obfuscated'],
11
24
  // Tracks the precedent of settings controlled by LASP.
12
25
  filter: function mostSecureRecordSQL(first, second) {
@@ -45,6 +58,8 @@ const LASP_MAP = {
45
58
  // TODO: rename config key, because the names contradict each other's behavior
46
59
  allow_raw_exception_messages: {
47
60
  path: 'strip_exception_messages.enabled',
61
+ // if raw messages are allowed, then we should not strip them
62
+ // policy.enabled: false === true, policy.enabled: true === false
48
63
  allowedValues: [true, false],
49
64
  filter: function mostSecureStripException(first, second) {
50
65
  return first || second
@@ -4,6 +4,7 @@ const errorsModule = require('./index')
4
4
 
5
5
  const logger = require('../logger').child({component: 'error_tracer'})
6
6
  const urltils = require('../util/urltils')
7
+ const Exception = require('../errors').Exception
7
8
  const errorHelper = require('./helper')
8
9
  const createError = errorsModule.createError
9
10
  const createEvent = errorsModule.createEvent
@@ -129,7 +130,7 @@ class ErrorCollector {
129
130
  if (transaction.userErrors.length) {
130
131
  for (let i = 0; i < transaction.userErrors.length; i++) {
131
132
  const exception = transaction.userErrors[i]
132
- if (this._collect(transaction, exception[0], exception[1], exception[2])) {
133
+ if (this.collect(transaction, exception)) {
133
134
  ++collectedErrors
134
135
  }
135
136
  }
@@ -145,13 +146,13 @@ class ErrorCollector {
145
146
  if (transaction.exceptions.length) {
146
147
  for (let i = 0; i < transaction.exceptions.length; i++) {
147
148
  const exception = transaction.exceptions[i]
148
- if (this.collect(transaction, exception[0], exception[1], exception[2])) {
149
+ if (this.collect(transaction, exception)) {
149
150
  ++collectedErrors
150
151
  // if we could collect it, then check if expected
151
152
  if (isExpectedErrorStatusCode ||
152
153
  errorHelper.isExpectedException(
153
154
  transaction,
154
- exception[0],
155
+ exception.error,
155
156
  this.config,
156
157
  urltils
157
158
  )
@@ -178,31 +179,32 @@ class ErrorCollector {
178
179
  }
179
180
 
180
181
  /**
181
- * This function collects the error right away when transaction is not supplied.
182
- * Otherwise it delays collecting the error until the transaction ends.
182
+ * This function collects the error right away when transaction is not supplied. Otherwise it
183
+ * delays collecting the error until the transaction ends.
183
184
  *
184
185
  * NOTE: this interface is unofficial and may change in future.
185
186
  *
186
- * @param {?Transaction} transaction
187
- * Transaction associated with the error.
188
- *
189
- * @param {Error} exception
190
- * The error to be traced.
191
- *
192
- * @param {object} customAttributes
193
- * Any custom attributes associated with the request (optional).
187
+ * @param {?Transaction} transaction Transaction associated with the error.
188
+ * @param {Error} error The error to be traced.
189
+ * @param {?object} customAttributes Custom attributes associated with the request (optional).
194
190
  */
195
- add(transaction, exception, customAttributes) {
196
- if (!exception) {
191
+ add(transaction, error, customAttributes) {
192
+ if (!error) {
193
+ return
194
+ }
195
+
196
+ if (errorHelper.shouldIgnoreError(transaction, error, this.config)) {
197
+ logger.trace('Ignoring error')
197
198
  return
198
199
  }
199
200
 
200
201
  const timestamp = Date.now()
202
+ const exception = new Exception({error, timestamp, customAttributes})
201
203
 
202
204
  if (transaction) {
203
- transaction.addException(exception, customAttributes, timestamp)
205
+ transaction.addException(exception)
204
206
  } else {
205
- this.collect(transaction, exception, customAttributes, timestamp)
207
+ this.collect(transaction, exception)
206
208
  }
207
209
  }
208
210
 
@@ -217,95 +219,54 @@ class ErrorCollector {
217
219
  *
218
220
  * NOTE: this interface is unofficial and may change in future.
219
221
  *
220
- * @param {?Transaction} transaction
221
- * Transaction associated with the error.
222
- *
223
- * @param {Error} exception
224
- * The error to be traced.
225
- *
226
- * @param {object} [customAttributes=null]
227
- * Any custom attributes associated with the request (optional).
222
+ * @param {?Transaction} transaction Transaction associated with the error.
223
+ * @param {Exception} exception The Exception to be traced.
228
224
  */
229
- addUserError(transaction, exception, customAttributes) {
230
- if (!exception) return
225
+ addUserError(transaction, error, customAttributes) {
226
+ if (!error) return
231
227
 
232
- var timestamp = Date.now()
228
+ const timestamp = Date.now()
229
+ const exception = new Exception({error, timestamp, customAttributes})
233
230
 
234
231
  if (transaction) {
235
- transaction.addUserError(exception, customAttributes, timestamp)
232
+ transaction.addUserError(exception)
236
233
  } else {
237
- this._collect(transaction, exception, customAttributes, timestamp)
238
- }
239
- }
240
-
241
- /**
242
- * Wrapper for _collect, include logic for whether an error should
243
- * be ignored or not. Exists to allow userErrors to bypass ignore
244
- * logic.
245
- *
246
- * NOTE: this interface is unofficial and may change in future.
247
- *
248
- * @param {?Transaction} transaction
249
- * Transaction associated with the error.
250
- *
251
- * @param {?Error} exception
252
- * The error to be traced.
253
- *
254
- * @param {?object} customAttributes
255
- * Any custom attributes associated with the request.
256
- *
257
- * @param {number} timestamp
258
- *
259
- * @return {bool} True if the error was collected.
260
- */
261
- collect(transaction, exception, customAttributes, timestamp) {
262
- if (errorHelper.shouldIgnoreError(transaction, exception, this.config)) {
263
- logger.trace("Ignoring error")
264
- return
234
+ this.collect(transaction, exception)
265
235
  }
266
- return this._collect(transaction, exception, customAttributes, timestamp)
267
236
  }
268
237
 
269
238
  /**
270
239
  * Collects the error and also creates the error event.
271
240
  *
272
- * @private
273
- *
274
- * This function uses an array of seen exceptions to ensure errors don't get
275
- * double-counted. It can also be used as an unofficial means of marking that
276
- * user errors shouldn't be traced.
241
+ * This function uses an array of seen exceptions to ensure errors don't get double-counted. It
242
+ * can also be used as an unofficial means of marking that user errors shouldn't be traced.
277
243
  *
278
- * For an error to be traced, at least one of the transaction or the error
279
- * must be present.
244
+ * For an error to be traced, at least one of the transaction or the error must be present.
280
245
  *
281
246
  * NOTE: this interface is unofficial and may change in future.
282
247
  *
283
- * @param {?Transaction} transaction
284
- * Transaction associated with the error.
285
- *
286
- * @param {?Error} exception
287
- * The error to be traced.
288
- *
289
- * @param {?object} customAttributes
290
- * Any custom attributes associated with the request.
291
- *
292
- * @param {number} timestamp
293
- *
294
- * @return {bool} True if the error was collected.
248
+ * @param {?Transaction} transaction Transaction associated with the error.
249
+ * @param {?Exception} exception The Exception object to be traced.
250
+ * @return {bool} True if the error was collected.
295
251
  */
296
- _collect(transaction, exception, customAttributes, timestamp) {
297
- if (exception) {
298
- if (this._haveSeen(transaction, exception)) {
252
+ collect(transaction, exception) {
253
+ if (!exception) {
254
+ exception = new Exception({})
255
+ }
256
+
257
+ if (exception.error) {
258
+ if (this._haveSeen(transaction, exception.error)) {
299
259
  return false
300
260
  }
301
261
 
302
- if (typeof exception !== 'string' && !exception.message && !exception.stack) {
303
- logger.trace(exception, 'Got error that is not an instance of Error or string.')
304
- exception = null
262
+ const error = exception.error
263
+ if (typeof error !== 'string' && !error.message && !error.stack) {
264
+ logger.trace(error, 'Got error that is not an instance of Error or string.')
265
+ exception.error = null
305
266
  }
306
267
  }
307
268
 
308
- if (!exception && (!transaction || !transaction.statusCode || transaction.error)) {
269
+ if (!exception.error && (!transaction || !transaction.statusCode || transaction.error)) {
309
270
  return false
310
271
  }
311
272
 
@@ -321,13 +282,13 @@ class ErrorCollector {
321
282
  return false
322
283
  }
323
284
 
324
- if (exception) {
325
- logger.trace(exception, 'Got exception to trace:')
285
+ if (exception.error) {
286
+ logger.trace(exception.error, 'Got exception to trace:')
326
287
  }
327
288
 
328
- const error = createError(transaction, exception, customAttributes, this.config)
289
+ const errorTrace = createError(transaction, exception, this.config)
329
290
 
330
- const isExpectedError = true === error[4].intrinsics['error.expected']
291
+ const isExpectedError = true === errorTrace[4].intrinsics['error.expected']
331
292
 
332
293
  if (isExpectedError) {
333
294
  this.metrics.getOrCreateMetric(NAMES.ERRORS.EXPECTED).incrementCallCount()
@@ -343,11 +304,11 @@ class ErrorCollector {
343
304
  }
344
305
  }
345
306
 
346
- this.traceAggregator.add(error)
307
+ this.traceAggregator.add(errorTrace)
347
308
 
348
309
  if (this.config.error_collector.capture_events === true) {
349
310
  const priority = transaction && transaction.priority || Math.random()
350
- const event = createEvent(transaction, error, timestamp, this.config)
311
+ const event = createEvent(transaction, errorTrace, exception.timestamp, this.config)
351
312
  this.eventAggregator.add(event, priority)
352
313
  }
353
314
 
@@ -48,7 +48,7 @@ module.exports = {
48
48
 
49
49
  extractErrorInformation: function extractErrorInformation(
50
50
  transaction,
51
- exception,
51
+ error,
52
52
  config,
53
53
  urltils
54
54
  ) {
@@ -58,21 +58,21 @@ module.exports = {
58
58
 
59
59
  // String errors do not provide us with as much information to provide to the
60
60
  // user, but it is a common pattern.
61
- if (typeof exception === 'string') {
62
- message = exception
61
+ if (typeof error === 'string') {
62
+ message = error
63
63
  } else if (
64
- exception !== null &&
65
- typeof exception === 'object' &&
66
- exception.message &&
64
+ error !== null &&
65
+ typeof error === 'object' &&
66
+ error.message &&
67
67
  !config.high_security &&
68
68
  !config.strip_exception_messages.enabled
69
69
  ) {
70
- message = exception.message
70
+ message = error.message
71
71
 
72
- if (exception.name) {
73
- type = exception.name
74
- } else if (exception.constructor && exception.constructor.name) {
75
- type = exception.constructor.name
72
+ if (error.name) {
73
+ type = error.name
74
+ } else if (error.constructor && error.constructor.name) {
75
+ type = error.constructor.name
76
76
  }
77
77
  } else if (transaction && transaction.statusCode &&
78
78
  urltils.isError(config, transaction.statusCode)) {
@@ -95,9 +95,9 @@ module.exports = {
95
95
  }
96
96
  },
97
97
 
98
- shouldIgnoreError: function shouldIgnoreError(transaction, exception, config) {
98
+ shouldIgnoreError: function shouldIgnoreError(transaction, error, config) {
99
99
  // extract _just_ the error information, not transaction stuff
100
- let errorInfo = this.extractErrorInformation(null, exception, config, null)
100
+ let errorInfo = this.extractErrorInformation(null, error, config, null)
101
101
 
102
102
  return this.shouldIgnoreErrorClass(errorInfo, config) ||
103
103
  this.shouldIgnoreErrorMessage(errorInfo, config) ||
@@ -6,8 +6,18 @@ var props = require('../util/properties')
6
6
  var urltils = require('../util/urltils')
7
7
  const errorHelper = require('../errors/helper')
8
8
 
9
- module.exports.createError = createError
10
- module.exports.createEvent = createEvent
9
+ class Exception {
10
+ constructor({error, timestamp, customAttributes, agentAttributes}) {
11
+ this.error = error
12
+ this.timestamp = timestamp || 0
13
+ this.customAttributes = customAttributes || {}
14
+ this.agentAttributes = agentAttributes || {}
15
+ }
16
+
17
+ getErrorDetails(config) {
18
+ return errorHelper.extractErrorInformation(null, this.error, config)
19
+ }
20
+ }
11
21
 
12
22
  /**
13
23
  * Given either or both of a transaction and an exception, generate an error
@@ -17,21 +27,20 @@ module.exports.createEvent = createEvent
17
27
  * handler, which traps actual instances of Error, try to set sensible
18
28
  * defaults for everything.
19
29
  *
20
- * @param {Transaction} transaction The agent transaction, presumably
21
- * coming out of the instrumentation.
22
- * @param {Error} exception Something trapped by an error listener.
23
- * @param {object} customAttributes Any custom attributes associated with
24
- * the request (optional).
30
+ * @param {Transaction} transaction The agent transaction, coming from the instrumentatation
31
+ * @param {Exception} exception An custom Exception object with the error and other information
32
+ * @param {object} config The configuration to use when creating the object
25
33
  */
26
- function createError(transaction, exception, customAttributes, config) {
34
+ function createError(transaction, exception, config) {
35
+ const error = exception.error
27
36
  let {name, message, type} = errorHelper.extractErrorInformation(
28
37
  transaction,
29
- exception,
38
+ error,
30
39
  config,
31
40
  urltils
32
41
  )
33
42
 
34
- var params = {
43
+ let params = {
35
44
  userAttributes: Object.create(null),
36
45
  agentAttributes: Object.create(null),
37
46
  intrinsics: Object.create(null)
@@ -39,20 +48,26 @@ function createError(transaction, exception, customAttributes, config) {
39
48
 
40
49
  if (transaction) {
41
50
  // Copy all of the parameters off of the transaction.
42
- params.agentAttributes = transaction.trace.attributes.get(DESTINATIONS.ERROR_EVENT)
43
51
  params.intrinsics = transaction.getIntrinsicAttributes()
52
+ let transactionAgentAttributes = transaction.trace.attributes.get(DESTINATIONS.ERROR_EVENT)
53
+ transactionAgentAttributes = transactionAgentAttributes || {}
54
+
55
+ // Merge the agent attributes specific to this error event with the transaction attributes
56
+ const agentAttributes = Object.assign(exception.agentAttributes, transactionAgentAttributes)
57
+ params.agentAttributes = agentAttributes
44
58
 
45
59
  // There should be no attributes to copy in HSM, but check status anyway
46
60
  if (!config.high_security) {
47
- var custom = transaction.trace.custom.get(DESTINATIONS.ERROR_EVENT)
61
+ const custom = transaction.trace.custom.get(DESTINATIONS.ERROR_EVENT)
48
62
  urltils.overwriteParameters(custom, params.userAttributes)
49
63
  }
50
64
  }
51
65
 
66
+ const customAttributes = exception.customAttributes
52
67
  if (!config.high_security && config.api.custom_attributes_enabled && customAttributes) {
53
- for (var key in customAttributes) {
68
+ for (let key in customAttributes) {
54
69
  if (props.hasOwn(customAttributes, key)) {
55
- var dest = config.attributeFilter.filterTransaction(
70
+ const dest = config.attributeFilter.filterTransaction(
56
71
  DESTINATIONS.ERROR_EVENT,
57
72
  key
58
73
  )
@@ -63,12 +78,11 @@ function createError(transaction, exception, customAttributes, config) {
63
78
  }
64
79
  }
65
80
 
66
-
67
- var stack = exception && exception.stack
81
+ const stack = exception.error && exception.error.stack
68
82
  if (stack) {
69
83
  params.stack_trace = ('' + stack).split(/[\n\r]/g)
70
84
  if (config.high_security || config.strip_exception_messages.enabled) {
71
- params.stack_trace[0] = exception.name + ': <redacted>'
85
+ params.stack_trace[0] = exception.error.name + ': <redacted>'
72
86
  }
73
87
  }
74
88
 
@@ -77,7 +91,7 @@ function createError(transaction, exception, customAttributes, config) {
77
91
  params.intrinsics['error.expected'] = true
78
92
  }
79
93
 
80
- var res = [0, name, message, type, params]
94
+ let res = [0, name, message, type, params]
81
95
  if (transaction) {
82
96
  res.transaction = transaction.id
83
97
  }
@@ -180,3 +194,7 @@ function _getErrorEventIntrinsicAttrs(
180
194
 
181
195
  return attributes
182
196
  }
197
+
198
+ module.exports.createError = createError
199
+ module.exports.createEvent = createEvent
200
+ module.exports.Exception = Exception
@@ -33,8 +33,20 @@ function initialize(agent, childProcess, moduleName, shim) {
33
33
  const args = shim.argsToArray.apply(shim, arguments)
34
34
  const cbIndex = args.length - 1
35
35
 
36
+ const originalListener = args[cbIndex]
37
+ if (!shim.isFunction(originalListener)) {
38
+ return fn.apply(this, arguments)
39
+ }
40
+
36
41
  shim.bindSegment(args, cbIndex)
37
42
 
43
+ // Leverage events.removeListener() mechanism that checks listener
44
+ // property to allow our wrapped listeners to match and remove appropriately.
45
+ // Avoids having to instrument removeListener() and potentially doubling
46
+ // lookup. Since our wrapping will only be referenced by the events
47
+ // collection, we should not need to unwrap.
48
+ args[cbIndex].listener = originalListener
49
+
38
50
  return fn.apply(this, args)
39
51
  }
40
52
  }
@@ -195,6 +195,10 @@ function handleError(segment, req, error) {
195
195
  * @param {http.IncomingMessage} res
196
196
  */
197
197
  function handleResponse(segment, hostname, req, res) {
198
+ // Add response attributes for spans
199
+ segment.addAttribute('http.statusCode', res.statusCode)
200
+ segment.addAttribute('http.statusText', res.statusMessage)
201
+
198
202
  // If CAT is enabled, grab those headers!
199
203
  const agent = segment.transaction.agent
200
204
  if (
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
- var shared = require('./shared')
3
+ const shared = require('./shared')
4
+ const record = require('../../metrics/recorders/generic')
4
5
 
5
6
  // TODO: abstract and consolidate mostly-shared hapi functionality
6
7
  module.exports = function initialize(agent, hapi, moduleName, shim) {
@@ -154,14 +155,20 @@ function wrapPreHandlers(shim, container, path) {
154
155
  }
155
156
  return container
156
157
  } else if (shim.isFunction(container)) {
157
- return wrapRouteHandler(shim, container, path)
158
+ return wrapPreHandler(shim, container, path)
158
159
  } else if (container.method && shim.isFunction(container.method)) {
159
160
  return shim.wrap(container, 'method', function wrapHandler(shim, handler) {
160
- return wrapRouteHandler(shim, handler, path)
161
+ return wrapPreHandler(shim, handler, path)
161
162
  })
162
163
  }
163
164
  }
164
165
 
166
+ function wrapPreHandler(shim, container, path) {
167
+ return shim.record(container, (shim) => {
168
+ return {name: [shim.HAPI, ' pre handler: ','(',path,')'].join(''), recorder: record}
169
+ })
170
+ }
171
+
165
172
  function wrapRouteHandler(shim, handler, path) {
166
173
  return shim.recordMiddleware(handler, {
167
174
  route: path,
@@ -12,6 +12,7 @@ const CATEGORIES = {
12
12
  DATASTORE: 'datastore',
13
13
  GENERIC: 'generic'
14
14
  }
15
+
15
16
  const EMPTY_USER_ATTRS = Object.freeze(Object.create(null))
16
17
 
17
18
  /**
@@ -48,7 +49,8 @@ class SpanIntrinsics {
48
49
  * @class
49
50
  */
50
51
  class SpanEvent {
51
- constructor(attributes) {
52
+ constructor(attributes, customAttributes) {
53
+ this.customAttributes = customAttributes
52
54
  this.attributes = attributes
53
55
  this.intrinsics = new SpanIntrinsics()
54
56
  }
@@ -78,14 +80,15 @@ class SpanEvent {
78
80
  */
79
81
  static fromSegment(segment, parentId = null, isRoot = false) {
80
82
  const attributes = segment.attributes.get(DESTINATIONS.SPAN_EVENT)
83
+ const customAttributes = segment.customAttributes.get(DESTINATIONS.SPAN_EVENT)
81
84
 
82
85
  let span = null
83
86
  if (HttpSpanEvent.testSegment(segment)) {
84
- span = new HttpSpanEvent(attributes)
87
+ span = new HttpSpanEvent(attributes, customAttributes)
85
88
  } else if (DatastoreSpanEvent.testSegment(segment)) {
86
- span = new DatastoreSpanEvent(attributes)
89
+ span = new DatastoreSpanEvent(attributes, customAttributes)
87
90
  } else {
88
- span = new SpanEvent(attributes)
91
+ span = new SpanEvent(attributes, customAttributes)
89
92
  }
90
93
 
91
94
  const tx = segment.transaction
@@ -120,11 +123,21 @@ class SpanEvent {
120
123
  toJSON() {
121
124
  return [
122
125
  _filterNulls(this.intrinsics),
123
- EMPTY_USER_ATTRS,
126
+ this.customAttributes ?
127
+ _filterNulls(this.customAttributes) :
128
+ EMPTY_USER_ATTRS,
124
129
  _filterNulls(this.attributes)
125
130
  ]
126
131
  }
127
132
 
133
+ addCustomAttribute(key, value, truncateExempt = false) {
134
+ const {attributeFilter} = Config.getInstance()
135
+ const dest = attributeFilter.filterSegment(DESTINATIONS.SPAN_EVENT, key)
136
+ if (dest & DESTINATIONS.SPAN_EVENT) {
137
+ this.customAttributes[key] = truncateExempt ? value : _truncate(value)
138
+ }
139
+ }
140
+
128
141
  addAttribute(key, value, truncateExempt = false) {
129
142
  const {attributeFilter} = Config.getInstance()
130
143
  const dest = attributeFilter.filterSegment(DESTINATIONS.SPAN_EVENT, key)
@@ -141,8 +154,8 @@ class SpanEvent {
141
154
  * @class
142
155
  */
143
156
  class HttpSpanEvent extends SpanEvent {
144
- constructor(attributes) {
145
- super(attributes)
157
+ constructor(attributes, customAttributes) {
158
+ super(attributes, customAttributes)
146
159
 
147
160
  this.intrinsics.category = CATEGORIES.HTTP
148
161
  this.intrinsics.component = attributes.library || HTTP_LIBRARY
@@ -175,8 +188,8 @@ class HttpSpanEvent extends SpanEvent {
175
188
  * @class.
176
189
  */
177
190
  class DatastoreSpanEvent extends SpanEvent {
178
- constructor(attributes) {
179
- super(attributes)
191
+ constructor(attributes, customAttributes) {
192
+ super(attributes, customAttributes)
180
193
 
181
194
  this.intrinsics.category = CATEGORIES.DATASTORE
182
195
  this.intrinsics['span.kind'] = CLIENT_KIND
@@ -678,19 +678,40 @@ Transaction.prototype.alternatePathHashes = function alternatePathHashes() {
678
678
  return altHashes.length === 0 ? null : altHashes.sort().join(',')
679
679
  }
680
680
 
681
+ /**
682
+ * Add the error information to the current segment and add the segment ID as
683
+ * an attribute onto the exception.
684
+ *
685
+ * @param {Exception} exception The exception object to be collected.
686
+ */
687
+ Transaction.prototype._linkExceptionToSegment = _linkExceptionToSegment
688
+
689
+ function _linkExceptionToSegment(exception) {
690
+ const segment = this.agent.tracer.getSegment()
691
+ if (!segment) {
692
+ return
693
+ }
694
+
695
+ // Add error attributes to the span
696
+ const details = exception.getErrorDetails(this.agent.config)
697
+ segment.addAttribute('error.message', details.message)
698
+ segment.addAttribute('error.class', details.type)
699
+
700
+ // Add the span/segment ID to the exception as agent attributes
701
+ exception.agentAttributes.spanId = segment.id
702
+ }
703
+
681
704
  /**
682
705
  * Associate an exception with the transaction. When the transaction ends,
683
706
  * the exception will be collected along with the transaction details.
684
707
  *
685
- * @param {Error} exception The exception to be collected.
686
- * @param {object} customAttributes Any custom attributes associated with
687
- * the request (optional).
688
- * @param {number} timestamp The timestamp for when the exception occurred.
708
+ * @param {Exception} exception The exception object to be collected.
689
709
  */
690
710
  Transaction.prototype.addException = _addException
691
711
 
692
- function _addException(exception, customAttributes, timestamp) {
693
- this.exceptions.push([exception, customAttributes, timestamp])
712
+ function _addException(exception) {
713
+ this._linkExceptionToSegment(exception)
714
+ this.exceptions.push(exception)
694
715
  }
695
716
 
696
717
  /**
@@ -698,20 +719,18 @@ function _addException(exception, customAttributes, timestamp) {
698
719
  * When the transaction ends, the exception will be collected along with the transaction
699
720
  * details.
700
721
  *
701
- * @param {Error} exception The exception to be collected.
702
- * @param {object} customAttributes Any custom attributes associated with
703
- * the request (optional).
704
- * @param {number} timestamp The timestamp for when the exception occurred.
722
+ * @param {Exception} exception The exception object to be collected.
705
723
  */
706
724
  Transaction.prototype.addUserError = _addUserError
707
725
 
708
- function _addUserError(exception, customAttributes, timestamp) {
709
- this.userErrors.push([exception, customAttributes, timestamp])
726
+ function _addUserError(exception) {
727
+ this._linkExceptionToSegment(exception)
728
+ this.userErrors.push(exception)
710
729
  }
711
730
 
712
731
  /**
713
- * Returns true if an error happened during the transaction or if the transaction itself
714
- * is considered to be an error.
732
+ * Returns true if an error happened during the transaction or if the transaction itself is
733
+ * considered to be an error.
715
734
  */
716
735
  Transaction.prototype.hasErrors = function _hasErrors() {
717
736
  var isErroredTransaction = urltils.isError(this.agent.config, this.statusCode)
@@ -720,10 +739,7 @@ Transaction.prototype.hasErrors = function _hasErrors() {
720
739
  return (transactionHasExceptions || transactionHasuserErrors || isErroredTransaction)
721
740
  }
722
741
 
723
- /**
724
- * Returns true if all the errors/exceptions collected so far
725
- * are expected errors.
726
- */
742
+ // Returns true if all the errors/exceptions collected so far are expected errors
727
743
  Transaction.prototype.hasOnlyExpectedErrors = function hasOnlyExpectedErrors() {
728
744
  if (0 === this.exceptions.length) {
729
745
  return false
@@ -735,13 +751,13 @@ Transaction.prototype.hasOnlyExpectedErrors = function hasOnlyExpectedErrors() {
735
751
  const isUnexpected = !(
736
752
  errorHelper.isExpectedException(
737
753
  this,
738
- exception[0],
754
+ exception.error,
739
755
  this.agent.config,
740
756
  urltils
741
757
  ) ||
742
758
  errorHelper.shouldIgnoreError(
743
759
  this,
744
- exception[0],
760
+ exception.error,
745
761
  this.agent.config
746
762
  )
747
763
  )
@@ -2,7 +2,7 @@
2
2
 
3
3
  var codec = require('../../util/codec')
4
4
  var Segment = require('./segment')
5
- var Attributes = require('../../attributes')
5
+ var {Attributes, MAXIMUM_CUSTOM_ATTRIBUTES} = require('../../attributes')
6
6
  var logger = require('../../logger').child({component: 'trace'})
7
7
 
8
8
 
@@ -29,7 +29,7 @@ function Trace(transaction) {
29
29
  this.segmentsSeen = 0
30
30
  this.totalTimeCache = null
31
31
 
32
- this.custom = new Attributes(ATTRIBUTE_SCOPE, 64)
32
+ this.custom = new Attributes(ATTRIBUTE_SCOPE, MAXIMUM_CUSTOM_ATTRIBUTES)
33
33
  this.attributes = new Attributes(ATTRIBUTE_SCOPE)
34
34
 
35
35
  // sending displayName if set by user
@@ -6,7 +6,7 @@ const Timer = require('../../timer')
6
6
  const urltils = require('../../util/urltils')
7
7
  const hashes = require('../../util/hashes')
8
8
 
9
- const Attributes = require('../../attributes')
9
+ const {Attributes, MAXIMUM_CUSTOM_ATTRIBUTES} = require('../../attributes')
10
10
  const ExclusiveCalculator = require('./exclusive-time-calculator')
11
11
 
12
12
  const NAMES = require('../../metrics/names')
@@ -52,6 +52,7 @@ function TraceSegment(transaction, name, recorder) {
52
52
  }
53
53
 
54
54
  this.attributes = new Attributes(ATTRIBUTE_SCOPE)
55
+ this.customAttributes = new Attributes(ATTRIBUTE_SCOPE, MAXIMUM_CUSTOM_ATTRIBUTES)
55
56
 
56
57
  this.children = []
57
58
 
@@ -90,6 +91,16 @@ TraceSegment.prototype.getAttributes = function getAttributes() {
90
91
  return this.attributes.get(DESTINATIONS.TRANS_SEGMENT)
91
92
  }
92
93
 
94
+ TraceSegment.prototype.addCustomSpanAttribute =
95
+ function addCustomSpanAttribute(key, value, truncateExempt = false) {
96
+ this.customAttributes.addAttribute(
97
+ DESTINATIONS.SPAN_EVENT,
98
+ key,
99
+ value,
100
+ truncateExempt
101
+ )
102
+ }
103
+
93
104
  TraceSegment.prototype.getSpanId = function getSpanId() {
94
105
  const conf = this.transaction.agent.config
95
106
  const enabled = conf.span_events.enabled && conf.distributed_tracing.enabled
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "6.4.2",
3
+ "version": "6.5.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "contributors": [
@@ -113,13 +113,12 @@
113
113
  "lint": "eslint ./*.js lib test",
114
114
  "public-docs": "npm ci && jsdoc -c ./jsdoc-conf.json --tutorials examples/shim api.js lib/shim/ lib/transaction/handle.js && cp examples/shim/*.png out/",
115
115
  "publish-docs": "./bin/publish-docs.sh",
116
- "security": "npm audit",
117
116
  "services": "./bin/docker-services.sh",
118
117
  "smoke": "npm run clean && ./bin/smoke.sh",
119
118
  "ssl": "./bin/ssl.sh",
120
119
  "sub-install": "node test/bin/install_sub_deps",
121
120
  "test": "npm run integration && npm run unit",
122
- "unit": "rm -f newrelic_agent.log && mocha -r nock -c test/unit --recursive --exit",
121
+ "unit": "rm -f newrelic_agent.log && time tap --test-regex='(\\/|^test\\/unit\\/.*\\.test\\.js)$' --timeout=120 --no-coverage",
123
122
  "update-cross-agent-tests": "./bin/update-cats.sh",
124
123
  "versioned-tests": "./bin/run-versioned-tests.sh",
125
124
  "versioned": "npm run prepare-test && time ./bin/run-versioned-tests.sh"
@@ -152,8 +151,7 @@
152
151
  "benchmark": "^2.1.4",
153
152
  "bluebird": "^3.4.7",
154
153
  "chai": "^4.1.2",
155
- "eslint": "^5.13.0",
156
- "eslint-plugin-mocha-no-only": "^1.1.0",
154
+ "eslint": "^6.8.0",
157
155
  "express": "*",
158
156
  "generic-pool": "^3.6.1",
159
157
  "glob": "^7.1.2",
@@ -162,7 +160,6 @@
162
160
  "lodash": "^4.17.14",
163
161
  "memcached": ">=0.2.8",
164
162
  "minami": "^1.1.1",
165
- "mocha": "^6.2.1",
166
163
  "mongodb": "^3.3.3",
167
164
  "mysql": "*",
168
165
  "nock": "11.8.0",