newrelic 14.2.1 → 14.3.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,54 +1,88 @@
1
- ### v14.2.1 (2026-07-09)
2
-
3
- #### Bug fixes
4
-
5
- * Updated `winston` instrumenting to assign `handleExceptions` to the first NrTransport to avoid registering duplicate `uncaughtException` handlers ([#4108](https://github.com/newrelic/node-newrelic/pull/4108)) ([d60a48d](https://github.com/newrelic/node-newrelic/commit/d60a48d3b9399d3a084c64cf53c3a62c7a4cab0b))
6
- * This would create a linear memory leak because the `NrTransport` objects for loggers no longer in use, would not be freed because of the global `process.on('uncaughtException')` handler
7
-
8
- #### Documentation
9
-
10
- * Updated compatibility report ([#4099](https://github.com/newrelic/node-newrelic/pull/4099)) ([058fde6](https://github.com/newrelic/node-newrelic/commit/058fde6d64e3606b82875380ba51f9bc44a4b994))
11
-
12
- #### Continuous integration
13
-
14
- * Pinned CI npm to 11 so native versioned suites build on Node 22 ([#4114](https://github.com/newrelic/node-newrelic/pull/4114)) ([b844e94](https://github.com/newrelic/node-newrelic/commit/b844e94317e573410d96f8971308b2d1535570ca))
15
- * Removed the 8 minute versioned shard timeout ([#4111](https://github.com/newrelic/node-newrelic/pull/4111)) ([0ef6374](https://github.com/newrelic/node-newrelic/commit/0ef63749b2739d7c7446a6b7c6d3ee80d2d360e5))
16
- * Attempted to fix memcached CI issue again ([#4098](https://github.com/newrelic/node-newrelic/pull/4098)) ([f67fef5](https://github.com/newrelic/node-newrelic/commit/f67fef5febe71ffab853164aba06c76cd2c6c26d))
17
- * Shard versioned tests across CI runners ([#4101](https://github.com/newrelic/node-newrelic/pull/4101)) ([1471734](https://github.com/newrelic/node-newrelic/commit/14717345d87ec46a4eba18c513e10e3caed40f7c))
18
-
19
- #### Tests
20
-
21
- * Added `samples` to prisma and next to cut down on versions under test on packages we recommend using hybrid ageninstrumentation ([#4112](https://github.com/newrelic/node-newrelic/pull/4112)) ([7d8bd7c](https://github.com/newrelic/node-newrelic/commit/7d8bd7cd2e3076d17ad25b75628956d1158fc23a))
22
- * Resolved `grpc` `proto-loader` from the running suite's `node_modules` ([#4110](https://github.com/newrelic/node-newrelic/pull/4110)) ([5b5a9f8](https://github.com/newrelic/node-newrelic/commit/5b5a9f8d8e0f17557b9f202b0098e2ec84c9e028))
23
-
24
- ### v14.2.0 (2026-07-07)
25
-
26
- #### Features
27
-
28
- * Added Node.js 26 support ([#4084](https://github.com/newrelic/node-newrelic/pull/4084)) ([4a7f777](https://github.com/newrelic/node-newrelic/commit/4a7f7775d8598656f84bd1590a80e56458feb185))
29
- * Added `span_id` and `trace_id` labels to `CpuProfiler` sample data ([#4049](https://github.com/newrelic/node-newrelic/pull/4049)) ([c2b1737](https://github.com/newrelic/node-newrelic/commit/c2b1737ac87a26b7fce6d170597301cf97d26b22))
30
- * Added new GCP Cloud Run hostname logic to include `K_REVISION` ([#4069](https://github.com/newrelic/node-newrelic/pull/4069)) ([02855dc](https://github.com/newrelic/node-newrelic/commit/02855dc82b636c655e9808b50897b4dc5c7b1bba))
31
- * Added `profiling.source_mapping.enabled` config option ([#4090](https://github.com/newrelic/node-newrelic/pull/4090)) ([43add80](https://github.com/newrelic/node-newrelic/commit/43add80d07cccb1fd48190a60895d88cbde17b00))
32
- * Removed `spanId` in outgoing `tracestate` header ([#4091](https://github.com/newrelic/node-newrelic/pull/4091)) ([40a8bf1](https://github.com/newrelic/node-newrelic/commit/40a8bf1682860d11f2bf46fbfdaae840b6f3fa18))
33
-
34
- #### Code refactoring
35
-
36
- * Refactored `mongodb` instrumentation to subscribe to events emitted ([#4059](https://github.com/newrelic/node-newrelic/pull/4059)) ([d2777ba](https://github.com/newrelic/node-newrelic/commit/d2777ba6b29ab30f1a158e993283029bf40cff86))
37
-
38
- #### Documentation
39
-
40
- * Updated compatibility report ([#4078](https://github.com/newrelic/node-newrelic/pull/4078)) ([047a063](https://github.com/newrelic/node-newrelic/commit/047a063dd9e21097d2995dbd7f14ee564b96efcd))
41
-
42
- #### Miscellaneous chores
43
-
44
- * Addressed long standing CI flakes ([#4093](https://github.com/newrelic/node-newrelic/pull/4093)) ([ae973a0](https://github.com/newrelic/node-newrelic/commit/ae973a0d7bfd7da54bd0fe5e68d79fa77b180cfe))
45
- * Fixed package version resolution on Node 26 ([#4094](https://github.com/newrelic/node-newrelic/pull/4094)) ([73d14a9](https://github.com/newrelic/node-newrelic/commit/73d14a98c3bd678be499e14533e66f86b7b9996b))
46
-
47
- #### Continuous integration
48
-
49
- * Updated `release-creation` to use `lts/*` for node-version ([#4080](https://github.com/newrelic/node-newrelic/pull/4080)) ([7eaed22](https://github.com/newrelic/node-newrelic/commit/7eaed22f92af2da5db31232b60b5bf35f0ca3e02))
50
- * Updated `setup-node` job to include `check-latest` to ensure we are getting the latest version of node ([#4079](https://github.com/newrelic/node-newrelic/pull/4079)) ([4a425f6](https://github.com/newrelic/node-newrelic/commit/4a425f692c8eacac1e4967e7517c34605f67ad82))
51
-
1
+ ### v14.3.0 (2026-07-14)
2
+
3
+ #### Features
4
+
5
+ * Added source mapping support to heap profiler ([#4122](https://github.com/newrelic/node-newrelic/pull/4122)) ([dcee39f](https://github.com/newrelic/node-newrelic/commit/dcee39fa76a102389f9dd7c186461346ef4fa265))
6
+ * Added source mapping support to CPU (wall time) profiler ([#4095](https://github.com/newrelic/node-newrelic/pull/4095)) ([f6123bc](https://github.com/newrelic/node-newrelic/commit/f6123bc939afc0122443fda3eab7d9e842dcbfef))
7
+ * Added `otlp_resource_attributes` to all outgoing OpenTelemetry metrics calls ([#4128](https://github.com/newrelic/node-newrelic/pull/4128)) ([a0c1c5b](https://github.com/newrelic/node-newrelic/commit/a0c1c5b0f314b7ca35b84f0c6cf8229395c4aa52))
8
+ * Added support for `ai_monitoring.enabled`,`ai_monitoring.streaming.enabled`, and `ai_monitoring.record_content.enabled` to be assigned via server-side config ([#4123](https://github.com/newrelic/node-newrelic/pull/4123)) ([c4d8032](https://github.com/newrelic/node-newrelic/commit/c4d8032bb2fcc7e346854b6ae818bea9e17f7f9f))
9
+
10
+ #### Bug fixes
11
+
12
+ * Fixed `bunyan` log forwarding multiple duplicate lines ([#4132](https://github.com/newrelic/node-newrelic/pull/4132)) ([b0f4242](https://github.com/newrelic/node-newrelic/commit/b0f4242098ffe8a63566519707987b588b22f6aa))
13
+ * Fixed OTEL crash when adding exceptions to spans ([#4130](https://github.com/newrelic/node-newrelic/pull/4130)) ([508542e](https://github.com/newrelic/node-newrelic/commit/508542e7cb8f2ad11952171a4e44dc71b2d23f07))
14
+
15
+ #### Documentation
16
+
17
+ * Updated compatibility report ([#4125](https://github.com/newrelic/node-newrelic/pull/4125)) ([405b59b](https://github.com/newrelic/node-newrelic/commit/405b59b20ed811b31eadc4a85bf8ff6a64c12edd)) ([#4107](https://github.com/newrelic/node-newrelic/pull/4107)) ([267a964](https://github.com/newrelic/node-newrelic/commit/267a964cffdab4373a9292e035cc877b814f8529))
18
+
19
+ #### Miscellaneous chores
20
+
21
+ * Changed `notify-release.yml` schedule with our bot user ([#4135](https://github.com/newrelic/node-newrelic/pull/4135)) ([a9318ae](https://github.com/newrelic/node-newrelic/commit/a9318aec1ff32fd8930431b20df7123f76ced513))
22
+ * Changed `versioned-security-agent.yml` schedule with our bot user ([#4134](https://github.com/newrelic/node-newrelic/pull/4134)) ([d7c6f64](https://github.com/newrelic/node-newrelic/commit/d7c6f64af2747a392ac905a1775aa70642a9f9b6))
23
+ * Changed smoke-test schedule with our bot user ([#4133](https://github.com/newrelic/node-newrelic/pull/4133)) ([400268e](https://github.com/newrelic/node-newrelic/commit/400268e98b9d559c1b46726a91f555aeff706bbe))
24
+ * Sharded the versioned tests w/ security agent workflow ([#4119](https://github.com/newrelic/node-newrelic/pull/4119)) ([fe200c4](https://github.com/newrelic/node-newrelic/commit/fe200c4fc1996ce7b115a7ab76e4b84659758d01))
25
+ * Updated test runner to accept suites as positional parameters ([#4117](https://github.com/newrelic/node-newrelic/pull/4117)) ([ac0985d](https://github.com/newrelic/node-newrelic/commit/ac0985d65291eb24637c90e4d96b0e3aff56ecfc))
26
+
27
+ #### Tests
28
+
29
+ * Added more scenarios to winston for constructing loggers ([#4120](https://github.com/newrelic/node-newrelic/pull/4120)) ([ea90fc3](https://github.com/newrelic/node-newrelic/commit/ea90fc30719824c8cdd7de67a9af88257ab97805))
30
+ * Added skip for winston test for ranges that do not support it ([#4121](https://github.com/newrelic/node-newrelic/pull/4121)) ([e75e15e](https://github.com/newrelic/node-newrelic/commit/e75e15e3ca3eb7344f9a9697efe9b49fef96f3af))
31
+ * Applied sampling to `pg-native` tests to reduce the matrix under test ([#4124](https://github.com/newrelic/node-newrelic/pull/4124)) ([b1ad2da](https://github.com/newrelic/node-newrelic/commit/b1ad2da853ee55ebe7477ff6a172e454a76d13bd))
32
+ * Fixed flaky amqplib async-work duration assertions ([#4118](https://github.com/newrelic/node-newrelic/pull/4118)) ([3544237](https://github.com/newrelic/node-newrelic/commit/3544237c908f908aad17cf4d6b713015f186be6e))
33
+ * Removed timing race from common.request unit tests ([#4113](https://github.com/newrelic/node-newrelic/pull/4113)) ([b22c25b](https://github.com/newrelic/node-newrelic/commit/b22c25b56f73b9821710e22988a4cdcbfb06d907))
34
+
35
+ ### v14.2.1 (2026-07-09)
36
+
37
+ #### Bug fixes
38
+
39
+ * Updated `winston` instrumenting to assign `handleExceptions` to the first NrTransport to avoid registering duplicate `uncaughtException` handlers ([#4108](https://github.com/newrelic/node-newrelic/pull/4108)) ([d60a48d](https://github.com/newrelic/node-newrelic/commit/d60a48d3b9399d3a084c64cf53c3a62c7a4cab0b))
40
+ * This would create a linear memory leak because the `NrTransport` objects for loggers no longer in use, would not be freed because of the global `process.on('uncaughtException')` handler
41
+
42
+ #### Documentation
43
+
44
+ * Updated compatibility report ([#4099](https://github.com/newrelic/node-newrelic/pull/4099)) ([058fde6](https://github.com/newrelic/node-newrelic/commit/058fde6d64e3606b82875380ba51f9bc44a4b994))
45
+
46
+ #### Continuous integration
47
+
48
+ * Pinned CI npm to 11 so native versioned suites build on Node 22 ([#4114](https://github.com/newrelic/node-newrelic/pull/4114)) ([b844e94](https://github.com/newrelic/node-newrelic/commit/b844e94317e573410d96f8971308b2d1535570ca))
49
+ * Removed the 8 minute versioned shard timeout ([#4111](https://github.com/newrelic/node-newrelic/pull/4111)) ([0ef6374](https://github.com/newrelic/node-newrelic/commit/0ef63749b2739d7c7446a6b7c6d3ee80d2d360e5))
50
+ * Attempted to fix memcached CI issue again ([#4098](https://github.com/newrelic/node-newrelic/pull/4098)) ([f67fef5](https://github.com/newrelic/node-newrelic/commit/f67fef5febe71ffab853164aba06c76cd2c6c26d))
51
+ * Shard versioned tests across CI runners ([#4101](https://github.com/newrelic/node-newrelic/pull/4101)) ([1471734](https://github.com/newrelic/node-newrelic/commit/14717345d87ec46a4eba18c513e10e3caed40f7c))
52
+
53
+ #### Tests
54
+
55
+ * Added `samples` to prisma and next to cut down on versions under test on packages we recommend using hybrid ageninstrumentation ([#4112](https://github.com/newrelic/node-newrelic/pull/4112)) ([7d8bd7c](https://github.com/newrelic/node-newrelic/commit/7d8bd7cd2e3076d17ad25b75628956d1158fc23a))
56
+ * Resolved `grpc` `proto-loader` from the running suite's `node_modules` ([#4110](https://github.com/newrelic/node-newrelic/pull/4110)) ([5b5a9f8](https://github.com/newrelic/node-newrelic/commit/5b5a9f8d8e0f17557b9f202b0098e2ec84c9e028))
57
+
58
+ ### v14.2.0 (2026-07-07)
59
+
60
+ #### Features
61
+
62
+ * Added Node.js 26 support ([#4084](https://github.com/newrelic/node-newrelic/pull/4084)) ([4a7f777](https://github.com/newrelic/node-newrelic/commit/4a7f7775d8598656f84bd1590a80e56458feb185))
63
+ * Added `span_id` and `trace_id` labels to `CpuProfiler` sample data ([#4049](https://github.com/newrelic/node-newrelic/pull/4049)) ([c2b1737](https://github.com/newrelic/node-newrelic/commit/c2b1737ac87a26b7fce6d170597301cf97d26b22))
64
+ * Added new GCP Cloud Run hostname logic to include `K_REVISION` ([#4069](https://github.com/newrelic/node-newrelic/pull/4069)) ([02855dc](https://github.com/newrelic/node-newrelic/commit/02855dc82b636c655e9808b50897b4dc5c7b1bba))
65
+ * Added `profiling.source_mapping.enabled` config option ([#4090](https://github.com/newrelic/node-newrelic/pull/4090)) ([43add80](https://github.com/newrelic/node-newrelic/commit/43add80d07cccb1fd48190a60895d88cbde17b00))
66
+ * Removed `spanId` in outgoing `tracestate` header ([#4091](https://github.com/newrelic/node-newrelic/pull/4091)) ([40a8bf1](https://github.com/newrelic/node-newrelic/commit/40a8bf1682860d11f2bf46fbfdaae840b6f3fa18))
67
+
68
+ #### Code refactoring
69
+
70
+ * Refactored `mongodb` instrumentation to subscribe to events emitted ([#4059](https://github.com/newrelic/node-newrelic/pull/4059)) ([d2777ba](https://github.com/newrelic/node-newrelic/commit/d2777ba6b29ab30f1a158e993283029bf40cff86))
71
+
72
+ #### Documentation
73
+
74
+ * Updated compatibility report ([#4078](https://github.com/newrelic/node-newrelic/pull/4078)) ([047a063](https://github.com/newrelic/node-newrelic/commit/047a063dd9e21097d2995dbd7f14ee564b96efcd))
75
+
76
+ #### Miscellaneous chores
77
+
78
+ * Addressed long standing CI flakes ([#4093](https://github.com/newrelic/node-newrelic/pull/4093)) ([ae973a0](https://github.com/newrelic/node-newrelic/commit/ae973a0d7bfd7da54bd0fe5e68d79fa77b180cfe))
79
+ * Fixed package version resolution on Node 26 ([#4094](https://github.com/newrelic/node-newrelic/pull/4094)) ([73d14a9](https://github.com/newrelic/node-newrelic/commit/73d14a98c3bd678be499e14533e66f86b7b9996b))
80
+
81
+ #### Continuous integration
82
+
83
+ * Updated `release-creation` to use `lts/*` for node-version ([#4080](https://github.com/newrelic/node-newrelic/pull/4080)) ([7eaed22](https://github.com/newrelic/node-newrelic/commit/7eaed22f92af2da5db31232b60b5bf35f0ca3e02))
84
+ * Updated `setup-node` job to include `check-latest` to ensure we are getting the latest version of node ([#4079](https://github.com/newrelic/node-newrelic/pull/4079)) ([4a425f6](https://github.com/newrelic/node-newrelic/commit/4a425f692c8eacac1e4967e7517c34605f67ad82))
85
+
52
86
  ### v14.1.2 (2026-06-25)
53
87
 
54
88
  #### Bug fixes
package/lib/agent.js CHANGED
@@ -397,7 +397,11 @@ Agent.prototype.start = function start(callback) {
397
397
  }
398
398
  logger.info('Starting New Relic for Node.js connection process.')
399
399
 
400
- this.collector.connect(function onStartConnect(error, response) {
400
+ // Build the profiling source mapper once, so it can be reused
401
+ // across profiling start/stop cycles.
402
+ this.sourceMapperReady = this.profilingData.initSourceMapper()
403
+
404
+ this.collector.connect(async function onStartConnect(error, response) {
401
405
  if (error || response.shouldShutdownRun()) {
402
406
  agent.healthReporter.setStatus(HealthReporter.STATUS_CONNECT_ERROR)
403
407
  agent.setState('errored')
@@ -408,6 +412,9 @@ Agent.prototype.start = function start(callback) {
408
412
 
409
413
  if (agent.collector.isConnected()) {
410
414
  const config = response.payload
415
+ // await for source mapper before starting the harvester
416
+ // which registers the profiling aggregator which could take the sourceMapper
417
+ await agent.sourceMapperReady
411
418
 
412
419
  const shouldImmediatelyHarvest = !agent.config.no_immediate_harvest
413
420
  agent.onConnect(shouldImmediatelyHarvest, () => {
@@ -453,7 +460,6 @@ Agent.prototype.onConnect = function onConnect(shouldImmediatelyHarvest, callbac
453
460
  }
454
461
 
455
462
  this._scheduleHarvests(shouldImmediatelyHarvest, callback)
456
-
457
463
  this.setState('started')
458
464
  }
459
465
 
@@ -37,6 +37,17 @@ class ProfilingAggregator extends BaseAggregator {
37
37
  return this.pprofData
38
38
  }
39
39
 
40
+ /**
41
+ * Builds the profiling SourceMapper once during agent startup so the profilers
42
+ * resolve frames to their original source. Delegated to the manager, which caches
43
+ * and consumes it; a no-op unless both profiling and source_mapping are enabled.
44
+ *
45
+ * @returns {Promise<void>}
46
+ */
47
+ initSourceMapper() {
48
+ return this.profilingManager.buildSourceMapper()
49
+ }
50
+
40
51
  /**
41
52
  * This overrides the default `start` method
42
53
  * as we want to collect profiling data for `cpu` and/or `heap`
@@ -141,6 +141,10 @@ function Config(config) {
141
141
 
142
142
  this.browser_monitoring.loader = 'rum'
143
143
  this.browser_monitoring.loader_version = ''
144
+ // default to empty object, this will get populated
145
+ // from `onConnect` if hybrid agent is enabled
146
+ // along with hybrid agent's metrics feature
147
+ this.otlp_resource_attributes = {}
144
148
 
145
149
  // Settings to play nice with DLPs (see NODE-1044).
146
150
  this.simple_compression = false // Disables subcomponent compression
@@ -357,12 +361,6 @@ Config.prototype._fromServer = function _fromServer(params, key) {
357
361
  case 'high_security':
358
362
  break
359
363
 
360
- // interpret AI Monitoring account setting
361
- case 'collect_ai':
362
- this._disableOption(params.collect_ai, 'ai_monitoring')
363
- this.emit('change', this)
364
- break
365
-
366
364
  // always accept these settings
367
365
  case 'encoding_key':
368
366
  this._alwaysUpdateIfChanged(params, key)
@@ -583,6 +581,41 @@ Config.prototype._fromServer = function _fromServer(params, key) {
583
581
  }
584
582
  break
585
583
 
584
+ // interpret AI Monitoring account setting.
585
+ // `ai_monitoring.enabled` from the connect response server-side config(SSC) takes precedence over
586
+ // SSC settings come in an `agent_config` object
587
+ // `collect_ai`; collect_ai is only the fallback when it is absent.
588
+ case 'collect_ai':
589
+ if (!params?.agent_config?.['ai_monitoring.enabled']) {
590
+ this._disableOption(params.collect_ai, 'ai_monitoring')
591
+ this.emit('change', this)
592
+ }
593
+ break
594
+ case 'ai_monitoring.enabled':
595
+ this._updateNestedIfChanged(
596
+ params,
597
+ this.ai_monitoring,
598
+ 'ai_monitoring.enabled',
599
+ 'enabled'
600
+ )
601
+ break
602
+ case 'ai_monitoring.streaming.enabled':
603
+ this._updateNestedIfChanged(
604
+ params,
605
+ this.ai_monitoring.streaming,
606
+ 'ai_monitoring.streaming.enabled',
607
+ 'enabled'
608
+ )
609
+ break
610
+ case 'ai_monitoring.record_content.enabled':
611
+ this._updateNestedIfChanged(
612
+ params,
613
+ this.ai_monitoring.record_content,
614
+ 'ai_monitoring.record_content.enabled',
615
+ 'enabled'
616
+ )
617
+ break
618
+
586
619
  case 'profiling.enabled':
587
620
  this._updateNestedIfChanged(
588
621
  params,
@@ -592,6 +625,10 @@ Config.prototype._fromServer = function _fromServer(params, key) {
592
625
  )
593
626
  break
594
627
 
628
+ case 'otlp_resource_attributes':
629
+ this.otlp_resource_attributes = params.otlp_resource_attributes ?? {}
630
+ break
631
+
595
632
  // These settings are not allowed from the server.
596
633
  case 'attributes.enabled':
597
634
  case 'attributes.exclude':
@@ -30,7 +30,11 @@ class AsyncLocalContextManager {
30
30
  * @returns {object} The current active context.
31
31
  */
32
32
  getContext() {
33
- return this._asyncLocalStorage.getStore() || (this.isOtelBridgeMode ? new OtelContext() : new Context())
33
+ return this._asyncLocalStorage.getStore() || (
34
+ this.isOtelBridgeMode
35
+ ? new OtelContext()
36
+ : new Context()
37
+ )
34
38
  }
35
39
 
36
40
  /**
@@ -10,12 +10,29 @@
10
10
  * within otel bridge. We have to create fake spans for new relic
11
11
  * segments. The only thing needed is a method for `spanContext`
12
12
  * which should return the spanId(segment id) and traceId(transaction trace id).
13
- * We hardcode traceFlags to 1.
13
+ * We hard code traceFlags to 1.
14
+ *
15
+ * Note: as of 2026-07 we stub methods on this object that do not make sense
16
+ * in the context of our bridge. For example, we internally manage the name
17
+ * and lifecyle of spans. So those methods are no-ops here.
18
+ *
19
+ * @see https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api._opentelemetry_api.Span.html
14
20
  */
15
21
  module.exports = class FakeSpan {
22
+ #segment
23
+ #transaction
24
+
16
25
  constructor(segment, transaction) {
17
- this.segmentId = segment.id
18
- this.traceId = transaction.traceId
26
+ this.#segment = segment
27
+ this.#transaction = transaction
28
+ }
29
+
30
+ get segmentId() {
31
+ return this.#segment.id
32
+ }
33
+
34
+ get traceId() {
35
+ return this.#transaction.traceId
19
36
  }
20
37
 
21
38
  spanContext() {
@@ -25,4 +42,103 @@ module.exports = class FakeSpan {
25
42
  traceFlags: 1
26
43
  }
27
44
  }
45
+
46
+ /**
47
+ * Adds a new attribute to the backing segment.
48
+ *
49
+ * @param {string} key The attribute name to add
50
+ * @param {*} value Some serializable JavaScript value.
51
+ *
52
+ * @returns {FakeSpan}
53
+ */
54
+ setAttribute(key, value) {
55
+ this.#segment.addAttribute(key, value)
56
+ return this
57
+ }
58
+
59
+ /**
60
+ * Add multiple attributes to the backing segment at once.
61
+ *
62
+ * @param {object} attributes Each field name is the key, and each field
63
+ * value the value passed to {@link #setAttribute}.
64
+ *
65
+ * @returns {FakeSpan}
66
+ */
67
+ setAttributes(attributes) {
68
+ for (const [k, v] of Object.entries(attributes)) {
69
+ this.setAttribute(k, v)
70
+ }
71
+ return this
72
+ }
73
+
74
+ addEvent(name, _attrsOrTime, _time) {
75
+ this.#segment.logger.warn(
76
+ 'addEvent is not implemented. Not adding event: %s.',
77
+ name
78
+ )
79
+ return this
80
+ }
81
+
82
+ /**
83
+ * Adds a new span link to the backing segment.
84
+ *
85
+ * @param {object} link An OTEL shaped span link.
86
+ *
87
+ * @see https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api._opentelemetry_api.Link.html
88
+ *
89
+ * @returns {FakeSpan}
90
+ */
91
+ addLink(link) {
92
+ this.#segment.addSpanLink(link)
93
+ return this
94
+ }
95
+
96
+ /**
97
+ * Adds multiple span links to the backing segment at once.
98
+ *
99
+ * @param {object[]} links A list of OTEL shaped span links.
100
+ *
101
+ * @see @see https://open-telemetry.github.io/opentelemetry-js/interfaces/_opentelemetry_api._opentelemetry_api.Link.html
102
+ *
103
+ * @returns {FakeSpan}
104
+ */
105
+ addLinks(links) {
106
+ for (const link of links) {
107
+ this.addLink(link)
108
+ }
109
+ return this
110
+ }
111
+
112
+ setStatus(otelSpanStatus) {
113
+ this.#segment.logger.warn(
114
+ 'setStatus is not implemented. Not setting status: %s.',
115
+ otelSpanStatus?.code
116
+ )
117
+ return this
118
+ }
119
+
120
+ updateName(name) {
121
+ this.#segment.logger.warn(
122
+ 'updateName is not implemented. Not setting name: %s.',
123
+ name
124
+ )
125
+ return this
126
+ }
127
+
128
+ end(_time) {
129
+ this.#segment.logger.warn(
130
+ 'end is not implemented. Not ending span.'
131
+ )
132
+ }
133
+
134
+ isRecording() {
135
+ return true
136
+ }
137
+
138
+ recordException(exception, time) {
139
+ // This replicates what the original actually does.
140
+ // See:
141
+ // https://github.com/open-telemetry/opentelemetry-js/blob/9b05f66/packages/sdk-trace/src/Span.ts#L427-L451.
142
+ this.addEvent(exception, null, time)
143
+ }
28
144
  }
@@ -115,7 +115,7 @@ class SetupMetrics extends SetupSignal {
115
115
  { agent, logger }
116
116
  )
117
117
 
118
- const resource = resourceFromAttributes({ 'entity.guid': config.entity_guid })
118
+ const resource = resourceFromAttributes({ 'entity.guid': config.entity_guid, ...config.otlp_resource_attributes })
119
119
  // Assigning the resource after having received the `entity.guid` from
120
120
  // the server is a key detail of this implementation. Unfortunately,
121
121
  // we don't have real public access to the object that retains the
@@ -16,17 +16,26 @@ class ProfilingManager {
16
16
  this.samplingInterval = samplingInterval
17
17
  this.profilers = new Map()
18
18
  this.startTime = null
19
+ this.sourceMapper = null
19
20
  }
20
21
 
21
22
  register() {
22
23
  if (this.config.include.includes('heap') && !this.profilers.has('HeapProfiler')) {
23
24
  const { HeapProfiler } = require('./profilers')
24
- this.profilers.set('HeapProfiler', new HeapProfiler({ logger: this.logger }))
25
+ this.profilers.set('HeapProfiler', new HeapProfiler({
26
+ logger: this.logger,
27
+ sourceMapper: this.sourceMapper
28
+ }))
25
29
  }
26
30
 
27
31
  if (this.config.include.includes('cpu') && !this.profilers.has('CpuProfiler')) {
28
32
  const { CpuProfiler } = require('./profilers')
29
- this.profilers.set('CpuProfiler', new CpuProfiler({ logger: this.logger, samplingInterval: this.samplingInterval, tracer: this.tracer }))
33
+ this.profilers.set('CpuProfiler', new CpuProfiler({
34
+ logger: this.logger,
35
+ samplingInterval: this.samplingInterval,
36
+ tracer: this.tracer,
37
+ sourceMapper: this.sourceMapper
38
+ }))
30
39
  }
31
40
  }
32
41
 
@@ -84,6 +93,31 @@ class ProfilingManager {
84
93
 
85
94
  return results
86
95
  }
96
+
97
+ /**
98
+ * Builds the `pprof` SourceMapper once by scanning the app root for source
99
+ * maps and caches it for the profilers to resolve frames to their original
100
+ * source. A no-op unless `source_mapping` is enabled; on failure, the mapper
101
+ * stays `null` and frames fall back to compiled file/line.
102
+ *
103
+ * @returns {Promise<void>}
104
+ */
105
+ async buildSourceMapper() {
106
+ if (!(this.config.enabled && this.config.source_mapping.enabled)) {
107
+ return
108
+ }
109
+
110
+ // lazy-require so pprof's native binding only loads when source mapping is enabled
111
+ const { SourceMapper } = require('@datadog/pprof')
112
+ const searchDir = process.cwd()
113
+ try {
114
+ const start = Date.now()
115
+ this.sourceMapper = await SourceMapper.create([searchDir])
116
+ this.logger.debug(`Built profiling SourceMapper from ${searchDir} in ${Date.now() - start} ms.`)
117
+ } catch (error) {
118
+ this.logger.error({ error }, `Failed to build profiling SourceMapper from ${searchDir}, frames will report compiled file/line.`)
119
+ }
120
+ }
87
121
  }
88
122
 
89
123
  module.exports = ProfilingManager
@@ -6,8 +6,17 @@
6
6
  'use strict'
7
7
 
8
8
  class BaseProfiler {
9
- constructor({ logger }) {
9
+ /**
10
+ * pprof `SourceMapper` to be shared across the entire agent
11
+ * instance, or `null`/`undefined` if disabled.
12
+ * When set, pprof resolves sample frames to their original
13
+ * source at serialization (e.g. TypeScript, minified code).
14
+ */
15
+ sourceMapper
16
+
17
+ constructor({ logger, sourceMapper }) {
10
18
  this.logger = logger
19
+ this.sourceMapper = sourceMapper
11
20
  }
12
21
 
13
22
  start() {
@@ -58,8 +58,8 @@ class CpuProfiler extends BaseProfiler {
58
58
  */
59
59
  #asyncHook
60
60
 
61
- constructor({ logger, samplingInterval, tracer }) {
62
- super({ logger })
61
+ constructor({ logger, samplingInterval, tracer, sourceMapper }) {
62
+ super({ logger, sourceMapper })
63
63
  this.#pprof = require('@datadog/pprof')
64
64
  this.#kSampleCount = this.#pprof.time.constants.kSampleCount
65
65
  this.#tracer = tracer
@@ -82,7 +82,8 @@ class CpuProfiler extends BaseProfiler {
82
82
  // needed for trace_id + span_id label
83
83
  withContexts: true,
84
84
  // carry the sample context per async-context-frame when ACF is active
85
- useCPED: this.#useCPED
85
+ useCPED: this.#useCPED,
86
+ sourceMapper: this.sourceMapper
86
87
  })
87
88
 
88
89
  if (this.#useCPED) {
@@ -11,8 +11,8 @@ class HeapProfiler extends BaseProfiler {
11
11
  #pprof
12
12
  #intervalBytes = 524288 // captures stack trace every 512 kb of allocated memory
13
13
  #stackDepth = 64
14
- constructor({ logger }) {
15
- super({ logger })
14
+ constructor({ logger, sourceMapper }) {
15
+ super({ logger, sourceMapper })
16
16
  this.#pprof = require('@datadog/pprof')
17
17
  }
18
18
 
@@ -30,7 +30,7 @@ class HeapProfiler extends BaseProfiler {
30
30
  }
31
31
 
32
32
  async collect() {
33
- const profile = this.#pprof.heap.profile()
33
+ const profile = this.#pprof.heap.profile(undefined, this.sourceMapper)
34
34
  return this.#pprof.encode(profile)
35
35
  }
36
36
  }
@@ -26,8 +26,23 @@ class AiMonitoringSubscriber extends Subscriber {
26
26
  this.trackingPrefix = trackingPrefix
27
27
  }
28
28
 
29
- get enabled() {
30
- return super.enabled && this.agent.config.ai_monitoring.enabled === true
29
+ /**
30
+ * Checks if AI monitoring is enabled. The subscriber itself is registered
31
+ * regardless of this value (gated only by `instrumentation.<lib>.enabled`),
32
+ * but AI monitoring behavior (segments, events, metrics) is gated on this.
33
+ * @returns {boolean} if AI monitoring is enabled
34
+ */
35
+ get aiEnabled() {
36
+ return this.agent.config.ai_monitoring.enabled === true
37
+ }
38
+
39
+ /**
40
+ * Checks if AI monitoring streaming is enabled. It is predicated on AI monitoring being enabled
41
+ * as if it isn't, no instrumentation should occur
42
+ * @returns {boolean} if we should be instrumenting streams
43
+ */
44
+ get streamingEnabled() {
45
+ return this.aiEnabled && this.agent.config.ai_monitoring.streaming.enabled === true
31
46
  }
32
47
 
33
48
  set name(name) {
@@ -96,7 +111,7 @@ class AiMonitoringSubscriber extends Subscriber {
96
111
  * @returns {Context} either new context or existing
97
112
  */
98
113
  handler(data, ctx) {
99
- if (!this.enabled) {
114
+ if (!this.aiEnabled) {
100
115
  this.logger.debug('`ai_monitoring.enabled` is set to false, not creating segment.')
101
116
  return ctx
102
117
  }
@@ -23,10 +23,6 @@ class AiMonitoringChatSubscriber extends AiMonitoringSubscriber {
23
23
  super({ agent, logger, packageName, channelName, name, trackingPrefix })
24
24
  }
25
25
 
26
- get streamingEnabled() {
27
- return this.agent.config.ai_monitoring.streaming.enabled === true
28
- }
29
-
30
26
  /**
31
27
  * Function that must be implemented by inherited subscriber to create an llm completion message.
32
28
  *
@@ -86,7 +82,7 @@ class AiMonitoringChatSubscriber extends AiMonitoringSubscriber {
86
82
  * @param {Array} params.tags used only for langchain events at the moment
87
83
  */
88
84
  recordChatCompletionEvents({ ctx, request, response, err, timeOfFirstToken, metadata = {}, tags = [] }) {
89
- if (!this.enabled) {
85
+ if (!this.aiEnabled) {
90
86
  this.logger.debug('config.ai_monitoring.enabled is set to false, not creating chat completion events.')
91
87
  return
92
88
  }
@@ -46,7 +46,7 @@ class AiMonitoringEmbeddingSubscriber extends AiMonitoringSubscriber {
46
46
  * @param {object} params.err error if present
47
47
  */
48
48
  recordEmbedding({ ctx, request, response, err }) {
49
- if (!this.enabled) {
49
+ if (!this.aiEnabled) {
50
50
  this.logger.debug('config.ai_monitoring.enabled is set to false, not creating embedding event.')
51
51
  return
52
52
  }
@@ -25,7 +25,7 @@ module.exports = class AnthropicChatCreateSubscriber extends AiMonitoringChatSub
25
25
 
26
26
  asyncEnd(data) {
27
27
  const { agent, logger } = this
28
- if (!this.enabled) {
28
+ if (!this.aiEnabled) {
29
29
  logger.debug('`ai_monitoring.enabled` is set to false, not creating chat completion events.')
30
30
  return
31
31
  }
@@ -22,6 +22,16 @@ class BunyanLoggerSubscriber extends BunyanSubscriber {
22
22
  const self = this
23
23
  if (this.isLogForwardingEnabled()) {
24
24
  const logger = data.self
25
+ // The `Logger` constructor runs for every logger, including every
26
+ // `logger.child(...)`. Child loggers inherit (copy) or share the parent's
27
+ // `streams` array, so re-adding our forwarder on each construction would
28
+ // accumulate duplicate `NRLogForwarder` streams and forward a single log
29
+ // line multiple times. Only add it if one isn't already present.
30
+ const alreadyForwarding = logger.streams?.some((s) => s.name === 'NRLogForwarder')
31
+ if (alreadyForwarding) {
32
+ return
33
+ }
34
+
25
35
  logger.addStream({
26
36
  name: 'NRLogForwarder',
27
37
  type: 'raw',
@@ -33,7 +33,7 @@ module.exports = class GoogleAdkAgentRunSubscriber extends AiMonitoringSubscribe
33
33
  // generator is created but before it is consumed by the caller.
34
34
  end(data) {
35
35
  const { agent, logger } = this
36
- if (!this.enabled) {
36
+ if (!this.aiEnabled) {
37
37
  logger.debug('Google ADK instrumentation is disabled, not instrumenting runAsync.')
38
38
  return
39
39
  }
@@ -36,7 +36,7 @@ module.exports = class GoogleAdkToolRunSubscriber extends AiMonitoringSubscriber
36
36
 
37
37
  asyncEnd(data) {
38
38
  const { agent, logger, toolName, toolInput } = this
39
- if (!this.enabled) {
39
+ if (!this.aiEnabled) {
40
40
  logger.debug('Google ADK instrumentation is disabled, not instrumenting FunctionTool.runAsync.')
41
41
  return
42
42
  }
@@ -76,11 +76,6 @@ class GoogleGenAIGenerateContentStreamSubscriber extends GoogleGenAIGenerateCont
76
76
  }
77
77
 
78
78
  asyncEnd(data) {
79
- // Check config to see if ai_monitoring is still enabled
80
- if (!this.enabled) {
81
- this.logger.debug('`ai_monitoring.enabled` is set to false, stream will not be instrumented.')
82
- return
83
- }
84
79
  if (!this.streamingEnabled) {
85
80
  this.logger.warn(
86
81
  '`ai_monitoring.streaming.enabled` is set to `false`, stream will not be instrumented.'
@@ -12,10 +12,6 @@ class LangchainRunnableStreamSubscriber extends LangchainRunnableSubscriber {
12
12
  }
13
13
 
14
14
  asyncEnd(data) {
15
- if (!this.enabled) {
16
- this.logger.debug('`ai_monitoring.enabled` is set to false, stream will not be instrumented.')
17
- return
18
- }
19
15
  if (!this.streamingEnabled) {
20
16
  this.logger.debug('`ai_monitoring.streaming.enabled` is set to false, stream will not be instrumented.')
21
17
  this.agent.metrics.getOrCreateMetric(STREAMING_DISABLED).incrementCallCount()
@@ -27,7 +27,7 @@ class LangchainToolSubscriber extends AiMonitoringSubscriber {
27
27
  asyncEnd(data) {
28
28
  // Extract data and exit early if need be.
29
29
  const { agent, logger } = this
30
- if (!this.enabled) {
30
+ if (!this.aiEnabled) {
31
31
  logger.debug('Langchain instrumentation is disabled, not recording Llm events.')
32
32
  return
33
33
  }
@@ -84,7 +84,7 @@ class LangchainVectorstoreSubscriber extends AiMonitoringSubscriber {
84
84
  }
85
85
 
86
86
  asyncEnd(data) {
87
- if (!this.enabled) {
87
+ if (!this.aiEnabled) {
88
88
  this.logger.debug('Langchain instrumentation is disabled, not recording Llm events.')
89
89
  return
90
90
  }
@@ -21,10 +21,6 @@ class LangGraphStreamSubscriber extends AiMonitoringSubscriber {
21
21
  this.events = ['asyncEnd']
22
22
  }
23
23
 
24
- get streamingEnabled() {
25
- return this.agent.config.ai_monitoring.streaming.enabled
26
- }
27
-
28
24
  handler(data, ctx) {
29
25
  // Store LangGraph AI agent name and update
30
26
  // segment name to use it.
@@ -35,10 +31,7 @@ class LangGraphStreamSubscriber extends AiMonitoringSubscriber {
35
31
 
36
32
  asyncEnd(data) {
37
33
  const { agent, logger, aiAgentName } = this
38
- if (!this.enabled) {
39
- logger.debug('LangGraph instrumentation is disabled, not instrumenting stream.')
40
- return
41
- }
34
+
42
35
  if (!this.streamingEnabled) {
43
36
  logger.debug('LangGraph streaming instrumentation is disabled, not instrumenting stream.')
44
37
  this.agent.metrics.getOrCreateMetric(STREAMING_DISABLED).incrementCallCount()
@@ -35,7 +35,7 @@ class OpenAIChatCompletions extends AiMonitoringChatSubscriber {
35
35
  }
36
36
 
37
37
  asyncEnd(data) {
38
- if (!this.enabled) {
38
+ if (!this.aiEnabled) {
39
39
  this.logger.debug('`ai_monitoring.enabled` is set to false, stream will not be instrumented.')
40
40
  return
41
41
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "14.2.1",
3
+ "version": "14.3.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [