newrelic 5.10.0 → 5.11.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,31 @@
1
+ ### 5.11.0 (2019-07-29):
2
+
3
+ * Implements Expected and Ignored Errors functionality
4
+
5
+ * Bumps jsdoc and lodash dev dependency to avoid upstream vulnerability warning.
6
+
7
+ * Added support for scoped package name introduced in hapi v18 (@hapi/hapi).
8
+
9
+ This will provide functionality at parity with instrumentation for hapi v17. Any
10
+ new features may not yet be supported.
11
+
12
+ Huge shoutout to Aori Nevo (@aorinevo) for this contribution.
13
+
14
+ * Fixed bug where agent would count errors towards error metrics even if they were
15
+ dropped due to the error collector being disabled.
16
+
17
+ * The agent will now properly track cached paths to files in loaded modules on Node
18
+ versions >10.
19
+
20
+ As of Node v11, the path to a file in a module being loaded will only be resolved
21
+ on the first load; subsequent resolution of that file will use a cached value.
22
+ The agent records this resolved path and uses it for relative file look ups in
23
+ order to deep link into modules using `Shim#require`. Since the agent couldn't
24
+ reliably get at the path on the subsequent calls to require, it now replicates
25
+ the caching logic and hold onto the resolved path for a given file.
26
+
27
+ * Adds detailed logging through harvest/collector code to increase supportability.
28
+
1
29
  ### 5.10.0 (2019-06-11):
2
30
 
3
31
  * The agent now allows installation on node v11 and v12.
package/lib/agent.js CHANGED
@@ -218,6 +218,7 @@ Agent.prototype.stop = function stop(callback) {
218
218
  const agent = this
219
219
 
220
220
  this.setState('stopping')
221
+
221
222
  this._stopHarvester()
222
223
  sampler.stop()
223
224
 
@@ -234,6 +235,8 @@ Agent.prototype.stop = function stop(callback) {
234
235
  callback(error)
235
236
  })
236
237
  } else {
238
+ logger.trace('Collector was not connected, invoking callback.')
239
+
237
240
  process.nextTick(callback)
238
241
  }
239
242
  }
