newrelic 13.6.4 → 13.6.6

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,11 +1,40 @@
1
- ### v13.6.4 (2025-11-13)
2
-
3
- #### Bug fixes
4
-
5
- * Updated `MessageConsumerSubscriber` to end transaction created for a given message consumption ([#3503](https://github.com/newrelic/node-newrelic/pull/3503)) ([ba30818](https://github.com/newrelic/node-newrelic/commit/ba308184e5cb4f916aec53963cce0caa9149efbe))
6
- * Updated `MessageProducerSubscriber` to properly set the sampled flag on `traceparent` ([#3505](https://github.com/newrelic/node-newrelic/pull/3505)) ([0b1a9a8](https://github.com/newrelic/node-newrelic/commit/0b1a9a80b81e0f4388d421f17176eab386d04cbc))
7
- * Updated the bedrock middleware to be registered with a `high` priority to ensure payload is properly deserialized ([#3500](https://github.com/newrelic/node-newrelic/pull/3500)) ([63e531b](https://github.com/newrelic/node-newrelic/commit/63e531bf50d600d92556b3785725dd38145f3491))
8
-
1
+ ### v13.6.6 (2025-11-19)
2
+
3
+ #### Bug fixes
4
+
5
+ * Updated `app.use` or `router.use` Express instrumentation to properly wrap all middleware defined ([#3520](https://github.com/newrelic/node-newrelic/pull/3520)) ([d3b20d0](https://github.com/newrelic/node-newrelic/commit/d3b20d09a9c3d21bff081771d8c11caff9c6a6e5))
6
+
7
+ #### Documentation
8
+
9
+ * Updated compatibility report ([#3522](https://github.com/newrelic/node-newrelic/pull/3522)) ([72ec652](https://github.com/newrelic/node-newrelic/commit/72ec65252700ab5f3831dab35d05b607a0723aa6)) ([#3516](https://github.com/newrelic/node-newrelic/pull/3516)) ([35defb0](https://github.com/newrelic/node-newrelic/commit/35defb009b7d3bbec0ffe084b434e368aa1cbc7c))
10
+
11
+ #### Tests
12
+
13
+ * Pinned next.js to skip 11.0.10 and 11.0.11 ([#3521](https://github.com/newrelic/node-newrelic/pull/3521)) ([20977ee](https://github.com/newrelic/node-newrelic/commit/20977eeb8552b7a445f5407d8df272aae49ea873))
14
+
15
+ ### v13.6.5 (2025-11-17)
16
+
17
+ #### Bug fixes
18
+
19
+ * Updated express instrumentation to ignore error when the `next` handler passes `route` or `router` ([#3513](https://github.com/newrelic/node-newrelic/pull/3513)) ([724b218](https://github.com/newrelic/node-newrelic/commit/724b218174bddcf2ecf8cffc317bbfad1f0012eb))
20
+ * Updated MessageConsumerSubscriber to wait for consumer callback to end when it is a promise ([#3510](https://github.com/newrelic/node-newrelic/pull/3510)) ([45667a0](https://github.com/newrelic/node-newrelic/commit/45667a0ce2aaaf57a5e7a8acc8f0feea31e6152f))
21
+
22
+ #### Documentation
23
+
24
+ * Updated compatibility report ([#3498](https://github.com/newrelic/node-newrelic/pull/3498)) ([ef41b38](https://github.com/newrelic/node-newrelic/commit/ef41b3851b2096035d5b7b655d5243a9aeb699c8))
25
+
26
+ #### Tests
27
+
28
+ * Updated aws sdk v3 s3 test to no longer test DeleteBucketCommand ([#3512](https://github.com/newrelic/node-newrelic/pull/3512)) ([df27448](https://github.com/newrelic/node-newrelic/commit/df274484e0e657fd9c38452e500450878fc24ee4))
29
+
30
+ ### v13.6.4 (2025-11-13)
31
+
32
+ #### Bug fixes
33
+
34
+ * Updated `MessageConsumerSubscriber` to end transaction created for a given message consumption ([#3503](https://github.com/newrelic/node-newrelic/pull/3503)) ([ba30818](https://github.com/newrelic/node-newrelic/commit/ba308184e5cb4f916aec53963cce0caa9149efbe))
35
+ * Updated `MessageProducerSubscriber` to properly set the sampled flag on `traceparent` ([#3505](https://github.com/newrelic/node-newrelic/pull/3505)) ([0b1a9a8](https://github.com/newrelic/node-newrelic/commit/0b1a9a80b81e0f4388d421f17176eab386d04cbc))
36
+ * Updated the bedrock middleware to be registered with a `high` priority to ensure payload is properly deserialized ([#3500](https://github.com/newrelic/node-newrelic/pull/3500)) ([63e531b](https://github.com/newrelic/node-newrelic/commit/63e531bf50d600d92556b3785725dd38145f3491))
37
+
9
38
  ### v13.6.3 (2025-11-11)
10
39
 
11
40
  #### Bug fixes
@@ -8048,3 +8077,5 @@ Special thanks to Ryan Copley (@RyanCopley) for the contribution.
8048
8077
 
8049
8078
 
8050
8079
 
8080
+
8081
+
@@ -8,6 +8,7 @@
8
8
  const defaultConfig = module.exports
9
9
  const { array, int, float, boolean, object, objectList, allowList, regex } = require('./formatters')
10
10
  const pkgInstrumentation = require('./build-instrumentation-config')
11
+ const { config: samplerConfigs } = require('./samplers')
11
12
 
12
13
  /**
13
14
  * A function that returns the definition of the agent configuration
@@ -1156,6 +1157,7 @@ defaultConfig.definition = () => {
1156
1157
  },
1157
1158
 
1158
1159
  sampler: {
1160
+ ...samplerConfigs,
1159
1161
  /**
1160
1162
  * The sampling target for adaptive sampling is controlled via this attribute when configuring the default/adaptive sampler.
1161
1163
  * The default sampling target is 10 transactions/min when it is not specified but **MUST** be within the range of [1, 120] (inclusive).
@@ -1166,77 +1168,23 @@ defaultConfig.definition = () => {
1166
1168
  formatter: int,
1167
1169
  default: 10
1168
1170
  },
1169
- // The root sampling options are defined in an unusual way compared to the rest of our configuration but this is done to
1170
- // allow compatibility with how OpenTelemetry sets their samplers and be backwards compatible as well. Root can either be
1171
- // a string or an object.
1172
- //
1173
- // Example setting root sampler via config to a string value -
1174
- // root: 'always_on'
1175
- //
1176
- // Example setting root sampler via config to trace id ratio based -
1177
- // root: {
1178
- // trace_id_ratio_based: {
1179
- // ratio: 0.5
1180
- // }
1181
- // }
1182
- //
1183
- root: {
1184
- formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off', 'default']),
1185
- default: 'default',
1186
- // if setting root to trace_id_ratio_based via env vars, set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT: 'trace_id_ratio_based'
1187
- // and then set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT_TRACE_ID_RATIO_BASED_RATIO to the desired ratio
1188
- trace_id_ratio_based: {
1189
- ratio: {
1190
- formatter: float,
1191
- env: 'NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT_TRACE_ID_RATIO_BASED_RATIO'
1192
- }
1193
- }
1194
- },
1195
-
1196
- /**
1197
- * When set to `always_on`, the sampled flag in the `traceparent` header
1198
- * being set to "true" will result in the local transaction being sampled
1199
- * with a priority value of "2". When set to `always_off`, the local
1200
- * transaction will never be sampled. At the default setting, the sampling
1201
- * decision will be determined according to the normal algorithm.
1202
- *
1203
- * This setting takes precedence over the `remote_parent_not_sampled`
1204
- * setting.
1205
- */
1206
- remote_parent_sampled: {
1207
- formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off', 'default']),
1208
- default: 'default',
1209
- // if setting remote_parent_sampled to trace_id_ratio_based via env vars, set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED: 'trace_id_ratio_based'
1210
- // and then set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED_TRACE_ID_RATIO_BASED_RATIO to the desired ratio
1211
- trace_id_ratio_based: {
1212
- ratio: {
1213
- formatter: float,
1214
- env: 'NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED_TRACE_ID_RATIO_BASED_RATIO'
1215
- }
1216
- }
1171
+ full_granularity: {
1172
+ enabled: {
1173
+ default: true,
1174
+ formatter: boolean
1175
+ },
1176
+ ...samplerConfigs
1217
1177
  },
1218
-
1219
- /**
1220
- * When set to `always_on`, the local transaction will be sampled with a
1221
- * priority of "2".
1222
- * When set to `always_off`, the local transaction will never be sampled.
1223
- * At the default setting, the sampling decision will be determined
1224
- * according to the normal algorithm.
1225
- *
1226
- * This setting only affects decisions when the traceparent sampled flag
1227
- * is set to 0.
1228
- */
1229
- remote_parent_not_sampled: {
1230
- formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off', 'default']),
1231
- default: 'default',
1232
- // if setting remote_parent_not_sampled to trace_id_ratio_based via env vars, set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED: 'trace_id_ratio_based'
1233
- // and then set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED_TRACE_ID_RATIO_BASED_RATIO to the desired ratio
1234
- trace_id_ratio_based: {
1235
- ratio: {
1236
- formatter: float,
1237
- env: 'NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED_TRACE_ID_RATIO_BASED_RATIO'
1238
- }
1239
- }
1178
+ partial_granularity: {
1179
+ enabled: {
1180
+ default: false,
1181
+ formatter: boolean
1182
+ },
1183
+ type: {
1184
+ formatter: allowList.bind(null, ['compact', 'essential', 'reduced']),
1185
+ default: 'essential'
1186
+ },
1187
+ ...samplerConfigs
1240
1188
  }
1241
1189
  }
1242
1190
  },
@@ -1909,4 +1857,5 @@ function setNestedKey(obj, keys, value) {
1909
1857
 
1910
1858
  obj[keys[len - 1]] = value
1911
1859
  }
1860
+
1912
1861
  defaultConfig.setNestedKey = setNestedKey
@@ -25,6 +25,7 @@ const mergeServerConfig = new MergeServerConfig()
25
25
  const { boolean: isTruthular } = require('./formatters')
26
26
  const configDefinition = definition()
27
27
  const parseLabels = require('../util/label-parser')
28
+ const { buildTraceIdRatioSamplers, setTraceIdRatioSamplerFromEnv } = require('./samplers')
28
29
 
29
30
  /**
30
31
  * CONSTANTS -- we gotta lotta 'em
@@ -1065,7 +1066,7 @@ Config.prototype._fromPassed = function _fromPassed(external, internal, arbitrar
1065
1066
  }
1066
1067
 
1067
1068
  // Handle trace_id_ratio_based for distributed tracing sampler configuration
1068
- this._buildTraceIdRatioSamplers(key, internal, external[key])
1069
+ buildTraceIdRatioSamplers({ config: external, key, incomingConfig: internal, logger })
1069
1070
 
1070
1071
  if (key === 'ssl' && !isTruthular(external.ssl)) {
1071
1072
  logger.warn(SSL_WARNING)
@@ -1090,54 +1091,6 @@ Config.prototype._fromPassed = function _fromPassed(external, internal, arbitrar
1090
1091
  }
1091
1092
  }
1092
1093
 
1093
- /**
1094
- * Builds the trace_id_ratio_based for distributed tracing sampler configuration structure
1095
- *
1096
- * @param {string} key - The configuration key being processed
1097
- * @param {object} internal - The internal configuration object being modified
1098
- * @param {object} samplerConfig - Sampler configuration object from defaultsConfig
1099
- */
1100
- Config.prototype._buildTraceIdRatioSamplers = function _buildTraceIdRatioSamplers(key, internal, samplerConfig) {
1101
- if (key !== 'sampler') {
1102
- return
1103
- }
1104
-
1105
- const samplers = Object.keys(samplerConfig)
1106
-
1107
- // user can set multiple samplers
1108
- for (const sampler of samplers) {
1109
- if (this._isTraceIdRatioBasedConfig(sampler, samplerConfig)) {
1110
- const ratioValue = samplerConfig[sampler].trace_id_ratio_based.ratio
1111
- if (ratioValue) {
1112
- internal[key][sampler] = {
1113
- trace_id_ratio_based: {
1114
- ratio: ratioValue
1115
- }
1116
- }
1117
- logger.trace('Setting trace id ratio based sampler on %s', samplers)
1118
- } else {
1119
- logger.error('Not setting trace id ratio based sampler on %s.', samplers)
1120
- }
1121
- }
1122
- }
1123
- }
1124
-
1125
- /**
1126
- * Checks if the external config contains a trace_id_ratio_based for distributed tracing
1127
- * sampler configuration
1128
- *
1129
- * @param {string} selectedSampler - The selected sampler key
1130
- * @param {object} samplerConfig - The sampler configuration object
1131
- * @returns {boolean} true if it's a trace_id_ratio_based config
1132
- */
1133
- Config.prototype._isTraceIdRatioBasedConfig = function _isTraceIdRatioBasedConfig(selectedSampler, samplerConfig) {
1134
- const samplers = ['root', 'remote_parent_sampled', 'remote_parent_not_sampled']
1135
-
1136
- return samplers.includes(selectedSampler) &&
1137
- typeof samplerConfig[selectedSampler] === 'object' &&
1138
- 'trace_id_ratio_based' in samplerConfig[selectedSampler]
1139
- }
1140
-
1141
1094
  /**
1142
1095
  * Some values should be picked up only if they're not otherwise set, like
1143
1096
  * the Windows / Azure application name. Don't set it if there's already
@@ -1214,53 +1167,6 @@ function setFromEnv({ config, key, envVar, formatter, paths }) {
1214
1167
  }
1215
1168
  }
1216
1169
 
1217
- /**
1218
- * Assigns the value of the distributed tracing sampler env var as an trace_id_ratio_based
1219
- * object to the sampler in the config.
1220
- *
1221
- * @param {object} params object passed to fn
1222
- * @param {string} params.key key of the sampler
1223
- * Example: 'root' or 'remote_parent_sampled'
1224
- * @param {object} params.value value of the sampler
1225
- * Example: { default: 'default', formatter: someFunc, trace_id_ratio_based: {ratio} }
1226
- * @param {object} params.config agent config
1227
- * @param {Array} params.paths list of leaf nodes leading to the sampling configuration value
1228
- * Example: ['distributed_tracing', 'sampler']
1229
- */
1230
- function setTraceIDRatioSampler({ key, value, config, paths }) {
1231
- // trace id ratio based sampler is nested in leaf nodes under distributed_tracing > samplers > key
1232
- // so don't continue if path is empty
1233
- if (paths.length === 0) {
1234
- return
1235
- }
1236
-
1237
- const samplers = ['root', 'remote_parent_sampled', 'remote_parent_not_sampled']
1238
- if (samplers.includes(key) && paths[paths.length - 1] === 'sampler') {
1239
- // config.distributed_tracing.sampler.[TYPE OF SAMPLER] needs to be set to trace_id_ratio_based
1240
- // in order to continue
1241
- if (config.distributed_tracing.sampler[key] !== 'trace_id_ratio_based') {
1242
- return
1243
- }
1244
-
1245
- // set the trace id based ratio
1246
- const envVar = value.trace_id_ratio_based.ratio.env
1247
- const formatter = value.trace_id_ratio_based.ratio.formatter
1248
- const setting = process.env[envVar]
1249
- if (setting) {
1250
- const formattedSetting = formatter(setting)
1251
- config.distributed_tracing.sampler[key] = {
1252
- trace_id_ratio_based: {
1253
- ratio: formattedSetting
1254
- }
1255
- }
1256
- logger.trace('Setting %s environment variable', envVar)
1257
- } else {
1258
- logger.error('Not setting %s environment variable. Setting sampler root to default.', envVar)
1259
- config.distributed_tracing.sampler.root = 'default'
1260
- }
1261
- }
1262
- }
1263
-
1264
1170
  /**
1265
1171
  * Recursively visit the nodes of the config definition and look for environment variable names, overriding any configuration values that are found.
1266
1172
  *
@@ -1304,11 +1210,12 @@ Config.prototype._fromEnvironment = function _fromEnvironment(
1304
1210
  // since sampler was set to a string but now has to be converted to an object.
1305
1211
  // This is called here because we need set the sampler to a value first in config before setting
1306
1212
  // trace id ratio sampler (if the user set that)
1307
- setTraceIDRatioSampler({
1213
+ setTraceIdRatioSamplerFromEnv({
1308
1214
  key,
1309
- value,
1310
1215
  config,
1311
- paths
1216
+ paths,
1217
+ logger,
1218
+ setNestedKey
1312
1219
  })
1313
1220
  continue
1314
1221
  }
@@ -0,0 +1,196 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const { allowList, float } = require('./formatters')
8
+
9
+ /**
10
+ * This configuration is currently the same for `distributed_tracing.sampler`,
11
+ * `distributed_tracing.sampler.full_granularity` and `distributed_tracing.sampler.partial_granularity`
12
+ * Note: This also just defines the outer structure. These values are mixed data types. Most of the time
13
+ * it is just a string. But if you want to use the `trace_id_ratio_based` sampler, then you need to
14
+ * provide an object with a `ratio` property. All of that mapping is done in `lib/config/index.js`
15
+ * This is done to allow compatibility with how OpenTelemetry sets their samplers and be backwards compatible as well.
16
+ */
17
+ const config = {
18
+ /**
19
+ * Example setting root sampler via config to a string value -
20
+ * root: 'always_on'
21
+ *
22
+ * Example setting root sampler via config to trace id ratio based -
23
+ * root: {
24
+ * trace_id_ratio_based: {
25
+ * ratio: 0.5
26
+ * }
27
+ * }
28
+ */
29
+ root: {
30
+ formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off', 'default']),
31
+ default: 'default',
32
+ },
33
+
34
+ /**
35
+ * When set to `always_on`, the sampled flag in the `traceparent` header
36
+ * being set to "true" will result in the local transaction being sampled
37
+ * with a priority value of "2". When set to `always_off`, the local
38
+ * transaction will never be sampled. At the default setting, the sampling
39
+ * decision will be determined according to the normal algorithm.
40
+ *
41
+ * This setting takes precedence over the `remote_parent_not_sampled`
42
+ * setting.
43
+ */
44
+ remote_parent_sampled: {
45
+ formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off', 'default']),
46
+ default: 'default',
47
+ },
48
+
49
+ /**
50
+ * When set to `always_on`, the local transaction will be sampled with a
51
+ * priority of "2".
52
+ * When set to `always_off`, the local transaction will never be sampled.
53
+ * At the default setting, the sampling decision will be determined
54
+ * according to the normal algorithm.
55
+ *
56
+ * This setting only affects decisions when the traceparent sampled flag
57
+ * is set to 0.
58
+ */
59
+ remote_parent_not_sampled: {
60
+ formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off', 'default']),
61
+ default: 'default',
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Builds the trace_id_ratio_based for distributed tracing sampler configuration structure
67
+ *
68
+ * @param {object} params to function
69
+ * @param {object} params.config - The full configuration object
70
+ * @param {string} params.key - The configuration key being processed
71
+ * @param {object} params.incomingConfig The internal configuration object being modified
72
+ * @param {Logger} params.logger - The logger instance
73
+ */
74
+ function buildTraceIdRatioSamplers({ config, key, incomingConfig, logger }) {
75
+ if (!isValidDTSampler(key)) {
76
+ return
77
+ }
78
+
79
+ const samplerConfig = config[key]
80
+ const samplers = Object.keys(samplerConfig)
81
+
82
+ // user can set multiple samplers
83
+ for (const sampler of samplers) {
84
+ if (isTraceIdRatioBasedConfig(sampler, samplerConfig)) {
85
+ const ratioValue = samplerConfig[sampler].trace_id_ratio_based.ratio
86
+ if (ratioValue) {
87
+ incomingConfig[key][sampler] = {
88
+ trace_id_ratio_based: {
89
+ ratio: ratioValue
90
+ }
91
+ }
92
+ logger.trace('Setting trace_id_ratio_based.ratio on %s.%s', key, sampler)
93
+ } else {
94
+ logger.trace('Not setting trace_id_ratio_based on %s.%s as ratio value is not present.', key, sampler)
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Checks if the external config contains a trace_id_ratio_based for distributed tracing
102
+ * sampler configuration
103
+ *
104
+ * @param {string} sampler - The selected sampler key
105
+ * @param {object} samplerConfig - The sampler configuration object
106
+ * @returns {boolean} true if it's a trace_id_ratio_based config
107
+ */
108
+ function isTraceIdRatioBasedConfig(sampler, samplerConfig) {
109
+ return isValidDTSamplerType(sampler) &&
110
+ typeof samplerConfig[sampler] === 'object' &&
111
+ 'trace_id_ratio_based' in samplerConfig[sampler]
112
+ }
113
+
114
+ /**
115
+ * Check if the sampler is a valid value of either 'sampler', 'full_granularity', or 'partial_granularity'
116
+ * @param {string} sampler - The sampler key to validate
117
+ * @returns {boolean} true if valid, false otherwise
118
+ */
119
+ function isValidDTSampler(sampler) {
120
+ return ['sampler', 'full_granularity', 'partial_granularity'].includes(sampler)
121
+ }
122
+
123
+ /**
124
+ * Check if the sampler type is a valid value of either 'root', 'remote_parent_sampled', or 'remote_parent_not_sampled'.
125
+ * @param {string} samplerType - The sampler type to validate
126
+ * @returns {boolean} true if valid, false otherwise
127
+ */
128
+ function isValidDTSamplerType(samplerType) {
129
+ return ['root', 'remote_parent_sampled', 'remote_parent_not_sampled'].includes(samplerType)
130
+ }
131
+
132
+ /**
133
+ * Assigns the value of the distributed tracing samplers env var as an trace_id_ratio_based
134
+ * object to the sampler in the config.
135
+ *
136
+ * @param {object} params object passed to fn
137
+ * @param {string} params.key key of the sampler
138
+ * Example: 'root' or 'remote_parent_sampled'
139
+ * @param {object} params.config agent config
140
+ * @param {Array} params.paths list of leaf nodes leading to the sampling configuration value
141
+ * Example: ['distributed_tracing', 'sampler']
142
+ * @param {Function} params.setNestedKey function to set nested key in config
143
+ * @param {Logger} params.logger logger instance
144
+ */
145
+ function setTraceIdRatioSamplerFromEnv({ key, config, paths, setNestedKey, logger }) {
146
+ // trace id ratio based sampler is nested in leaf nodes under distributed_tracing > samplers > key
147
+ // so don't continue if path is empty
148
+ if (paths.length === 0) {
149
+ return
150
+ }
151
+
152
+ const lastPath = paths[paths.length - 1]
153
+ if (isValidDTSampler(lastPath) && isValidDTSamplerType(key)) {
154
+ // Get the value of `config.distributed_tracing.sampler[key]` or `config.distributed_tracing.sampler.full_granularity[key]` or `config.distributed_tracing.sampler.partial_granularity[key]`
155
+ const nestedValue = getNestedValue(config, [...paths, key])
156
+ if (nestedValue !== 'trace_id_ratio_based') {
157
+ return
158
+ }
159
+
160
+ // set the trace id based ratio
161
+ const envVar = `NEW_RELIC_${[...paths, key, 'trace_id_ratio_based', 'ratio'].join('_').toUpperCase()}`
162
+ const setting = process.env[envVar]
163
+ if (setting) {
164
+ const formattedSetting = float(setting)
165
+ setNestedKey(config, [...paths, key], { trace_id_ratio_based: { ratio: formattedSetting } })
166
+ logger.trace('Setting %s environment variable to %s', envVar, formattedSetting)
167
+ } else {
168
+ logger.trace('Not setting %s environment variable. Setting %s.%s to `default`', envVar, paths.join('.'), key)
169
+ setNestedKey(config, [...paths, key], 'default')
170
+ }
171
+ }
172
+ }
173
+ /**
174
+ * Retrieves a value from a nested object by providing the list of parent keys.
175
+ * @param {object} obj object to assign value to
176
+ * @param {Array} keys list of parent keys
177
+ * @returns {*} value of the nested object key
178
+ */
179
+ function getNestedValue(obj, keys) {
180
+ const len = keys.length
181
+ for (let i = 0; i < len - 1; i++) {
182
+ const elem = keys[i]
183
+ if (!obj[elem]) {
184
+ obj[elem] = {}
185
+ }
186
+
187
+ obj = obj[elem]
188
+ }
189
+ return obj[keys[len - 1]]
190
+ }
191
+
192
+ module.exports = {
193
+ config,
194
+ buildTraceIdRatioSamplers,
195
+ setTraceIdRatioSamplerFromEnv
196
+ }
@@ -9,8 +9,11 @@ const isString = require('#agentlib/util/is-string.js')
9
9
 
10
10
  /**
11
11
  * This subscriber is two-fold:
12
- * 1. capture the connection arguments and assign to symbol on connection to be used for all other amqplib subscribers
13
- * 2. create a segment for the connect. Not this will attempt to create a segment but the base subscriber will decide if it is created or not, otherwise it'll return the current context
12
+ * 1. Capture the connection arguments and assign to symbol on connection to be
13
+ * used for all other amqplib subscribers.
14
+ * 2. Create a segment for the connect. Note this will attempt to create a segment
15
+ * but the base subscriber will decide if it is created or not, otherwise it'll
16
+ * return the current context.
14
17
  */
15
18
  class ConnectSubscriber extends Subscriber {
16
19
  constructor({ agent, logger }) {
@@ -114,7 +114,9 @@ class Subscriber {
114
114
  asyncStart.runStores(context, () => {
115
115
  try {
116
116
  if (callback) {
117
- return Reflect.apply(callback, this, arguments)
117
+ const cbResult = Reflect.apply(callback, this, arguments)
118
+ context.cbResult = cbResult
119
+ return cbResult
118
120
  }
119
121
  } finally {
120
122
  asyncEnd.publish(context)
@@ -0,0 +1,26 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ const MiddlewareSubscriber = require('../middleware')
7
+
8
+ /**
9
+ * Special error handler to ignore if the error passed to next handler
10
+ * is 'route' or 'router', which are used by Express internally to
11
+ * skip out of a route or router.
12
+ *
13
+ * @param {*} err - The error passed to the next handler
14
+ * @returns {boolean} - returns true if error exists and not equal to a string or `route` or `router`
15
+ */
16
+ function errorHandler(err) {
17
+ return err && err !== 'route' && err !== 'router'
18
+ }
19
+
20
+ class ExpressSubscriber extends MiddlewareSubscriber {
21
+ constructor({ agent, logger, packageName = 'express', channelName }) {
22
+ super({ agent, logger, packageName, channelName, system: 'Expressjs', errorHandler })
23
+ }
24
+ }
25
+
26
+ module.exports = ExpressSubscriber
@@ -5,11 +5,11 @@
5
5
 
6
6
  'use strict'
7
7
 
8
- const MiddlewareSubscriber = require('../middleware')
8
+ const ExpressSubscriber = require('./base')
9
9
 
10
- class ExpressParamSubscriber extends MiddlewareSubscriber {
11
- constructor({ agent, logger, packageName = 'express' }) {
12
- super({ agent, logger, packageName, channelName: 'nr_param', system: 'Expressjs' })
10
+ class ExpressParamSubscriber extends ExpressSubscriber {
11
+ constructor({ agent, logger, packageName }) {
12
+ super({ agent, logger, packageName, channelName: 'nr_param' })
13
13
  }
14
14
 
15
15
  handler(data) {
@@ -5,12 +5,12 @@
5
5
 
6
6
  'use strict'
7
7
 
8
- const MiddlewareSubscriber = require('../middleware')
9
8
  const methods = ['all', 'delete', 'get', 'head', 'post', 'put', 'patch']
9
+ const ExpressSubscriber = require('./base')
10
10
 
11
- class ExpressRouteSubscriber extends MiddlewareSubscriber {
12
- constructor({ agent, logger, packageName = 'express' }) {
13
- super({ agent, logger, packageName, channelName: 'nr_route', system: 'Expressjs' })
11
+ class ExpressRouteSubscriber extends ExpressSubscriber {
12
+ constructor({ agent, logger, packageName }) {
13
+ super({ agent, logger, packageName, channelName: 'nr_route' })
14
14
  this.events = ['end']
15
15
  }
16
16
 
@@ -5,41 +5,92 @@
5
5
 
6
6
  'use strict'
7
7
 
8
- const MiddlewareSubscriber = require('../middleware')
8
+ const ExpressSubscriber = require('./base')
9
9
 
10
- class ExpressUseSubscriber extends MiddlewareSubscriber {
11
- constructor({ agent, logger, packageName = 'express' }) {
12
- super({ agent, logger, packageName, channelName: 'nr_use', system: 'Expressjs' })
10
+ class ExpressUseSubscriber extends ExpressSubscriber {
11
+ constructor({ agent, logger, packageName }) {
12
+ super({ agent, logger, packageName, channelName: 'nr_use' })
13
13
  }
14
14
 
15
15
  handler(data) {
16
16
  const { arguments: args } = data
17
- let [route, fn] = args
18
- let fnIndex = 1
19
- let method = null
20
- if (typeof route === 'function') {
21
- fn = route
22
- route = null
23
- fnIndex = 0
24
- }
25
- let segmentName = null
17
+ const route = this.routeParser(args[0])
26
18
  // sometimes route is undefined, default to `/` for segmentName only
27
19
  const routeName = route || '/'
28
- // Pre v5 these were marked as `lazyrouter`
29
- // check for both
30
- if (fn.lazyrouter || fn.name === 'mounted_app') {
31
- segmentName = `${this.wrapper.system}/Mounted App: ${routeName}`
32
- } else if (fn.stack) {
33
- segmentName = `${this.wrapper.system}/Router: ${routeName}`
34
- method = 'handle'
20
+ this.wrapAllMiddleware({
21
+ middlewares: args,
22
+ route,
23
+ routeName
24
+ })
25
+ }
26
+
27
+ /**
28
+ * Iterates over all arguments passed to `.use` and wraps any middleware functions
29
+ * They can be:
30
+ * 1. single middleware
31
+ * 2. series of middleware
32
+ * 3. array of middleware
33
+ * 4. combination of any mentioned above
34
+ *
35
+ * @param {object} params to function
36
+ * @param {Array} params.middlewares array of middleware to wrap
37
+ * @param {string|null} params.route route defined in `.use`
38
+ * @param {string} params.routeName route name for segment naming
39
+ */
40
+ wrapAllMiddleware({ middlewares, route, routeName }) {
41
+ for (let i = 0; i < middlewares.length; i++) {
42
+ const middleware = middlewares[i]
43
+ if (Array.isArray(middleware)) {
44
+ this.wrapAllMiddleware({ middlewares: middleware, route })
45
+ continue
46
+ }
47
+
48
+ // we only want to wrap functions
49
+ // This is the route definition not middleware
50
+ if (typeof middleware !== 'function') {
51
+ continue
52
+ }
53
+
54
+ let segmentName = null
55
+ let method = null
56
+ // Pre v5 these were marked as `lazyrouter`
57
+ // check for both
58
+ if (middleware?.lazyrouter || middleware?.name === 'mounted_app') {
59
+ segmentName = `${this.wrapper.system}/Mounted App: ${routeName}`
60
+ } else if (middleware?.stack) {
61
+ segmentName = `${this.wrapper.system}/Router: ${routeName}`
62
+ method = 'handle'
63
+ }
64
+
65
+ const wrappedMw = this.wrapper.wrap({
66
+ handler: middleware[method] ?? middleware,
67
+ route,
68
+ segmentName
69
+ })
70
+ if (method) {
71
+ middlewares[i][method] = wrappedMw
72
+ } else {
73
+ middlewares[i] = wrappedMw
74
+ }
35
75
  }
76
+ }
36
77
 
37
- const wrappedFn = this.wrapper.wrap({ handler: fn[method] ?? fn, route, segmentName })
38
- if (method) {
39
- data.arguments[fnIndex][method] = wrappedFn
40
- } else {
41
- data.arguments[fnIndex] = wrappedFn
78
+ /**
79
+ * Handles extracting the route from a `.use` method.
80
+ * If a route is not defined it defaults to `null`
81
+ *
82
+ * @param {*} route first arg passed to `.use`
83
+ * @returns {*} route or null
84
+ */
85
+ routeParser(route) {
86
+ if (route instanceof RegExp) {
87
+ return `/${route.source}/`
88
+ } else if (typeof route === 'string') {
89
+ return route
90
+ } else if (Array.isArray(route) && typeof route[0] !== 'function') {
91
+ return route.join(',')
42
92
  }
93
+ return null
43
94
  }
44
95
  }
45
96
 
@@ -66,11 +66,22 @@ class MessageConsumerSubscriber extends Subscriber {
66
66
  }
67
67
 
68
68
  /**
69
- * Ends the transaction created for the consumption callback.
69
+ * Checks the result of the message handler.
70
+ * If it's a promise, waits for it to resolve before ending transaction.
71
+ * This ensures that the transaction stays active until the promise resolves.
72
+ * @param {object} data the data associated with the `asyncEnd` event
70
73
  */
71
- asyncEnd() {
74
+ asyncEnd(data) {
72
75
  const ctx = this.agent.tracer.getContext()
73
- ctx?.transaction?.end()
76
+ const result = data.cbResult
77
+ if (typeof result?.then === 'function') {
78
+ const prom = Promise.resolve(result)
79
+ prom.finally(() => {
80
+ ctx?.transaction?.end()
81
+ })
82
+ } else {
83
+ ctx?.transaction?.end()
84
+ }
74
85
  }
75
86
 
76
87
  enable() {
@@ -19,6 +19,17 @@ function getFunctionName(handler) {
19
19
  return handler.name === '' ? '<anonymous>' : handler.name
20
20
  }
21
21
 
22
+ /**
23
+ * Default error handler for determining if error should stored with transaction.
24
+ * Note: Based on previous shim instrumentation only Express, Restify, and Hapi have
25
+ * a different handler.
26
+ * @param {Error} err error passed to done handler
27
+ * @returns {boolean} returns true if error exists
28
+ */
29
+ function defaultErrorHandler(err) {
30
+ return err
31
+ }
32
+
22
33
  /**
23
34
  * The baseline parameters available to the middleware wrapper
24
35
  *
@@ -26,6 +37,7 @@ function getFunctionName(handler) {
26
37
  * @property {object} agent A New Relic Node.js agent instance.
27
38
  * @property {object} logger An agent logger instance.
28
39
  * @property {string} system handling the instrumentation(i.e - Fastify, Expressjs, Hapi, etc)
40
+ * @property {Function} [errorHandler] optional function to determine if error should be recorded
29
41
  */
30
42
 
31
43
  /**
@@ -36,13 +48,14 @@ function getFunctionName(handler) {
36
48
  * @property {string} prefix formatted prefix to name segments/timeslice metrics
37
49
  */
38
50
  class MiddlewareWrapper {
39
- constructor({ agent, logger, system }) {
51
+ constructor({ agent, logger, system, errorHandler }) {
40
52
  this.agent = agent
41
53
  this.logger = logger
42
54
  this.system = system
43
55
  this.config = agent.config
44
56
  this.prefix = `Nodejs/Middleware/${this.system}`
45
57
  this.agent.environment.setFramework(system)
58
+ this.isError = errorHandler ?? defaultErrorHandler
46
59
  }
47
60
 
48
61
  /**
@@ -180,7 +193,7 @@ class MiddlewareWrapper {
180
193
  if (isSync) {
181
194
  function wrappedDone(...doneArgs) {
182
195
  const [err] = doneArgs
183
- if (err) {
196
+ if (self.isError(err)) {
184
197
  self.storeError(txInfo, err)
185
198
  // route has been completed, pop from path
186
199
  // to allow other handlers to name it more accurately
@@ -8,12 +8,12 @@ const Subscriber = require('./base')
8
8
  const MiddlewareWrapper = require('./middleware-wrapper')
9
9
 
10
10
  class MiddlewareSubscriber extends Subscriber {
11
- constructor({ agent, logger, packageName, channelName, system }) {
11
+ constructor({ agent, logger, packageName, channelName, system, errorHandler }) {
12
12
  super({ agent, logger, packageName, channelName })
13
13
  // this is because the handler simply wraps a function
14
14
  // that is executed later when a request is made
15
15
  this.requireActiveTx = false
16
- this.wrapper = new MiddlewareWrapper({ agent, logger, system })
16
+ this.wrapper = new MiddlewareWrapper({ agent, logger, system, errorHandler })
17
17
  }
18
18
  }
19
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "13.6.4",
3
+ "version": "13.6.6",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [