dd-trace 5.73.0 → 5.74.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.
@@ -50,6 +50,7 @@ dev,@stylistic/eslint-plugin,MIT,Copyright OpenJS Foundation and other contribut
50
50
  dev,axios,MIT,Copyright 2014-present Matt Zabriskie
51
51
  dev,benchmark,MIT,Copyright 2010-2016 Mathias Bynens Robert Kieffer John-David Dalton
52
52
  dev,body-parser,MIT,Copyright 2014 Jonathan Ong 2014-2015 Douglas Christopher Wilson
53
+ dev,bun,MIT,Copyright contributors
53
54
  dev,chai,MIT,Copyright 2017 Chai.js Assertion Library
54
55
  dev,eslint,MIT,Copyright JS Foundation and other contributors https://js.foundation
55
56
  dev,eslint-plugin-cypress,MIT,Copyright (c) 2019 Cypress.io
package/index.d.ts CHANGED
@@ -134,6 +134,11 @@ interface Tracer extends opentracing.Tracer {
134
134
 
135
135
  dogstatsd: tracer.DogStatsD;
136
136
 
137
+ /**
138
+ * Data Streams manual checkpointer API.
139
+ */
140
+ dataStreamsCheckpointer: tracer.DataStreamsCheckpointer;
141
+
137
142
  /**
138
143
  * LLM Observability SDK
139
144
  */
@@ -1026,6 +1031,29 @@ declare namespace tracer {
1026
1031
  flush(): void
1027
1032
  }
1028
1033
 
1034
+ /**
1035
+ * Manual Data Streams Monitoring checkpointer API.
1036
+ */
1037
+ export interface DataStreamsCheckpointer {
1038
+ /**
1039
+ * Sets a produce checkpoint and injects the DSM context into the provided carrier.
1040
+ * @param type The streaming technology (e.g., kafka, kinesis, sns).
1041
+ * @param target The target of data (topic, exchange, stream name).
1042
+ * @param carrier The carrier object to inject DSM context into.
1043
+ */
1044
+ setProduceCheckpoint (type: string, target: string, carrier: any): void;
1045
+
1046
+ /**
1047
+ * Sets a consume checkpoint and extracts DSM context from the provided carrier.
1048
+ * @param type The streaming technology (e.g., kafka, kinesis, sns).
1049
+ * @param source The source of data (topic, exchange, stream name).
1050
+ * @param carrier The carrier object to extract DSM context from.
1051
+ * @param manualCheckpoint Whether this checkpoint was manually set. Defaults to true.
1052
+ * @returns The DSM context associated with the current pathway.
1053
+ */
1054
+ setConsumeCheckpoint (type: string, source: string, carrier: any, manualCheckpoint?: boolean): any;
1055
+ }
1056
+
1029
1057
  export interface EventTrackingV2 {
1030
1058
  /**
1031
1059
  * Links a successful login event to the current trace. Will link the passed user to the current trace with Appsec.setUser() internally.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.73.0",
3
+ "version": "5.74.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -32,6 +32,8 @@
32
32
  "test:trace:core:ci": "npm run test:trace:core -- --coverage --nyc-arg=--include=\"packages/dd-trace/src/**/*.js\"",
33
33
  "test:trace:guardrails": "mocha -r \"packages/dd-trace/test/setup/mocha.js\" \"packages/dd-trace/test/guardrails/**/*.spec.js\"",
34
34
  "test:trace:guardrails:ci": "nyc --no-clean --include \"packages/dd-trace/src/guardrails/**/*.js\" -- npm run test:trace:guardrails",
35
+ "test:esbuild": "mocha -r \"packages/dd-trace/test/setup/mocha.js\" \"packages/datadog-esbuild/test/**/*.spec.js\"",
36
+ "test:esbuild:ci": "nyc --no-clean --include \"packages/datadog-esbuild/test/**/*.js\" -- npm run test:esbuild",
35
37
  "test:instrumentations": "mocha -r 'packages/dd-trace/test/setup/mocha.js' \"packages/datadog-instrumentations/test/@($(echo $PLUGINS)).spec.js\"",
36
38
  "test:instrumentations:ci": "yarn services && nyc --no-clean --include \"packages/datadog-instrumentations/src/@($(echo $PLUGINS)).js\" --include \"packages/datadog-instrumentations/src/@($(echo $PLUGINS))/**/*.js\" -- npm run test:instrumentations",
37
39
  "test:instrumentations:misc": "mocha -r 'packages/dd-trace/test/setup/mocha.js' 'packages/datadog-instrumentations/test/*/**/*.spec.js'",
@@ -158,7 +160,7 @@
158
160
  },
159
161
  "peerDependencies": {
160
162
  "@openfeature/core": "^1.9.0",
161
- "@openfeature/server-sdk": "~1.19.0"
163
+ "@openfeature/server-sdk": "~1.20.0"
162
164
  },
163
165
  "peerDependenciesMeta": {
164
166
  "@openfeature/core": {
@@ -174,7 +176,7 @@
174
176
  "@eslint/js": "^9.29.0",
175
177
  "@msgpack/msgpack": "^3.1.2",
176
178
  "@openfeature/core": "^1.8.1",
177
- "@openfeature/server-sdk": "~1.19.0",
179
+ "@openfeature/server-sdk": "~1.20.0",
178
180
  "@stylistic/eslint-plugin": "^5.0.0",
179
181
  "@types/chai": "^4.3.16",
180
182
  "@types/mocha": "^10.0.10",
@@ -184,6 +186,7 @@
184
186
  "axios": "^1.12.2",
185
187
  "benchmark": "^2.1.4",
186
188
  "body-parser": "^2.2.0",
189
+ "bun": "1.3.1",
187
190
  "chai": "^4.5.0",
188
191
  "eslint": "^9.29.0",
189
192
  "eslint-plugin-cypress": "^5.1.0",
@@ -5,7 +5,11 @@ module.exports = function set (object, path, value) {
5
5
  while (true) {
6
6
  const nextIndex = path.indexOf('.', index + 1)
7
7
  if (nextIndex === -1) {
8
- object[path.slice(index + 1)] = value
8
+ if (index === -1) {
9
+ object[path] = value
10
+ } else {
11
+ object[path.slice(index + 1)] = value
12
+ }
9
13
  return
10
14
  }
11
15
  object = object[path.slice(index + 1, nextIndex)] ??= {}
@@ -8,6 +8,12 @@ const extractPackageAndModulePath = require(
8
8
  '../datadog-instrumentations/src/helpers/extract-package-and-module-path.js'
9
9
  )
10
10
 
11
+ const { pathToFileURL, fileURLToPath } = require('url')
12
+ const { processModule, isESMFile } = require('./src/utils.js')
13
+
14
+ const ESM_INTERCEPTED_SUFFIX = '._dd_esbuild_intercepted'
15
+ const INTERNAL_ESM_INTERCEPTED_PREFIX = '/_dd_esm_internal_/'
16
+
11
17
  let rewriter
12
18
 
13
19
  for (const hook of Object.values(hooks)) {
@@ -30,7 +36,6 @@ for (const instrumentation of Object.values(instrumentations)) {
30
36
  }
31
37
  }
32
38
 
33
- const INSTRUMENTED = Object.keys(instrumentations)
34
39
  const RAW_BUILTINS = require('module').builtinModules
35
40
  const CHANNEL = 'dd-trace:bundler:load'
36
41
  const path = require('path')
@@ -47,14 +52,6 @@ for (const builtin of RAW_BUILTINS) {
47
52
  const DEBUG = !!process.env.DD_TRACE_DEBUG
48
53
  const DD_IAST_ENABLED = process.env.DD_IAST_ENABLED?.toLowerCase() === 'true' || process.env.DD_IAST_ENABLED === '1'
49
54
 
50
- // We don't want to handle any built-in packages
51
- // Those packages will still be handled via RITM
52
- // Attempting to instrument them would fail as they have no package.json file
53
- for (const pkg of INSTRUMENTED) {
54
- if (builtins.has(pkg) || pkg.startsWith('node:')) continue
55
- modulesOfInterest.add(pkg)
56
- }
57
-
58
55
  module.exports.name = 'datadog-esbuild'
59
56
 
60
57
  function isESMBuild (build) {
@@ -123,7 +120,8 @@ ${build.initialOptions.banner.js}`
123
120
  build.initialOptions.external.push('@openfeature/core')
124
121
  }
125
122
 
126
- if (isESMBuild(build)) {
123
+ const esmBuild = isESMBuild(build)
124
+ if (esmBuild) {
127
125
  if (!build.initialOptions.banner.js.includes('import { createRequire as $dd_createRequire } from \'module\'')) {
128
126
  build.initialOptions.banner.js = `import { createRequire as $dd_createRequire } from 'module';
129
127
  import { fileURLToPath as $dd_fileURLToPath } from 'url';
@@ -157,6 +155,9 @@ ${build.initialOptions.banner.js}`
157
155
  console.warn('Warning: No git metadata available - skipping injection')
158
156
  }
159
157
 
158
+ // first time is intercepted, proxy should be created, next time the original should be loaded
159
+ const interceptedESMModules = new Set()
160
+
160
161
  build.onResolve({ filter: /.*/ }, args => {
161
162
  if (externalModules.has(args.path)) {
162
163
  // Internal Node.js packages will still be instrumented via require()
@@ -175,7 +176,7 @@ ${build.initialOptions.banner.js}`
175
176
 
176
177
  let fullPathToModule
177
178
  try {
178
- fullPathToModule = dotFriendlyResolve(args.path, args.resolveDir)
179
+ fullPathToModule = dotFriendlyResolve(args.path, args.resolveDir, args.kind === 'import-statement')
179
180
  } catch (err) {
180
181
  if (DEBUG) {
181
182
  console.warn(`Warning: Unable to find "${args.path}".` +
@@ -205,8 +206,25 @@ ${build.initialOptions.banner.js}`
205
206
  if (args.namespace === 'file' && (
206
207
  modulesOfInterest.has(args.path) || modulesOfInterest.has(`${extracted.pkg}/${extracted.path}`))
207
208
  ) {
209
+ // Internal module like http/fs is imported and the build output is ESM
210
+ if (internal && args.kind === 'import-statement' && esmBuild && !interceptedESMModules.has(fullPathToModule)) {
211
+ fullPathToModule = `${INTERNAL_ESM_INTERCEPTED_PREFIX}${fullPathToModule}${ESM_INTERCEPTED_SUFFIX}`
212
+
213
+ return {
214
+ path: fullPathToModule,
215
+ pluginData: {
216
+ pkg: extracted?.pkg,
217
+ path: extracted?.path,
218
+ full: fullPathToModule,
219
+ raw: args.path,
220
+ pkgOfInterest: true,
221
+ kind: args.kind,
222
+ internal,
223
+ isESM: true
224
+ }
225
+ }
226
+ }
208
227
  // The file namespace is used when requiring files from disk in userland
209
-
210
228
  let pathToPackageJson
211
229
  try {
212
230
  // we can't use require.resolve('pkg/package.json') as ESM modules don't make the file available
@@ -228,6 +246,11 @@ ${build.initialOptions.banner.js}`
228
246
 
229
247
  const packageJson = JSON.parse(fs.readFileSync(pathToPackageJson).toString())
230
248
 
249
+ const isESM = isESMFile(fullPathToModule, pathToPackageJson, packageJson)
250
+ if (isESM && !interceptedESMModules.has(fullPathToModule)) {
251
+ fullPathToModule += ESM_INTERCEPTED_SUFFIX
252
+ }
253
+
231
254
  if (DEBUG) console.log(`RESOLVE: ${args.path}@${packageJson.version}`)
232
255
 
233
256
  // https://esbuild.github.io/plugins/#on-resolve-arguments
@@ -240,13 +263,15 @@ ${build.initialOptions.banner.js}`
240
263
  full: fullPathToModule,
241
264
  raw: args.path,
242
265
  pkgOfInterest: true,
243
- internal
266
+ kind: args.kind,
267
+ internal,
268
+ isESM
244
269
  }
245
270
  }
246
271
  }
247
272
  })
248
273
 
249
- build.onLoad({ filter: /.*/ }, args => {
274
+ build.onLoad({ filter: /.*/ }, async args => {
250
275
  if (args.pluginData?.pkgOfInterest) {
251
276
  const data = args.pluginData
252
277
 
@@ -257,26 +282,63 @@ ${build.initialOptions.banner.js}`
257
282
  : data.pkg
258
283
 
259
284
  // Read the content of the module file of interest
260
- const fileCode = fs.readFileSync(args.path, 'utf8')
285
+ let contents
286
+
287
+ if (data.isESM) {
288
+ if (args.path.endsWith(ESM_INTERCEPTED_SUFFIX)) {
289
+ args.path = args.path.slice(0, -1 * ESM_INTERCEPTED_SUFFIX.length)
290
+
291
+ if (data.internal) {
292
+ args.path = args.path.slice(INTERNAL_ESM_INTERCEPTED_PREFIX.length)
293
+ }
294
+
295
+ interceptedESMModules.add(args.path)
296
+
297
+ const setters = await processModule({
298
+ path: args.path,
299
+ internal: data.internal,
300
+ context: { format: 'module' }
301
+ })
302
+
303
+ const iitmPath = require.resolve('import-in-the-middle/lib/register.js')
304
+ const toRegister = data.internal ? args.path : pathToFileURL(args.path)
305
+ // Mimic a Module object (https://tc39.es/ecma262/#sec-module-namespace-objects).
306
+ contents = `
307
+ import { register } from ${JSON.stringify(iitmPath)};
308
+ import * as namespace from ${JSON.stringify(args.path)};
309
+ const _ = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } });
310
+ const set = {};
311
+ const get = {};
312
+
313
+ ${Array.from(setters.values()).join(';\n')};
314
+
315
+ register(${JSON.stringify(toRegister)}, _, set, get, ${JSON.stringify(data.raw)});
316
+ `
317
+ } else {
318
+ contents = fs.readFileSync(args.path, 'utf8')
319
+ }
320
+ } else {
321
+ const fileCode = fs.readFileSync(args.path, 'utf8')
322
+ contents = `
323
+ (function() {
324
+ ${fileCode}
325
+ })(...arguments);
326
+ {
327
+ const dc = require('dc-polyfill');
328
+ const ch = dc.channel('${CHANNEL}');
329
+ const mod = module.exports
330
+ const payload = {
331
+ module: mod,
332
+ version: '${data.version}',
333
+ package: '${data.pkg}',
334
+ path: '${pkgPath}'
335
+ };
336
+ ch.publish(payload);
337
+ module.exports = payload.module;
338
+ }
339
+ `
340
+ }
261
341
 
262
- const contents = `
263
- (function() {
264
- ${fileCode}
265
- })(...arguments);
266
- {
267
- const dc = require('dc-polyfill');
268
- const ch = dc.channel('${CHANNEL}');
269
- const mod = module.exports
270
- const payload = {
271
- module: mod,
272
- version: '${data.version}',
273
- package: '${data.pkg}',
274
- path: '${pkgPath}'
275
- };
276
- ch.publish(payload);
277
- module.exports = payload.module;
278
- }
279
- `
280
342
  // https://esbuild.github.io/plugins/#on-load-results
281
343
  return {
282
344
  contents,
@@ -284,7 +346,6 @@ ${build.initialOptions.banner.js}`
284
346
  resolveDir: path.dirname(args.path)
285
347
  }
286
348
  }
287
-
288
349
  if (DD_IAST_ENABLED && args.pluginData?.applicationFile) {
289
350
  const ext = path.extname(args.path).toLowerCase()
290
351
  const isJs = /^\.(js|mjs|cjs)$/.test(ext)
@@ -303,12 +364,19 @@ ${build.initialOptions.banner.js}`
303
364
  }
304
365
 
305
366
  // @see https://github.com/nodejs/node/issues/47000
306
- function dotFriendlyResolve (path, directory) {
367
+ function dotFriendlyResolve (path, directory, usesImportStatement) {
307
368
  if (path === '.') {
308
369
  path = './'
309
370
  } else if (path === '..') {
310
371
  path = '../'
311
372
  }
373
+ let conditions
374
+ if (usesImportStatement) {
375
+ conditions = new Set(['import', 'node'])
376
+ }
312
377
 
313
- return require.resolve(path, { paths: [directory] })
378
+ if (path.startsWith('file://')) {
379
+ path = fileURLToPath(path)
380
+ }
381
+ return require.resolve(path, { paths: [directory], conditions })
314
382
  }
@@ -0,0 +1,198 @@
1
+ 'use strict'
2
+
3
+ // The content of this file is copied from the `import-in-the-middle` package with minor modifications (https://www.npmjs.com/package/import-in-the-middle)
4
+ const { pathToFileURL, fileURLToPath } = require('node:url')
5
+ const fs = require('node:fs')
6
+ const path = require('node:path')
7
+ const { NODE_MAJOR, NODE_MINOR } = require('../../../version.js')
8
+
9
+ const getExportsImporting = (url) => import(url).then(Object.keys)
10
+ const getExports = NODE_MAJOR >= 20 || (NODE_MAJOR === 18 && NODE_MINOR >= 19)
11
+ ? require('import-in-the-middle/lib/get-exports.js')
12
+ : getExportsImporting
13
+
14
+ function isStarExportLine (line) {
15
+ return /^\* from /.test(line)
16
+ }
17
+
18
+ function isBareSpecifier (specifier) {
19
+ // Relative and absolute paths are not bare specifiers.
20
+ if (
21
+ specifier.startsWith('.') ||
22
+ specifier.startsWith('/')) {
23
+ return false
24
+ }
25
+
26
+ // Valid URLs are not bare specifiers. (file:, http:, node:, etc.)
27
+
28
+ if (URL.hasOwnProperty('canParse')) {
29
+ // eslint-disable-next-line n/no-unsupported-features/node-builtins
30
+ return !URL.canParse(specifier)
31
+ }
32
+
33
+ try {
34
+ // eslint-disable-next-line no-new
35
+ new URL(specifier)
36
+ return false
37
+ } catch {
38
+ return true
39
+ }
40
+ }
41
+
42
+ function resolve (specifier, context) {
43
+ // This comes from an import, that is why import makes preference
44
+ const conditions = ['import']
45
+
46
+ if (specifier.startsWith('file://')) {
47
+ specifier = fileURLToPath(specifier)
48
+ }
49
+
50
+ const resolved = require.resolve(specifier, { conditions, paths: [fileURLToPath(context.parentURL)] })
51
+
52
+ return {
53
+ url: pathToFileURL(resolved),
54
+ format: isESMFile(resolved) ? 'module' : 'commonjs'
55
+ }
56
+ }
57
+
58
+ function getSource (url, { format }) {
59
+ return {
60
+ source: fs.readFileSync(fileURLToPath(url), 'utf8'),
61
+ format
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Generates the pieces of code for the proxy module before the path
67
+ *
68
+ * @param {Object} moduleData { path, internal, context, excludeDefault }
69
+ * @returns {Promise<Map>}
70
+ */
71
+ async function processModule ({ path, internal, context, excludeDefault }) {
72
+ let exportNames, srcUrl
73
+ if (internal) {
74
+ // we can not read and parse of internal modules
75
+ exportNames = await getExportsImporting(path)
76
+ } else {
77
+ srcUrl = pathToFileURL(path)
78
+ exportNames = await getExports(srcUrl, context, getSource)
79
+ }
80
+
81
+ const starExports = new Set()
82
+ const setters = new Map()
83
+
84
+ const addSetter = (name, setter, isStarExport = false) => {
85
+ if (setters.has(name)) {
86
+ if (isStarExport) {
87
+ // If there's already a matching star export, delete it
88
+ if (starExports.has(name)) {
89
+ setters.delete(name)
90
+ }
91
+ // and return so this is excluded
92
+ return
93
+ }
94
+
95
+ // if we already have this export but it is from a * export, overwrite it
96
+ if (starExports.has(name)) {
97
+ starExports.delete(name)
98
+ setters.set(name, setter)
99
+ }
100
+ } else {
101
+ // Store export * exports so we know they can be overridden by explicit
102
+ // named exports
103
+ if (isStarExport) {
104
+ starExports.add(name)
105
+ }
106
+
107
+ setters.set(name, setter)
108
+ }
109
+ }
110
+
111
+ for (const n of exportNames) {
112
+ if (n === 'default' && excludeDefault) continue
113
+
114
+ if (isStarExportLine(n) === true) {
115
+ // export * from 'wherever'
116
+ const [, modFile] = n.split('* from ')
117
+
118
+ // Relative paths need to be resolved relative to the parent module
119
+ const newSpecifier = isBareSpecifier(modFile) ? modFile : new URL(modFile, srcUrl).href
120
+ // We need to call `parentResolve` to resolve bare specifiers to a full
121
+ // URL. We also need to call `parentResolve` for all sub-modules to get
122
+ // the `format`. We can't rely on the parents `format` to know if this
123
+ // sub-module is ESM or CJS!
124
+
125
+ const result = resolve(newSpecifier, { parentURL: srcUrl })
126
+
127
+ // eslint-disable-next-line no-await-in-loop
128
+ const subSetters = await processModule({
129
+ path: fileURLToPath(result.url),
130
+ context: { ...context, format: result.format },
131
+ excludeDefault: true
132
+ })
133
+
134
+ for (const [name, setter] of subSetters.entries()) {
135
+ addSetter(name, setter, true)
136
+ }
137
+ } else {
138
+ const variableName = `$${n.replaceAll(/[^a-zA-Z0-9_$]/g, '_')}`
139
+ const objectKey = JSON.stringify(n)
140
+ const reExportedName = n === 'default' ? n : objectKey
141
+
142
+ addSetter(n, `
143
+ let ${variableName}
144
+ try {
145
+ ${variableName} = _[${objectKey}] = namespace[${objectKey}]
146
+ } catch (err) {
147
+ if (!(err instanceof ReferenceError)) throw err
148
+ }
149
+ export { ${variableName} as ${reExportedName} }
150
+ set[${objectKey}] = (v) => {
151
+ ${variableName} = v
152
+ return true
153
+ }
154
+ get[${objectKey}] = () => ${variableName}
155
+ `)
156
+ }
157
+ }
158
+
159
+ return setters
160
+ }
161
+
162
+ /**
163
+ * Determines if a file is a ESM module or CommonJS
164
+ *
165
+ * @param {string} fullPathToModule File to analize
166
+ * @param {string} [modulePackageJsonPath] Path of the package.json
167
+ * @param {Object} [packageJson] The content of the module package.json
168
+ * @returns {boolean}
169
+ */
170
+ function isESMFile (fullPathToModule, modulePackageJsonPath, packageJson = {}) {
171
+ if (fullPathToModule.endsWith('.mjs')) return true
172
+ if (fullPathToModule.endsWith('.cjs')) return false
173
+
174
+ const pathParts = fullPathToModule.split(path.sep)
175
+ do {
176
+ pathParts.pop()
177
+
178
+ const packageJsonPath = [...pathParts, 'package.json'].join(path.sep)
179
+ if (packageJsonPath === modulePackageJsonPath) {
180
+ return packageJson.type === 'module'
181
+ }
182
+
183
+ try {
184
+ const packageJsonContent = fs.readFileSync(packageJsonPath).toString()
185
+ const packageJson = JSON.parse(packageJsonContent)
186
+ return packageJson.type === 'module'
187
+ } catch {
188
+ // file does not exit, continue
189
+ }
190
+ } while (pathParts.length > 0)
191
+
192
+ return packageJson.type === 'module'
193
+ }
194
+
195
+ module.exports = {
196
+ processModule,
197
+ isESMFile
198
+ }
@@ -37,5 +37,6 @@ addHook({
37
37
  name: 'express-session',
38
38
  versions: ['>=1.5.0']
39
39
  }, session => {
40
+ if (session.default) return session
40
41
  return shimmer.wrapFunction(session, wrapSession)
41
42
  })
@@ -3,8 +3,18 @@
3
3
  const { createWrapRouterMethod } = require('./router')
4
4
  const shimmer = require('../../datadog-shimmer')
5
5
  const { addHook, channel, tracingChannel } = require('./helpers/instrument')
6
+ const {
7
+ setRouterMountPath,
8
+ markAppMounted,
9
+ normalizeRoutePaths,
10
+ wrapRouteMethodsAndPublish,
11
+ extractMountPaths,
12
+ hasRouterCycle,
13
+ collectRoutesFromRouter
14
+ } = require('./helpers/router-helper')
6
15
 
7
16
  const handleChannel = channel('apm:express:request:handle')
17
+ const routeAddedChannel = channel('apm:express:route:added')
8
18
 
9
19
  function wrapHandle (handle) {
10
20
  return function handleWithTrace (req, res) {
@@ -56,8 +66,80 @@ function wrapResponseRender (render) {
56
66
  }
57
67
  }
58
68
 
69
+ function wrapAppAll (all) {
70
+ return function wrappedAll (path, ...otherArgs) {
71
+ if (!routeAddedChannel.hasSubscribers) return all.call(this, path, ...otherArgs)
72
+
73
+ const paths = normalizeRoutePaths(path)
74
+
75
+ for (const p of paths) {
76
+ routeAddedChannel.publish({ method: '*', path: p })
77
+ }
78
+
79
+ return all.call(this, path, ...otherArgs)
80
+ }
81
+ }
82
+
83
+ // Wrap app.route() to instrument Route object
84
+ function wrapAppRoute (route) {
85
+ return function wrappedRoute (path, ...otherArgs) {
86
+ const routeObj = route.call(this, path, ...otherArgs)
87
+
88
+ if (!routeAddedChannel.hasSubscribers) return routeObj
89
+
90
+ const paths = normalizeRoutePaths(path)
91
+
92
+ if (!paths.length) return routeObj
93
+
94
+ wrapRouteMethodsAndPublish(routeObj, paths, ({ method, path }) => {
95
+ routeAddedChannel.publish({ method, path })
96
+ })
97
+
98
+ return routeObj
99
+ }
100
+ }
101
+
102
+ function wrapAppUse (use) {
103
+ return function wrappedUse (...args) {
104
+ if (!args.length) return use.call(this)
105
+
106
+ // Get mount argument and use it to register each router against the exact paths Express will use.
107
+ const { mountPaths, startIdx } = extractMountPaths(args[0])
108
+ const pathsToRegister = mountPaths.length ? mountPaths : ['/']
109
+
110
+ for (let i = startIdx; i < args.length; i++) {
111
+ const router = args[i]
112
+
113
+ if (!router || typeof router !== 'function') continue
114
+
115
+ markAppMounted(router)
116
+
117
+ // Avoid enumerating routes for routers that contain cycles.
118
+ // Express will refuse those at runtime, but collecting them here could loop forever.
119
+ let skipCollection = false
120
+ if (routeAddedChannel.hasSubscribers) {
121
+ skipCollection = hasRouterCycle(router)
122
+ }
123
+
124
+ for (const mountPath of pathsToRegister) {
125
+ const normalizedMountPath = mountPath || '/'
126
+ setRouterMountPath(router, normalizedMountPath)
127
+
128
+ if (!skipCollection && routeAddedChannel.hasSubscribers) {
129
+ collectRoutesFromRouter(router, normalizedMountPath)
130
+ }
131
+ }
132
+ }
133
+
134
+ return use.apply(this, args)
135
+ }
136
+ }
137
+
59
138
  addHook({ name: 'express', versions: ['>=4'], file: ['lib/express.js'] }, express => {
60
139
  shimmer.wrap(express.application, 'handle', wrapHandle)
140
+ shimmer.wrap(express.application, 'all', wrapAppAll)
141
+ shimmer.wrap(express.application, 'route', wrapAppRoute)
142
+ shimmer.wrap(express.application, 'use', wrapAppUse)
61
143
 
62
144
  shimmer.wrap(express.response, 'json', wrapResponseJson)
63
145
  shimmer.wrap(express.response, 'jsonp', wrapResponseJson)