@@ -331,6 +334,8 @@ Agent.prototype.reset = function reset() {
331
334
  * struggling.
332
335
  */
333
336
  Agent.prototype.harvest = function harvest(callback) {
337
+ logger.trace('Peparing to harvest.')
338
+
334
339
  if (!callback) {
335
340
  throw new TypeError('callback required!')
336
341
  }
@@ -338,7 +343,10 @@ Agent.prototype.harvest = function harvest(callback) {
338
343
  // Generate metrics for this harvest and then check we are connected to the
339
344
  // collector.
340
345
  this._generateHarvestMetrics()
346
+
341
347
  if (!this.collector.isConnected()) {
348
+ logger.trace('Collector not connected.')
349
+
342
350
  return setImmediate(function immediatelyError() {
343
351
  callback(new Error('Not connected to New Relic!'))
344
352
  })
@@ -346,6 +354,8 @@ Agent.prototype.harvest = function harvest(callback) {
346
354
 
347
355
  // We have a connection, create a new harvest.
348
356
  this.emit('harvestStarted')
357
+ logger.info('Harvest started.')
358
+
349
359
  this._lastHarvest = new Harvest(this)
350
360
  this._lastHarvest.prepare(Harvest.ALL_ENDPOINTS)
351
361
 
@@ -355,30 +365,40 @@ Agent.prototype.harvest = function harvest(callback) {
355
365
  // Send the harvest!
356
366
  const collector = this.collector
357
367
  const agent = this
368
+
369
+ logger.trace('Sending harvest data...')
358
370
  this._lastHarvest.send(function afterHarvest(err, agentRunAction) {
359
371
  // Do we need to do anything to the agent run?
360
372
  if (agentRunAction === AGENT_RUN_BEHAVIOR.SHUTDOWN) {
361
373
  agent.emit('harvestFinished')
374
+ logger.info('Harvest finished. Shutdown requested.')
375
+
362
376
  agent.stop(function afterStop(stopError) {
363
377
  const shutdownError = stopError || err
364
378
  callback(shutdownError)
365
379
  })
366
380
  } else if (agentRunAction === AGENT_RUN_BEHAVIOR.RESTART) {
381
+ logger.info('Restart requested. Harvest will complete upon restart finish.')
382
+
367
383
  collector.restart(function afterRestart(restartError) {
368
384
  // TODO: What if preconnect/connect respond with shutdown here?
369
385
  if (restartError) {
370
386
  logger.warn('Failed to restart agent run after harvest')
371
387
  callback(restartError)
372
388
  } else {
389
+ logger.trace('Restart succeeded. Finishing harvest.')
373
390
  _finish(err)
374
391
  }
375
392
  })
376
393
  } else {
394
+ logger.trace('Data sent successfully. Finishing harvest.')
377
395
  _finish(err)
378
396
  }
379
397
 
380
398
  function _finish(error) {
381
399
  agent.emit('harvestFinished')
400
+ logger.info('Harvest finished.')
401
+
382
402
  agent._scheduleHarvester(agent.config.data_report_period)
383
403
  callback(error)
384
404
  }
@@ -391,16 +411,20 @@ Agent.prototype.harvest = function harvest(callback) {
391
411
  * NOTE: this doesn't currently work outside of serverless mode.
392
412
  */
393
413
  Agent.prototype.harvestSync = function harvestSync() {
414
+ logger.trace('Peparing to harvest.')
415
+
394
416
  // Generate metrics for this harvest and then check we are connected to the
395
417
  // collector.
396
418
  this._generateHarvestMetrics()
397
419
 
398
420
  if (!this.collector.isConnected()) {
399
- throw new Error('Not connected to New Relic!')
421
+ throw new Error('Sync harvest not connected/enabled!')
400
422
  }
401
423
 
402
424
  // We have a connection, create a new harvest.
403
425
  this.emit('harvestStarted')
426
+ logger.info('Harvest started.')
427
+
404
428
  this._lastHarvest = new Harvest(this)
405
429
  this._lastHarvest.prepare(Harvest.ALL_ENDPOINTS)
406
430
 
@@ -414,7 +438,9 @@ Agent.prototype.harvestSync = function harvestSync() {
414
438
  const payloads = this._lastHarvest.getPayloads()
415
439
  collector.populateDataSync(payloads)
416
440
  collector.flushPayloadSync()
441
+
417
442
  agent.emit('harvestFinished')
443
+ logger.info('Harvest finished.')
418
444
  }
419
445
 
420
446
  Agent.prototype._generateHarvestMetrics = function _generateHarvestMetrics() {
@@ -489,7 +515,7 @@ Agent.prototype.setState = function setState(newState) {
489
515
  throw new TypeError('Invalid state ' + newState)
490
516
  }
491
517
 
492
- logger.debug('Agent state changed from %s to %s.', this._state, newState)
518
+ logger.info('Agent state changed from %s to %s.', this._state, newState)
493
519
  this._state = newState
494
520
  this.emit(this._state)
495
521
  }
@@ -541,6 +567,8 @@ function _harvesterIntervalChange(interval, callback) {
541
567
  * @param {number} harvestSeconds How many seconds between harvests.
542
568
  */
543
569
  Agent.prototype._restartHarvester = function _restartHarvester(harvestSeconds) {
570
+ logger.trace('Restarting harvester.')
571
+
544
572
  this._stopHarvester()
545
573
  this._scheduleHarvester(harvestSeconds)
546
574
  }
@@ -549,6 +577,8 @@ Agent.prototype._restartHarvester = function _restartHarvester(harvestSeconds) {
549
577
  * Safely stop the harvest cycle timer.
550
578
  */
551
579
  Agent.prototype._stopHarvester = function _stopHarvester() {
580
+ logger.trace('Stopping harvester.')
581
+
552
582
  if (this.harvesterHandle) {
553
583
  clearTimeout(this.harvesterHandle)
554
584
  }
@@ -569,11 +599,17 @@ Agent.prototype._scheduleHarvester = function _scheduleHarvester(harvestSeconds)
569
599
 
570
600
  // If there was a previous harvest, we want to schedule the next one based on
571
601
  // its start time.
572
- if (this._lastHarvest && this._lastHarvest.startTime) {
602
+ const lastHarvestStart = this._lastHarvest && this._lastHarvest.startTime
603
+ if (lastHarvestStart) {
573
604
  const timeSinceHarvest = Date.now() - this._lastHarvest.startTime
574
605
  harvestDelay = Math.max(0, harvestDelay - timeSinceHarvest)
575
606
  }
576
607
 
608
+ logger.trace(
609
+ 'Scheduling harvester. ' +
610
+ `Last harvest start: ${lastHarvestStart}. Next harvest delay: ${harvestDelay}`
611
+ )
612
+
577
613
  this.harvesterHandle = setTimeout(function doHarvest() {
578
614
  // Agent#harvest handles scheduling the next harvest and properly reacting to
579
615
  // any errors or commands. All we need to do is note any errors it spits out.
@@ -583,6 +619,7 @@ Agent.prototype._scheduleHarvester = function _scheduleHarvester(harvestSeconds)
583
619
  }
584
620
  })
585
621
  }, harvestDelay)
622
+
586
623
  this.harvesterHandle.unref()
587
624
  }
588
625
 
@@ -735,7 +772,13 @@ Agent.prototype._transactionFinished = function _transactionFinished(transaction
735
772
  logger.debug('Explicitly not ignoring %s (%s).', transaction.name, transaction.id)
736
773
  }
737
774
  this.metrics.merge(transaction.metrics)
738
- this.errors.onTransactionFinished(transaction, this.metrics)
775
+ let collectedErrors = this.errors.onTransactionFinished(transaction)
776
+
777
+ // the metric should be incremented only if the error was actually collected
778
+ if (collectedErrors > 0) {
779
+ this.metrics.getOrCreateMetric(NAMES.ERRORS.PREFIX + transaction.getFullName())
780
+ .incrementCallCount(collectedErrors)
781
+ }
739
782
  this.traces.add(transaction)
740
783
 
741
784
  const trace = transaction.trace
@@ -73,6 +73,8 @@ CollectorAPI.prototype.connect = function connect(callback) {
73
73
  throw new TypeError('callback is required')
74
74
  }
75
75
 
76
+ logger.trace('Starting collector.')
77
+
76
78
  this._agent.setState('connecting')
77
79
 
78
80
  const api = this
@@ -422,6 +424,8 @@ CollectorAPI.prototype.shutdown = function shutdown(callback) {
422
424
  throw new TypeError('callback is required')
423
425
  }
424
426
 
427
+ logger.info('Shutting down collector.')
428
+
425
429
  var agent = this._agent
426
430
  this._methods.shutdown.invoke(null, this._reqHeadersMap, onShutdown)
427
431
 
@@ -442,6 +446,8 @@ CollectorAPI.prototype.shutdown = function shutdown(callback) {
442
446
  }
443
447
 
444
448
  CollectorAPI.prototype.restart = function restart(callback) {
449
+ logger.info('Restarting collector.')
450
+
445
451
  var api = this
446
452
  this.shutdown(function reconnect() {
447
453
  api.connect(callback)
@@ -64,6 +64,8 @@ class ServerlessCollector {
64
64
  * @param {Function} cb The callback to invoke upon disabling the collector.
65
65
  */
66
66
  shutdown(cb) {
67
+ logger.trace('Disabling serverless collector.')
68
+
67
69
  this.enabled = false
68
70
  setImmediate(cb, null, CollectorResponse.success(null))
69
71
  }
@@ -298,7 +298,13 @@ exports.config = () => ({
298
298
  * your own server's sake. The payload of events is compressed, but if it
299
299
  * grows too large the New Relic servers may reject it.
300
300
  */
301
- max_event_samples_stored: 100
301
+ max_event_samples_stored: 100,
302
+
303
+ expected_classes: [],
304
+ expected_messages: {},
305
+ expected_status_codes: [],
306
+ ignore_classes:[],
307
+ ignore_messages:{}
302
308
  },
303
309
  /**
304
310
  * Error message redaction
package/lib/config/env.js CHANGED
@@ -36,7 +36,10 @@ const ENV_MAPPING = {
36
36
  enabled: 'NEW_RELIC_ERROR_COLLECTOR_ATTRIBUTES_ENABLED'
37
37
  },
38
38
  enabled: 'NEW_RELIC_ERROR_COLLECTOR_ENABLED',
39
- ignore_status_codes: 'NEW_RELIC_ERROR_COLLECTOR_IGNORE_ERROR_CODES'
39
+ ignore_status_codes: 'NEW_RELIC_ERROR_COLLECTOR_IGNORE_ERROR_CODES',
40
+ ignore_classes: 'NEW_RELIC_ERROR_COLLECTOR_IGNORE_ERRORS',
41
+ expected_status_codes: 'NEW_RELIC_ERROR_COLLECTOR_EXPECTED_ERROR_CODES',
42
+ expected_classes: 'NEW_RELIC_ERROR_COLLECTOR_EXPECTED_ERRORS',
40
43
  },
41
44
  strip_exception_messages: {
42
45
  enabled: 'NEW_RELIC_STRIP_EXCEPTION_MESSAGES_ENABLED'
@@ -147,6 +150,9 @@ const LIST_VARS = new Set([
147
150
  'NEW_RELIC_ATTRIBUTES_EXCLUDE',
148
151
  'NEW_RELIC_ATTRIBUTES_INCLUDE',
149
152
  'NEW_RELIC_ERROR_COLLECTOR_IGNORE_ERROR_CODES',
153
+ 'NEW_RELIC_ERROR_COLLECTOR_IGNORE_ERRORS',
154
+ 'NEW_RELIC_ERROR_COLLECTOR_EXPECTED_ERROR_CODES',
155
+ 'NEW_RELIC_ERROR_COLLECTOR_EXPECTED_ERRORS',
150
156
  'NEW_RELIC_ERROR_COLLECTOR_ATTRIBUTES_EXCLUDE',
151
157
  'NEW_RELIC_ERROR_COLLECTOR_ATTRIBUTES_INCLUDE',
152
158
  'NEW_RELIC_TRANSACTION_TRACER_ATTRIBUTES_EXCLUDE',
@@ -2,6 +2,7 @@
2
2
 
3
3
  const AttributeFilter = require('./attribute-filter')
4
4
  const CollectorResponse = require('../collector/response')
5
+ const MergeServerConfig = require('./merge-server-config')
5
6
  const copy = require('../util/copy')
6
7
  const defaultConfig = require('./default').config
7
8
  const EventEmitter = require('events').EventEmitter
@@ -31,9 +32,11 @@ const CONFIG_FILE_LOCATIONS = [
31
32
  path.join(__dirname, '../../../..') // above node_modules
32
33
  ]
33
34
 
34
- const HAS_ARBITRARY_KEYS = [
35
+ const HAS_ARBITRARY_KEYS = new Set([
36
+ 'ignore_messages',
37
+ 'expected_messages',
35
38
  'labels'
36
- ]
39
+ ])
37
40
 
38
41
  const LASP_MAP = require('./lasp').LASP_MAP
39
42
  const ENV = require('./env')
@@ -72,7 +75,7 @@ function fromObjectList(setting) {
72
75
  try {
73
76
  return JSON.parse('[' + setting + ']')
74
77
  } catch (error) {
75
- logger.error("New Relic configurator could not deserialize object list:")
78
+ logger.error('New Relic configurator could not deserialize object list:')
76
79
  logger.error(error.stack)
77
80
  }
78
81
  }
@@ -96,6 +99,8 @@ function _findConfigFile() {
96
99
  function Config(config) {
97
100
  EventEmitter.call(this)
98
101
 
102
+ // helper object for merging server side values
103
+ this.mergeServerConfig = new MergeServerConfig(this)
99
104
  // 1. start by cloning the defaults
100
105
  try {
101
106
  Object.assign(this, defaultConfig())
@@ -151,7 +156,7 @@ function Config(config) {
151
156
  this.browser_monitoring.loader_version = ''
152
157
 
153
158
  // Settings to play nice with DLPs (see NODE-1044).
154
- this.compressed_content_encoding = "deflate" // Deflate or gzip
159
+ this.compressed_content_encoding = 'deflate' // Deflate or gzip
155
160
  this.simple_compression = false // Disables subcomponent compression
156
161
  this.put_for_data_send = false // Changes http verb for harvest
157
162
 
@@ -178,10 +183,10 @@ function Config(config) {
178
183
  if (this.high_security) {
179
184
  if (this.security_policies_token) {
180
185
  throw new Error(
181
- "Security Policies and High Security Mode cannot both be present " +
182
- "in the agent configuration. If Security Policies have been set " +
183
- "for your account, please ensure the security_policies_token is " +
184
- "set but high_security is disabled (default)."
186
+ 'Security Policies and High Security Mode cannot both be present ' +
187
+ 'in the agent configuration. If Security Policies have been set ' +
188
+ 'for your account, please ensure the security_policies_token is ' +
189
+ 'set but high_security is disabled (default).'
185
190
  )
186
191
  }
187
192
  this._applyHighSecurity()
@@ -398,7 +403,7 @@ Config.prototype._fromServer = function _fromServer(params, key) {
398
403
  )
399
404
  break
400
405
  case 'error_collector.ignore_status_codes':
401
- this._updateNestedIfChanged(
406
+ this._validateThenUpdateStatusCodes(
402
407
  params,
403
408
  this.error_collector,
404
409
  'error_collector.ignore_status_codes',
@@ -406,6 +411,47 @@ Config.prototype._fromServer = function _fromServer(params, key) {
406
411
  )
407
412
  this._canonicalize()
408
413
  break
414
+ case 'error_collector.expected_status_codes':
415
+ this._validateThenUpdateStatusCodes(
416
+ params,
417
+ this.error_collector,
418
+ 'error_collector.expected_status_codes',
419
+ 'expected_status_codes'
420
+ )
421
+ this._canonicalize()
422
+ break
423
+ case 'error_collector.ignore_classes':
424
+ this._validateThenUpdateErrorClasses(
425
+ params,
426
+ this.error_collector,
427
+ 'error_collector.ignore_classes',
428
+ 'ignore_classes'
429
+ )
430
+ break
431
+ case 'error_collector.expected_classes':
432
+ this._validateThenUpdateErrorClasses(
433
+ params,
434
+ this.error_collector,
435
+ 'error_collector.expected_classes',
436
+ 'expected_classes'
437
+ )
438
+ break
439
+ case 'error_collector.ignore_messages':
440
+ this._validateThenUpdateErrorMessages(
441
+ params,
442
+ this.error_collector,
443
+ 'error_collector.ignore_messages',
444
+ 'ignore_messages'
445
+ )
446
+ break
447
+ case 'error_collector.expected_messages':
448
+ this._validateThenUpdateErrorMessages(
449
+ params,
450
+ this.error_collector,
451
+ 'error_collector.expected_messages',
452
+ 'expected_messages'
453
+ )
454
+ break
409
455
  case 'error_collector.capture_events':
410
456
  this._updateNestedIfChanged(
411
457
  params,
@@ -550,6 +596,153 @@ Config.prototype._updateIfChanged = function _updateIfChanged(json, key) {
550
596
  this._updateNestedIfChanged(json, this, key, key)
551
597
  }
552
598
 
599
+ /**
600
+ * Expected and Ignored status code configuration values should look like this
601
+ *
602
+ * [500,'501','503-507']
603
+ *
604
+ * If the server side config is not in this format, it might put the agent
605
+ * in a world of hurt. So, before we pass everything on to
606
+ * _updateNestedIfChanged, we'll do some validation.
607
+ *
608
+ * @param {object} remote JSON sent from New Relic.
609
+ * @param {object} local A portion of this configuration object.
610
+ * @param {string} remoteKey The name sent by New Relic.
611
+ * @param {string} localKey The local name.
612
+ */
613
+ Config.prototype._validateThenUpdateStatusCodes = _validateThenUpdateStatusCodes
614
+ function _validateThenUpdateStatusCodes(remote, local, remoteKey, localKey) {
615
+ let valueToTest = remote[remoteKey]
616
+ if (!Array.isArray(valueToTest)) {
617
+ logger.warn(
618
+ 'Saw SSC (ignore|expect)_status_codes that is not an array, will not merge: %s',
619
+ valueToTest
620
+ )
621
+ return
622
+ }
623
+
624
+ let valid = true
625
+ valueToTest.forEach(function validateArray(thingToTest) {
626
+ if (!('string' === (typeof thingToTest) || 'number' === (typeof thingToTest))) {
627
+ logger.warn(
628
+ 'Saw SSC (ignore|expect)_status_code that is not a number or string,' +
629
+ 'will not merge: %s', thingToTest
630
+ )
631
+ valid = false
632
+ }
633
+ })
634
+ if (!valid) {
635
+ return
636
+ }
637
+
638
+ return this._updateNestedIfChanged(remote, local, remoteKey, localKey)
639
+ }
640
+
641
+ /**
642
+ * Expected and Ignored classes configuration values should look like this
643
+ *
644
+ * ['Error','Again']
645
+ *
646
+ * If the server side config is not in this format, it might put the agent
647
+ * in a world of hurt. So, before we pass everything on to
648
+ * _updateNestedIfChanged, we'll do some validation.
649
+ *
650
+ * @param {object} remote JSON sent from New Relic.
651
+ * @param {object} local A portion of this configuration object.
652
+ * @param {string} remoteKey The name sent by New Relic.
653
+ * @param {string} localKey The local name.
654
+ */
655
+ Config.prototype._validateThenUpdateErrorClasses = _validateThenUpdateErrorClasses
656
+
657
+ function _validateThenUpdateErrorClasses(remote, local, remoteKey, localKey) {
658
+ let valueToTest = remote[remoteKey]
659
+ if (!Array.isArray(valueToTest)) {
660
+ logger.warn(
661
+ 'Saw SSC (ignore|expect)_classes that is not an array, will not merge: %s',
662
+ valueToTest
663
+ )
664
+ return
665
+ }
666
+
667
+ let valid = true
668
+ Object.keys(valueToTest).forEach(function validateArray(key) {
669
+ let thingToTest = valueToTest[key]
670
+ if ('string' !== (typeof thingToTest)) {
671
+ logger.warn(
672
+ 'Saw SSC (ignore|expect)_class that is not a string, will not merge: %s',
673
+ thingToTest
674
+ )
675
+ valid = false
676
+ }
677
+ })
678
+ if (!valid) {
679
+ return
680
+ }
681
+
682
+ return this._updateNestedIfChanged(remote, local, remoteKey, localKey)
683
+ }
684
+
685
+ /**
686
+ * Expected and Ignore messages configuration values should look like this
687
+ *
688
+ * {'ErrorType':['Error Message']}
689
+ *
690
+ * If the server side config is not in this format, it might put the agent
691
+ * in a world of hurt. So, before we pass everything on to
692
+ * _updateNestedIfChanged, we'll do some validation.
693
+ *
694
+ * @param {object} remote JSON sent from New Relic.
695
+ * @param {object} local A portion of this configuration object.
696
+ * @param {string} remoteKey The name sent by New Relic.
697
+ * @param {string} localKey The local name.
698
+ */
699
+ Config.prototype._validateThenUpdateErrorMessages = _validateThenUpdateErrorMessages
700
+
701
+ function _validateThenUpdateErrorMessages(remote, local, remoteKey, localKey) {
702
+ let valueToTest = remote[remoteKey]
703
+ if (Array.isArray(valueToTest)) {
704
+ logger.warn(
705
+ 'Saw SSC (ignore|expect)_message that is an Array, will not merge: %s',
706
+ valueToTest
707
+ )
708
+ return
709
+ }
710
+
711
+ if (!valueToTest) {
712
+ logger.warn('SSC ignore|expect_message is null or undefined, will not merge')
713
+ return
714
+ }
715
+
716
+ if ('object' !== typeof valueToTest) {
717
+ logger.warn(
718
+ 'Saw SSC (ignore|expect)_message that is primitive/scaler, will not merge: %s',
719
+ valueToTest
720
+ )
721
+ return
722
+ }
723
+
724
+ if (!valueToTest) {
725
+ logger.warn('SSC ignore|expect_message is null or undefined, will not merge')
726
+ return
727
+ }
728
+
729
+ let valid = true
730
+ Object.keys(valueToTest).forEach(function validateArray(key) {
731
+ let arrayToTest = valueToTest[key]
732
+ if (!Array.isArray(arrayToTest)) {
733
+ logger.warn(
734
+ 'Saw SSC message array that is not an array, will not merge: %s',
735
+ arrayToTest
736
+ )
737
+ valid = false
738
+ }
739
+ })
740
+ if (!valid) {
741
+ return
742
+ }
743
+
744
+ return this._updateNestedIfChanged(remote, local, remoteKey, localKey)
745
+ }
553
746
  /**
554
747
  * Some parameter values are nested, need a simple way to change them as well.
555
748
  * Will merge local and remote if and only if both are arrays.
@@ -572,18 +765,13 @@ function _updateNestedIfChanged(remote, local, remoteKey, localKey) {
572
765
  Config.prototype._updateNestedIfChangedRaw = _updateNestedIfChangedRaw
573
766
 
574
767
  function _updateNestedIfChangedRaw(remote, local, remoteKey, localKey) {
575
- var value = remote[remoteKey]
576
- if (value != null && local[localKey] !== value) {
577
- if (Array.isArray(value) && Array.isArray(local[localKey])) {
578
- value.forEach(function pushIfNew(element) {
579
- if (local[localKey].indexOf(element) === -1) local[localKey].push(element)
580
- })
581
- } else {
582
- local[localKey] = value
583
- }
584
- this.emit(remoteKey, value)
585
- logger.debug('Configuration of %s was changed to %s by New Relic.', remoteKey, value)
586
- }
768
+ return this.mergeServerConfig.updateNestedIfChanged(
769
+ remote,
770
+ local,
771
+ remoteKey,
772
+ localKey,
773
+ logger
774
+ )
587
775
  }
588
776
 
589
777
  /**
@@ -645,7 +833,7 @@ Config.prototype.logUnsupported = function logUnsupported(json, key) {
645
833
  Config.prototype.logUnknown = function logUnknown(json, key) {
646
834
  var value = json[key]
647
835
  logger.debug(
648
- "New Relic sent unknown configuration parameter %s with value %s.",
836
+ 'New Relic sent unknown configuration parameter %s with value %s.',
649
837
  key,
650
838
  value
651
839
  )
@@ -813,8 +1001,8 @@ Config.prototype._fromPassed = function _fromPassed(external, internal, arbitrar
813
1001
 
814
1002
  if (typeof node === 'object' && !Array.isArray(node)) {
815
1003
  // is top level and can have arbitrary keys
816
- var isTop = internal === this && HAS_ARBITRARY_KEYS.indexOf(key) !== -1
817
- this._fromPassed(node, internal[key], isTop)
1004
+ var allowArbitrary = internal === this || HAS_ARBITRARY_KEYS.has(key)
1005
+ this._fromPassed(node, internal[key], allowArbitrary)
818
1006
  } else {
819
1007
  internal[key] = node
820
1008
  }
@@ -994,6 +1182,11 @@ Config.prototype._canonicalize = function _canonicalize() {
994
1182
  this.error_collector.ignore_status_codes = _parseCodes(statusCodes)
995
1183
  }
996
1184
 
1185
+ const expectedCodes = this.error_collector && this.error_collector.expected_status_codes
1186
+ if (statusCodes) {
1187
+ this.error_collector.expected_status_codes = _parseCodes(expectedCodes)
1188
+ }
1189
+
997
1190
  var logAliases = {
998
1191
  verbose: 'trace',
999
1192
  debugging: 'debug',
@@ -1327,7 +1520,7 @@ function initialize(config) {
1327
1520
 
1328
1521
  config = new Config(userConf)
1329
1522
  config.config_file_path = filepath
1330
- logger.debug("Using configuration file %s.", filepath)
1523
+ logger.debug('Using configuration file %s.', filepath)
1331
1524
 
1332
1525
  config.validateFlags()
1333
1526
 
@@ -0,0 +1,56 @@
1
+ 'use strict'
2
+ class MergeServerConfig {
3
+ constructor(config) {
4
+ this.config = config
5
+ }
6
+
7
+ updateNestedIfChanged(remote, local, remoteKey, localKey, logger) {
8
+ var value = remote[remoteKey]
9
+
10
+ // if the value hasn't changed, skip this work
11
+ if (value === null || local[localKey] === value) {
12
+ return
13
+ }
14
+
15
+ // we need different update/merge logic if the server
16
+ // value is an array, a simple object, or anything else
17
+ if (Array.isArray(value) && Array.isArray(local[localKey])) {
18
+ this.updateArray(value, local, localKey)
19
+ } else if (this.isSimpleObject(value) && this.isSimpleObject(local[localKey])) {
20
+ this.updateObject(value, local, localKey)
21
+ } else {
22
+ local[localKey] = value
23
+ }
24
+ this.config.emit(remoteKey, value)
25
+ logger.debug('Configuration of %s was changed to %s by New Relic.', remoteKey, value)
26
+ }
27
+
28
+ updateArray(value, local, localKey) {
29
+ value.forEach( (element) => {
30
+ if (local[localKey].indexOf(element) === -1) local[localKey].push(element)
31
+ })
32
+ }
33
+
34
+ updateObject(value, local, localKey) {
35
+ // go through each key of the object and update it
36
+ Object.keys(value).forEach( (element) => {
37
+ if (Array.isArray(local[localKey][element]) && Array.isArray(value[element])) {
38
+ // if both key-values are arrays, push the remote value onto the local array
39
+ value[element].forEach( (elementValue) => {
40
+ if (-1 === local[localKey][element].indexOf(elementValue)) {
41
+ local[localKey][element].push(elementValue)
42
+ }
43
+ })
44
+ } else {
45
+ // otherwise, replace the local value with the server value
46
+ local[localKey][element] = value[element]
47
+ }
48
+ })
49
+ }
50
+
51
+ isSimpleObject(thing) {
52
+ return 'object' === (typeof thing) && this !== null
53
+ }
54
+ }
55
+
56
+ module.exports = MergeServerConfig