newrelic 9.1.0 → 9.2.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,27 +1,43 @@
1
- ### v9.1.0 (2022-09-22)
2
-
3
- * Added [experimental loader](https://nodejs.org/api/esm.html#loaders) to support instrumentation of CommonJS packages in ECMAScript Module(ESM) applications.
4
- * It only supports versions of Node.js >= `16.12.0`.
5
- * It is subject to change due to its experimental stability.
6
-
7
- * Enhanced supportability metrics for ESM support.
8
- * Added new metrics to track usage of ESM loader(`Supportability/Features/ESM/Loader` and `Supportability/Features/ESM/UnsupportedLoader`).
9
- * Updated instrumentation map to include an optional "friendly name" for tracking metrics.
10
-
11
- * Enabled re-throwing ESM import errors of `newrelic.js` so that the user is informed to rename it to `newrelic.cjs`
12
-
13
- * Fixed an issue with mongodb instrumentation where IPv6 address([::1]) was not getting mapped to localhost when setting the host attribute on the segment.
14
-
15
- * Added a test ESM loader to properly mock out agent in versioned tests.
16
-
17
- * Added ESM versioned tests for: `express`, `pg`, `mongodb`, and `@grpc/grpc-js`.
18
-
19
- ### v9.0.3 (2022-09-06)
20
-
21
- * Updated gRPC client instrumentation to respect `grpc.record_errors` when deciding to log errors on gRPC client requests.
22
-
23
- * Fixed transaction name finalization to properly copy the appropriate transaction name to root segment.
24
-
1
+ ### v9.2.0 (2022-10-06)
2
+
3
+ * Added ability to instrument ES Modules with the New Relic ESM Loader.
4
+ * [Example ESM application](https://github.com/newrelic/newrelic-node-examples/tree/main/esm-app)
5
+
6
+ * Added support for custom ESM instrumentation.
7
+ * There is structure to registering custom ESM instrumentation. Set the relative path to the instrumentation entry point via `api.esm.custom_instrumentation_entrypoint`
8
+ * [Sample custom ESM instrumentation entrypoint](https://github.com/newrelic/newrelic-node-examples/blob/main/esm-app/custom-instrumentation/index.js)
9
+ * All the `newrelic.instrument*` methods will still work except `newrelic.instrumentLoadedModule`. This is because it is geared toward CommonJS modules.
10
+
11
+ * Added test for asserting ESM loader functionality on ESM-only package
12
+
13
+ * Added supportability metric of `Supportability/Nodejs/Collector/MaxPayloadSizeLimit/<endpoint>` when `max_payload_size_in_bytes` configuration value is exceeded.
14
+
15
+ * Removed `application_logging.forwarding.enabled` stanza from sample config as the feature is now enabled by default.
16
+
17
+ ### v9.1.0 (2022-09-22)
18
+
19
+ * Added [experimental loader](https://nodejs.org/api/esm.html#loaders) to support instrumentation of CommonJS packages in ECMAScript Module(ESM) applications.
20
+ * It only supports versions of Node.js >= `16.12.0`.
21
+ * It is subject to change due to its experimental stability.
22
+
23
+ * Enhanced supportability metrics for ESM support.
24
+ * Added new metrics to track usage of ESM loader(`Supportability/Features/ESM/Loader` and `Supportability/Features/ESM/UnsupportedLoader`).
25
+ * Updated instrumentation map to include an optional "friendly name" for tracking metrics.
26
+
27
+ * Enabled re-throwing ESM import errors of `newrelic.js` so that the user is informed to rename it to `newrelic.cjs`
28
+
29
+ * Fixed an issue with mongodb instrumentation where IPv6 address([::1]) was not getting mapped to localhost when setting the host attribute on the segment.
30
+
31
+ * Added a test ESM loader to properly mock out agent in versioned tests.
32
+
33
+ * Added ESM versioned tests for: `express`, `pg`, `mongodb`, and `@grpc/grpc-js`.
34
+
35
+ ### v9.0.3 (2022-09-06)
36
+
37
+ * Updated gRPC client instrumentation to respect `grpc.record_errors` when deciding to log errors on gRPC client requests.
38
+
39
+ * Fixed transaction name finalization to properly copy the appropriate transaction name to root segment.
40
+
25
41
  ### v9.0.2 (2022-08-23)
26
42
 
27
43
  * Added unit test suite for `lib/logger.js`.
@@ -1334,7 +1334,7 @@ SOFTWARE.
1334
1334
 
1335
1335
  ### @newrelic/eslint-config
1336
1336
 
1337
- This product includes source derived from [@newrelic/eslint-config](https://github.com/newrelic/eslint-config-newrelic) ([v0.1.0](https://github.com/newrelic/eslint-config-newrelic/tree/v0.1.0)), distributed under the [Apache-2.0 License](https://github.com/newrelic/eslint-config-newrelic/blob/v0.1.0/LICENSE):
1337
+ This product includes source derived from [@newrelic/eslint-config](https://github.com/newrelic/eslint-config-newrelic) ([v0.2.0](https://github.com/newrelic/eslint-config-newrelic/tree/v0.2.0)), distributed under the [Apache-2.0 License](https://github.com/newrelic/eslint-config-newrelic/blob/v0.2.0/LICENSE):
1338
1338
 
1339
1339
  ```
1340
1340
  Apache License
@@ -2224,7 +2224,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
2224
2224
 
2225
2225
  ### eslint-plugin-jsdoc
2226
2226
 
2227
- This product includes source derived from [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) ([v36.1.1](https://github.com/gajus/eslint-plugin-jsdoc/tree/v36.1.1)), distributed under the [BSD-3-Clause License](https://github.com/gajus/eslint-plugin-jsdoc/blob/v36.1.1/LICENSE):
2227
+ This product includes source derived from [eslint-plugin-jsdoc](https://github.com/gajus/eslint-plugin-jsdoc) ([v39.3.6](https://github.com/gajus/eslint-plugin-jsdoc/tree/v39.3.6)), distributed under the [BSD-3-Clause License](https://github.com/gajus/eslint-plugin-jsdoc/blob/v39.3.6/LICENSE):
2228
2228
 
2229
2229
  ```
2230
2230
  Copyright (c) 2018, Gajus Kuizinas (http://gajus.com/)
@@ -2318,10 +2318,10 @@ OTHER DEALINGS IN THE SOFTWARE.
2318
2318
 
2319
2319
  ### eslint
2320
2320
 
2321
- This product includes source derived from [eslint](https://github.com/eslint/eslint) ([v7.32.0](https://github.com/eslint/eslint/tree/v7.32.0)), distributed under the [MIT License](https://github.com/eslint/eslint/blob/v7.32.0/LICENSE):
2321
+ This product includes source derived from [eslint](https://github.com/eslint/eslint) ([v8.24.0](https://github.com/eslint/eslint/tree/v8.24.0)), distributed under the [MIT License](https://github.com/eslint/eslint/blob/v8.24.0/LICENSE):
2322
2322
 
2323
2323
  ```
2324
- Copyright JS Foundation and other contributors, https://js.foundation
2324
+ Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
2325
2325
 
2326
2326
  Permission is hereby granted, free of charge, to any person obtaining a copy
2327
2327
  of this software and associated documentation files (the "Software"), to deal
package/api.js CHANGED
@@ -1298,7 +1298,9 @@ API.prototype.instrumentMessages = function instrumentMessages(moduleName, onReq
1298
1298
  }
1299
1299
 
1300
1300
  /**
1301
- * Applies an instrumentation to an already loaded module.
1301
+ * Applies an instrumentation to an already loaded CommonJs module.
1302
+ *
1303
+ * Note: This function will not work for ESM packages.
1302
1304
  *
1303
1305
  * // oh no, express was loaded before newrelic
1304
1306
  * const express = require('express')
package/esm-loader.mjs CHANGED
@@ -8,14 +8,30 @@ import shimmer from './lib/shimmer.js'
8
8
  import loggingModule from './lib/logger.js'
9
9
  import NAMES from './lib/metrics/names.js'
10
10
  import semver from 'semver'
11
+ import path from 'node:path'
11
12
 
12
13
  const isSupportedVersion = () => semver.gte(process.version, 'v16.12.0')
14
+ // This check will prevent resolve hooks executing from within this file
15
+ // If I do `import('foo')` in here it'll hit the resolve hook multiple times
16
+ const isFromEsmLoader = (context) =>
17
+ context && context.parentURL && context.parentURL.includes('newrelic/esm-loader.mjs')
18
+
13
19
  const logger = loggingModule.child({ component: 'esm-loader' })
20
+ const esmShimPath = new URL('./lib/esm-shim.mjs', import.meta.url)
21
+ const customEntryPoint = newrelic?.agent?.config?.api.esm.custom_instrumentation_entrypoint
14
22
 
15
- if (newrelic.agent) {
16
- addESMSupportabilityMetrics(newrelic.agent)
23
+ // Hook point within agent for customers to register their custom instrumentation.
24
+ if (customEntryPoint) {
25
+ const resolvedEntryPoint = path.resolve(customEntryPoint)
26
+ logger.debug('Registering custom ESM instrumentation at %s', resolvedEntryPoint)
27
+ await import(resolvedEntryPoint)
17
28
  }
18
29
 
30
+ addESMSupportabilityMetrics(newrelic.agent)
31
+
32
+ // exporting for testing purposes
33
+ export const registeredSpecifiers = new Map()
34
+
19
35
  /**
20
36
  * Hook chain responsible for resolving a file URL for a given module specifier
21
37
  *
@@ -24,17 +40,14 @@ if (newrelic.agent) {
24
40
  *
25
41
  * Docs: https://nodejs.org/api/esm.html#resolvespecifier-context-nextresolve
26
42
  *
27
- * @param {string} specifier
28
- * String identifier in an import statement or import() expression
29
- * @param {object} context
30
- * Metadata about the specifier, including url of the parent module and any import assertions
43
+ * @param {string} specifier string identifier in an import statement or import() expression
44
+ * @param {object} context metadata about the specifier, including url of the parent module and any import assertions
31
45
  * Optional argument that only needs to be passed when changed
32
- * @param {Function} nextResolve
33
- * The Node.js default resolve hook
46
+ * @param {Function} nextResolve The subsequent resolve hook in the chain, or the Node.js default resolve hook after the last user-supplied resolve hook
34
47
  * @returns {Promise} Promise object representing the resolution of a given specifier
35
48
  */
36
49
  export async function resolve(specifier, context, nextResolve) {
37
- if (!newrelic.agent || !isSupportedVersion()) {
50
+ if (!newrelic.agent || !isSupportedVersion() || isFromEsmLoader(context)) {
38
51
  return nextResolve(specifier, context, nextResolve)
39
52
  }
40
53
 
@@ -49,14 +62,15 @@ export async function resolve(specifier, context, nextResolve) {
49
62
  const instrumentationDefinition = shimmer.registeredInstrumentations[instrumentationName]
50
63
 
51
64
  if (instrumentationDefinition) {
52
- logger.debug(`Instrumentation exists for ${specifier}`)
65
+ const { url, format } = resolvedModule
66
+ logger.debug(`Instrumentation exists for ${specifier} ${format} package.`)
53
67
 
54
- if (resolvedModule.format === 'commonjs') {
68
+ if (format === 'commonjs') {
55
69
  // ES Modules translate import statements into fully qualified filepaths, so we create a copy of our instrumentation under this filepath
56
70
  const instrumentationDefinitionCopy = Object.assign({}, instrumentationDefinition)
57
71
 
58
72
  // Stripping the prefix is necessary because the code downstream gets this url without it
59
- instrumentationDefinitionCopy.moduleName = resolvedModule.url.replace('file://', '')
73
+ instrumentationDefinitionCopy.moduleName = url.replace('file://', '')
60
74
 
61
75
  // Added to keep our Supportability metrics from exploding/including customer info via full filepath
62
76
  instrumentationDefinitionCopy.specifier = specifier
@@ -66,14 +80,72 @@ export async function resolve(specifier, context, nextResolve) {
66
80
  logger.debug(
67
81
  `Registered CommonJS instrumentation for ${specifier} under ${instrumentationDefinitionCopy.moduleName}`
68
82
  )
83
+ } else if (format === 'module') {
84
+ registeredSpecifiers.set(url, specifier)
85
+ const modifiedUrl = new URL(url)
86
+ // add a query param to the resolved url so the load hook below knows
87
+ // to rewrite and wrap the source code
88
+ modifiedUrl.searchParams.set('hasNrInstrumentation', 'true')
89
+ resolvedModule.url = modifiedUrl.href
69
90
  } else {
70
- logger.debug(`${specifier} is not a CommonJS module, skipping for now`)
91
+ logger.debug(`${specifier} is not a CommonJS nor ESM package, skipping for now.`)
71
92
  }
72
93
  }
73
94
 
74
95
  return resolvedModule
75
96
  }
76
97
 
98
+ /**
99
+ * Hook chain responsible for determining how a URL should be interpreted, retrieved, and parsed.
100
+ *
101
+ * Our loader has to be the last user-supplied loader if chaining is happening,
102
+ * as we rely on `nextLoad` being the default Node.js resolve hook to load the ESM.
103
+ *
104
+ * Docs: https://nodejs.org/dist/latest-v18.x/docs/api/esm.html#loadurl-context-nextload
105
+ *
106
+ * @param {string} url the URL returned by the resolve chain
107
+ * @param {object} context metadata about the url, including conditions, format and import assertions
108
+ * @param {Function} nextLoad the subsequent load hook in the chain, or the Node.js default load hook after the last user-supplied load hook
109
+ * @returns {Promise} Promise object representing the load of a given url
110
+ */
111
+ export async function load(url, context, nextLoad) {
112
+ if (!newrelic.agent || !isSupportedVersion()) {
113
+ return nextLoad(url, context, nextLoad)
114
+ }
115
+
116
+ let parsedUrl
117
+
118
+ try {
119
+ parsedUrl = new URL(url)
120
+ } catch (err) {
121
+ logger.error('Unable to parse url: %s, msg: %s', url, err.message)
122
+ return nextLoad(url, context, nextLoad)
123
+ }
124
+
125
+ const hasNrInstrumentation = parsedUrl.searchParams.get('hasNrInstrumentation')
126
+
127
+ if (!hasNrInstrumentation) {
128
+ return nextLoad(url, context, nextLoad)
129
+ }
130
+
131
+ /**
132
+ * undo the work we did in the resolve hook above
133
+ * so we can properly rewrite source and not get in an infinite loop
134
+ */
135
+ parsedUrl.searchParams.delete('hasNrInstrumentation')
136
+
137
+ const originalUrl = parsedUrl.href
138
+ const specifier = registeredSpecifiers.get(originalUrl)
139
+ const rewrittenSource = await wrapEsmSource(originalUrl, specifier)
140
+ logger.debug(`Registered module instrumentation for ${specifier}.`)
141
+
142
+ return {
143
+ format: 'module',
144
+ source: rewrittenSource,
145
+ shortCircuit: true
146
+ }
147
+ }
148
+
77
149
  /**
78
150
  * Helper function for determining which of our Supportability metrics to use for the current loader invocation
79
151
  *
@@ -81,7 +153,11 @@ export async function resolve(specifier, context, nextResolve) {
81
153
  * instantiation of the New Relic agent
82
154
  * @returns {void}
83
155
  */
84
- export function addESMSupportabilityMetrics(agent) {
156
+ function addESMSupportabilityMetrics(agent) {
157
+ if (!agent) {
158
+ return
159
+ }
160
+
85
161
  if (isSupportedVersion()) {
86
162
  agent.metrics.getOrCreateMetric(NAMES.FEATURES.ESM.LOADER).incrementCallCount()
87
163
  } else {
@@ -91,4 +167,38 @@ export function addESMSupportabilityMetrics(agent) {
91
167
  )
92
168
  agent.metrics.getOrCreateMetric(NAMES.FEATURES.ESM.UNSUPPORTED_LOADER).incrementCallCount()
93
169
  }
170
+
171
+ if (customEntryPoint) {
172
+ agent.metrics.getOrCreateMetric(NAMES.FEATURES.ESM.CUSTOM_INSTRUMENTATION).incrementCallCount()
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Rewrites the source code of a ES module we want to instrument.
178
+ * This is done by injecting the ESM shim which proxies every property on the exported
179
+ * module and registers the module with shimmer so instrumentation can be registered properly.
180
+ *
181
+ * @param {string} url the URL returned by the resolve chain
182
+ * @param {string} specifier string identifier in an import statement or import() expression
183
+ * @returns {string} source code rewritten to wrap with our esm-shim
184
+ */
185
+ async function wrapEsmSource(url, specifier) {
186
+ const pkg = await import(url)
187
+ const props = Object.keys(pkg)
188
+ const trimmedUrl = url.replace('file://', '')
189
+
190
+ return `
191
+ import wrapModule from '${esmShimPath.href}'
192
+ import * as _originalModule from '${url}'
193
+ const _wrappedModule = wrapModule(_originalModule, '${specifier}', '${trimmedUrl}')
194
+ ${props
195
+ .map((propName) => {
196
+ const propertyExportSource = `
197
+ let _${propName} = _wrappedModule.${propName}
198
+ export { _${propName} as ${propName} }`
199
+
200
+ return propertyExportSource
201
+ })
202
+ .join('\n')}
203
+ `
94
204
  }
@@ -235,6 +235,9 @@ RemoteMethod.prototype._safeRequest = function _safeRequest(options) {
235
235
  let level = 'trace'
236
236
 
237
237
  if (!isValidLength(options.body, maxPayloadSize)) {
238
+ this._agent.metrics
239
+ .getOrCreateMetric(`${DATA_USAGE.PREFIX}/MaxPayloadSizeLimit/${this.name}`)
240
+ .incrementCallCount()
238
241
  logger.warn(
239
242
  'The payload size %d being sent to method %s exceeded the maximum size of %d',
240
243
  Buffer.byteLength(options.body, 'utf8'),
@@ -26,7 +26,7 @@ exports.config = () => ({
26
26
  license_key: '',
27
27
  /**
28
28
  *
29
- * Enables/Disables security politices. Paste your security policies
29
+ * Enables/Disables security policies. Paste your security policies
30
30
  * token from the New Relic Admin below.
31
31
  *
32
32
  * @env NEW_RELIC_SECURITY_POLICIES_TOKEN
@@ -623,7 +623,15 @@ exports.config = () => ({
623
623
  *
624
624
  * @env NEW_RELIC_API_NOTICE_ERROR
625
625
  */
626
- notice_error_enabled: true
626
+ notice_error_enabled: true,
627
+ /**
628
+ * Sets the path to custom ESM instrumentation
629
+ *
630
+ * @env NEW_RELIC_API_ESM_CUSTOM_INSTRUMENTATION_ENTRYPOINT
631
+ */
632
+ esm: {
633
+ custom_instrumentation_entrypoint: null
634
+ }
627
635
  },
628
636
  /**
629
637
  * Transaction Events
package/lib/config/env.js CHANGED
@@ -114,7 +114,10 @@ const ENV_MAPPING = {
114
114
  api: {
115
115
  custom_attributes_enabled: 'NEW_RELIC_API_CUSTOM_ATTRIBUTES',
116
116
  custom_events_enabled: 'NEW_RELIC_API_CUSTOM_EVENTS',
117
- notice_error_enabled: 'NEW_RELIC_API_NOTICE_ERROR'
117
+ notice_error_enabled: 'NEW_RELIC_API_NOTICE_ERROR',
118
+ esm: {
119
+ custom_instrumentation_entrypoint: 'NEW_RELIC_API_ESM_CUSTOM_INSTRUMENTATION_ENTRYPOINT'
120
+ }
118
121
  },
119
122
  datastore_tracer: {
120
123
  instance_reporting: {
@@ -0,0 +1,36 @@
1
+ /*
2
+ * Copyright 2022 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import shimmer from './shimmer.js'
7
+ import newrelic from '../index.js'
8
+ const { agent } = newrelic
9
+
10
+ export default function wrapModule(module, moduleName, resolvedPath) {
11
+ const proxiedProps = Object.assign(Object.create(null), module)
12
+ const proxiedModule = new Proxy(module, {
13
+ get: (target, key) => {
14
+ return proxiedProps[key] || target[key]
15
+ },
16
+ set: (target, key, val) => {
17
+ return (proxiedProps[key] = val)
18
+ },
19
+ /**
20
+ * This is normally not allowed.
21
+ * We have some things that define property, unsure if at the module level.
22
+ * Ideally, we can get away from doing this sort of thing at all in the future
23
+ * in a post AsyncLocalStorage world, etc.
24
+ * If we never do at the module level, perhaps we can not define this or defer upstream
25
+ *
26
+ * @param target
27
+ * @param key
28
+ * @param descriptor
29
+ */
30
+ defineProperty: (target, key, descriptor) => {
31
+ return Object.defineProperty(proxiedProps, key, descriptor)
32
+ }
33
+ })
34
+
35
+ return shimmer.instrumentPostLoad(agent, proxiedModule, moduleName, resolvedPath, true)
36
+ }
@@ -259,7 +259,8 @@ const INFINITE_TRACING = {
259
259
  const FEATURES = {
260
260
  ESM: {
261
261
  LOADER: `${SUPPORTABILITY.FEATURES}/ESM/Loader`,
262
- UNSUPPORTED_LOADER: `${SUPPORTABILITY.FEATURES}/ESM/UnsupportedLoader`
262
+ UNSUPPORTED_LOADER: `${SUPPORTABILITY.FEATURES}/ESM/UnsupportedLoader`,
263
+ CUSTOM_INSTRUMENTATION: `${SUPPORTABILITY.FEATURES}/ESM/CustomInstrumentation`
263
264
  },
264
265
  CERTIFICATES: SUPPORTABILITY.FEATURES + '/Certificates',
265
266
  INSTRUMENTATION: {
package/lib/shimmer.js CHANGED
@@ -467,12 +467,12 @@ const shimmer = (module.exports = {
467
467
  return instrumentation
468
468
  },
469
469
 
470
- instrumentPostLoad(agent, module, moduleName, resolvedName) {
470
+ instrumentPostLoad(agent, module, moduleName, resolvedName, returnModule = false) {
471
471
  const result = _postLoad(agent, module, moduleName, resolvedName)
472
472
  // This is to not break the public API
473
473
  // previously it would just call instrumentation
474
474
  // and not check the result
475
- return !!result.__NR_instrumented
475
+ return returnModule ? result : !!result.__NR_instrumented
476
476
  }
477
477
  })
478
478
 
package/newrelic.js CHANGED
@@ -28,14 +28,6 @@ exports.config = {
28
28
  * attributes include/exclude lists.
29
29
  */
30
30
  allow_all_headers: true,
31
- application_logging: {
32
- forwarding: {
33
- /**
34
- * Toggles whether the agent gathers log records for sending to New Relic.
35
- */
36
- enabled: true
37
- }
38
- },
39
31
  attributes: {
40
32
  /**
41
33
  * Prefix of attributes to exclude from all destinations. Allows * as wildcard
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "9.1.0",
3
+ "version": "9.2.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [
@@ -191,7 +191,7 @@
191
191
  "@newrelic/native-metrics": "^9.0.0"
192
192
  },
193
193
  "devDependencies": {
194
- "@newrelic/eslint-config": "^0.1.0",
194
+ "@newrelic/eslint-config": "^0.2.0",
195
195
  "@newrelic/newrelic-oss-cli": "^0.1.2",
196
196
  "@newrelic/proxy": "^2.0.0",
197
197
  "@newrelic/test-utilities": "^7.1.1",
@@ -201,11 +201,11 @@
201
201
  "async": "^3.2.4",
202
202
  "chai": "^4.1.2",
203
203
  "commander": "^7.0.0",
204
- "eslint": "^7.32.0",
204
+ "eslint": "^8.24.0",
205
205
  "eslint-config-prettier": "^8.3.0",
206
206
  "eslint-plugin-disable": "^2.0.1",
207
207
  "eslint-plugin-header": "^3.1.1",
208
- "eslint-plugin-jsdoc": "^36.1.0",
208
+ "eslint-plugin-jsdoc": "^39.3.6",
209
209
  "eslint-plugin-node": "^11.1.0",
210
210
  "eslint-plugin-prettier": "^3.4.0",
211
211
  "express": "*",