dd-trace 4.8.0 → 4.9.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/MIGRATING.md CHANGED
@@ -29,20 +29,6 @@ switching to `jest-circus` to anyone still using `jest-jasmine2`.
29
29
 
30
30
  We now support only Next.js 10.2 and up.
31
31
 
32
- ### W3C headers are now prioritized over Datadog headers
33
-
34
- As we move towards open standards, we have decided to prioritize W3C Trace
35
- Context headers over our own vendor-specific headers for context propagation
36
- across services. For most applications this shouldn't change anything and
37
- distributed tracing should continue to work seamlessly.
38
-
39
- In some rare cases it's possible that some of the services involved in a trace
40
- are not instrumented by Datadog at all which can cause spans within the trace to
41
- become disconnected. While the data would still be available in the UI, the
42
- relationship between spans would no longer be visible. This can be addressed by
43
- restoring the previous behaviour using
44
- `DD_TRACE_PROPAGATION_STYLE='datadog,tracecontext'`.
45
-
46
32
  ## 2.0 to 3.0
47
33
 
48
34
  ### Node 12 is no longer supported
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "4.8.0",
3
+ "version": "4.9.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -76,10 +76,10 @@
76
76
  "@opentelemetry/core": "^1.14.0",
77
77
  "crypto-randomuuid": "^1.0.0",
78
78
  "diagnostics_channel": "^1.1.0",
79
- "ignore": "^5.2.0",
80
- "import-in-the-middle": "^1.3.5",
79
+ "ignore": "^5.2.4",
80
+ "import-in-the-middle": "^1.4.1",
81
81
  "int64-buffer": "^0.1.9",
82
- "ipaddr.js": "^2.0.1",
82
+ "ipaddr.js": "^2.1.0",
83
83
  "istanbul-lib-coverage": "3.2.0",
84
84
  "koalas": "^1.0.2",
85
85
  "limiter": "^1.1.4",
@@ -91,24 +91,24 @@
91
91
  "methods": "^1.1.2",
92
92
  "module-details-from-path": "^1.0.3",
93
93
  "msgpack-lite": "^0.1.26",
94
- "node-abort-controller": "^3.0.1",
94
+ "node-abort-controller": "^3.1.1",
95
95
  "opentracing": ">=0.12.1",
96
96
  "path-to-regexp": "^0.1.2",
97
97
  "protobufjs": "^7.2.4",
98
- "retry": "^0.10.1",
99
- "semver": "^7.3.8"
98
+ "retry": "^0.13.1",
99
+ "semver": "^7.5.4"
100
100
  },
101
101
  "devDependencies": {
102
102
  "@types/node": ">=16",
103
103
  "autocannon": "^4.5.2",
104
104
  "axios": "^0.21.2",
105
105
  "benchmark": "^2.1.4",
106
- "body-parser": "^1.18.2",
107
- "chai": "^4.2.0",
108
- "chalk": "^3.0.0",
109
- "checksum": "^0.1.1",
110
- "cli-table3": "^0.5.1",
111
- "dotenv": "8.2.0",
106
+ "body-parser": "^1.20.2",
107
+ "chai": "^4.3.7",
108
+ "chalk": "^5.3.0",
109
+ "checksum": "^1.0.0",
110
+ "cli-table3": "^0.6.3",
111
+ "dotenv": "16.3.1",
112
112
  "esbuild": "0.16.12",
113
113
  "eslint": "^8.23.0",
114
114
  "eslint-config-standard": "^11.0.0-beta.0",
@@ -118,13 +118,13 @@
118
118
  "eslint-plugin-node": "^5.2.1",
119
119
  "eslint-plugin-promise": "^3.6.0",
120
120
  "eslint-plugin-standard": "^3.0.1",
121
- "express": "^4.16.2",
121
+ "express": "^4.18.2",
122
122
  "get-port": "^3.2.0",
123
123
  "glob": "^7.1.6",
124
124
  "graphql": "0.13.2",
125
125
  "jszip": "^3.5.0",
126
126
  "knex": "^2.4.2",
127
- "mkdirp": "^0.5.1",
127
+ "mkdirp": "^3.0.1",
128
128
  "mocha": "8",
129
129
  "multer": "^1.4.5-lts.1",
130
130
  "nock": "^11.3.3",
@@ -132,9 +132,9 @@
132
132
  "pprof-format": "^2.0.7",
133
133
  "proxyquire": "^1.8.0",
134
134
  "rimraf": "^3.0.0",
135
- "sinon": "^11.1.2",
135
+ "sinon": "^15.2.0",
136
136
  "sinon-chai": "^3.7.0",
137
- "tap": "^16.3.4",
138
- "tape": "^4.9.1"
137
+ "tap": "^16.3.7",
138
+ "tape": "^5.6.5"
139
139
  }
140
140
  }
@@ -2,73 +2,105 @@
2
2
 
3
3
  /* eslint-disable no-console */
4
4
 
5
- const NAMESPACE = 'datadog'
6
-
7
- const instrumented = Object.keys(require('../datadog-instrumentations/src/helpers/hooks.js'))
8
- const rawBuiltins = require('module').builtinModules
5
+ const instrumentations = require('../datadog-instrumentations/src/helpers/instrumentations.js')
6
+ const hooks = require('../datadog-instrumentations/src/helpers/hooks.js')
9
7
 
10
8
  warnIfUnsupported()
11
9
 
10
+ for (const hook of Object.values(hooks)) {
11
+ hook()
12
+ }
13
+
14
+ const modulesOfInterest = new Set()
15
+
16
+ for (const instrumentation of Object.values(instrumentations)) {
17
+ for (const entry of instrumentation) {
18
+ if (!entry.file) {
19
+ modulesOfInterest.add(entry.name) // e.g. "redis"
20
+ } else {
21
+ modulesOfInterest.add(`${entry.name}/${entry.file}`) // e.g. "redis/my/file.js"
22
+ }
23
+ }
24
+ }
25
+
26
+ const NAMESPACE = 'datadog'
27
+ const NM = 'node_modules/'
28
+ const INSTRUMENTED = Object.keys(instrumentations)
29
+ const RAW_BUILTINS = require('module').builtinModules
30
+ const CHANNEL = 'dd-trace:bundler:load'
31
+
12
32
  const builtins = new Set()
13
33
 
14
- for (const builtin of rawBuiltins) {
34
+ for (const builtin of RAW_BUILTINS) {
15
35
  builtins.add(builtin)
16
36
  builtins.add(`node:${builtin}`)
17
37
  }
18
38
 
19
- const packagesOfInterest = new Set()
20
-
21
39
  const DEBUG = !!process.env.DD_TRACE_DEBUG
22
40
 
23
- // We don't want to handle any built-in packages via DCITM
41
+ // We don't want to handle any built-in packages
24
42
  // Those packages will still be handled via RITM
25
43
  // Attempting to instrument them would fail as they have no package.json file
26
- for (const pkg of instrumented) {
44
+ for (const pkg of INSTRUMENTED) {
27
45
  if (builtins.has(pkg)) continue
28
46
  if (pkg.startsWith('node:')) continue
29
- packagesOfInterest.add(pkg)
47
+ modulesOfInterest.add(pkg)
30
48
  }
31
49
 
32
- const DC_CHANNEL = 'dd-trace:bundledModuleLoadStart'
33
-
34
50
  module.exports.name = 'datadog-esbuild'
35
51
 
36
52
  module.exports.setup = function (build) {
37
53
  build.onResolve({ filter: /.*/ }, args => {
54
+ let fullPathToModule
55
+ try {
56
+ fullPathToModule = dotFriendlyResolve(args.path, args.resolveDir)
57
+ } catch (err) {
58
+ console.warn(`Unable to find "${args.path}". Is the package dead code?`)
59
+ return
60
+ }
61
+ const extracted = extractPackageAndModulePath(fullPathToModule)
38
62
  const packageName = args.path
39
63
 
40
- if (args.namespace === 'file' && packagesOfInterest.has(packageName)) {
64
+ const internal = builtins.has(args.path)
65
+
66
+ if (args.namespace === 'file' && (
67
+ modulesOfInterest.has(packageName) || modulesOfInterest.has(`${extracted.pkg}/${extracted.path}`))
68
+ ) {
41
69
  // The file namespace is used when requiring files from disk in userland
42
70
 
43
71
  let pathToPackageJson
44
72
  try {
45
- pathToPackageJson = require.resolve(`${packageName}/package.json`, { paths: [ args.resolveDir ] })
73
+ pathToPackageJson = require.resolve(`${extracted.pkg}/package.json`, { paths: [ args.resolveDir ] })
46
74
  } catch (err) {
47
75
  if (err.code === 'MODULE_NOT_FOUND') {
48
- console.warn(`Unable to open "${packageName}/package.json". Is the "${packageName}" package dead code?`)
76
+ if (!internal) {
77
+ console.warn(`Unable to find "${extracted.pkg}/package.json". Is the package dead code?`)
78
+ }
49
79
  return
50
80
  } else {
51
81
  throw err
52
82
  }
53
83
  }
54
84
 
55
- const pkg = require(pathToPackageJson)
85
+ const packageJson = require(pathToPackageJson)
56
86
 
57
- if (DEBUG) {
58
- console.log(`resolve ${packageName}@${pkg.version}`)
59
- }
87
+ if (DEBUG) console.log(`RESOLVE ${packageName}@${packageJson.version}`)
60
88
 
61
89
  // https://esbuild.github.io/plugins/#on-resolve-arguments
62
90
  return {
63
- path: packageName,
91
+ path: fullPathToModule,
64
92
  namespace: NAMESPACE,
65
93
  pluginData: {
66
- version: pkg.version
94
+ version: packageJson.version,
95
+ pkg: extracted.pkg,
96
+ path: extracted.path,
97
+ full: fullPathToModule,
98
+ raw: packageName,
99
+ internal
67
100
  }
68
101
  }
69
- } else if (args.namespace === 'datadog') {
102
+ } else if (args.namespace === NAMESPACE) {
70
103
  // The datadog namespace is used when requiring files that are injected during the onLoad stage
71
- // see note in onLoad
72
104
 
73
105
  if (builtins.has(packageName)) return
74
106
 
@@ -80,23 +112,28 @@ module.exports.setup = function (build) {
80
112
  })
81
113
 
82
114
  build.onLoad({ filter: /.*/, namespace: NAMESPACE }, args => {
83
- if (DEBUG) {
84
- console.log(`load ${args.path}@${args.pluginData.version}`)
85
- }
115
+ const data = args.pluginData
116
+
117
+ if (DEBUG) console.log(`LOAD ${data.pkg}@${data.version}, pkg "${data.path}"`)
118
+
119
+ const path = data.raw !== data.pkg
120
+ ? `${data.pkg}/${data.path}`
121
+ : data.pkg
86
122
 
87
- // JSON.stringify adds double quotes. For perf gain could simply add in quotes when we know it's safe.
88
123
  const contents = `
89
124
  const dc = require('diagnostics_channel');
90
- const ch = dc.channel(${JSON.stringify(DC_CHANNEL + ':' + args.path)});
91
- const mod = require(${JSON.stringify(args.path)});
125
+ const ch = dc.channel('${CHANNEL}');
126
+ const mod = require('${args.path}');
92
127
  const payload = {
93
128
  module: mod,
94
- path: ${JSON.stringify(args.path)},
95
- version: ${JSON.stringify(args.pluginData.version)}
129
+ version: '${data.version}',
130
+ package: '${data.pkg}',
131
+ path: '${path}'
96
132
  };
97
133
  ch.publish(payload);
98
134
  module.exports = payload.module;
99
135
  `
136
+
100
137
  // https://esbuild.github.io/plugins/#on-load-results
101
138
  return {
102
139
  contents,
@@ -121,3 +158,44 @@ function warnIfUnsupported () {
121
158
  console.error('more recent version is used at runtime, third party packages won\'t be instrumented.')
122
159
  }
123
160
  }
161
+
162
+ // @see https://github.com/nodejs/node/issues/47000
163
+ function dotFriendlyResolve (path, directory) {
164
+ if (path === '.') {
165
+ path = './'
166
+ } else if (path === '..') {
167
+ path = '../'
168
+ }
169
+
170
+ return require.resolve(path, { paths: [ directory ] })
171
+ }
172
+
173
+ /**
174
+ * For a given full path to a module,
175
+ * return the package name it belongs to and the local path to the module
176
+ * input: '/foo/node_modules/@co/stuff/foo/bar/baz.js'
177
+ * output: { pkg: '@co/stuff', path: 'foo/bar/baz.js' }
178
+ */
179
+ function extractPackageAndModulePath (fullPath) {
180
+ const nm = fullPath.lastIndexOf(NM)
181
+ if (nm < 0) {
182
+ return { pkg: null, path: null }
183
+ }
184
+
185
+ const subPath = fullPath.substring(nm + NM.length)
186
+ const firstSlash = subPath.indexOf('/')
187
+
188
+ if (subPath[0] === '@') {
189
+ const secondSlash = subPath.substring(firstSlash + 1).indexOf('/')
190
+
191
+ return {
192
+ pkg: subPath.substring(0, firstSlash + 1 + secondSlash),
193
+ path: subPath.substring(firstSlash + 1 + secondSlash + 1)
194
+ }
195
+ }
196
+
197
+ return {
198
+ pkg: subPath.substring(0, firstSlash),
199
+ path: subPath.substring(firstSlash + 1)
200
+ }
201
+ }
@@ -1,3 +1,4 @@
1
1
  'use strict'
2
2
 
3
+ require('./src/helpers/bundler-register')
3
4
  require('./src/helpers/register')
@@ -177,7 +177,6 @@ addHook({ name: '@aws-sdk/smithy-client', versions: ['>=3'] }, smithy => {
177
177
  })
178
178
 
179
179
  addHook({ name: 'aws-sdk', versions: ['>=2.3.0'] }, AWS => {
180
- shimmer.wrap(AWS.Request.prototype, 'promise', wrapRequest)
181
180
  shimmer.wrap(AWS.config, 'setPromisesDependency', setPromisesDependency => {
182
181
  return function wrappedSetPromisesDependency (dep) {
183
182
  const result = setPromisesDependency.apply(this, arguments)
@@ -188,9 +187,14 @@ addHook({ name: 'aws-sdk', versions: ['>=2.3.0'] }, AWS => {
188
187
  return AWS
189
188
  })
190
189
 
190
+ addHook({ name: 'aws-sdk', file: 'lib/core.js', versions: ['>=2.3.0'] }, AWS => {
191
+ shimmer.wrap(AWS.Request.prototype, 'promise', wrapRequest)
192
+ return AWS
193
+ })
194
+
191
195
  // <2.1.35 has breaking changes for instrumentation
192
196
  // https://github.com/aws/aws-sdk-js/pull/629
193
- addHook({ name: 'aws-sdk', versions: ['>=2.1.35'] }, AWS => {
197
+ addHook({ name: 'aws-sdk', file: 'lib/core.js', versions: ['>=2.1.35'] }, AWS => {
194
198
  shimmer.wrap(AWS.Request.prototype, 'send', wrapRequest)
195
199
  return AWS
196
200
  })
@@ -17,29 +17,32 @@ function wrapFetch (fetch, Request) {
17
17
  const headers = req.headers
18
18
  const message = { req, headers }
19
19
 
20
- startChannel.publish(message)
21
-
22
- // Request object is read-only so we need new objects to change headers.
23
- arguments[0] = message.req
24
- arguments[1] = { headers: message.headers }
25
-
26
- return fetch.apply(this, arguments)
27
- .then(
28
- res => {
29
- finishChannel.publish({ req, res })
30
-
31
- return res
32
- },
33
- err => {
34
- if (err.name !== 'AbortError') {
35
- errorChannel.publish(err)
36
- }
20
+ return startChannel.runStores(message, () => {
21
+ // Request object is read-only so we need new objects to change headers.
22
+ arguments[0] = message.req
23
+ arguments[1] = { headers: message.headers }
24
+
25
+ return fetch.apply(this, arguments)
26
+ .then(
27
+ res => {
28
+ message.res = res
29
+
30
+ finishChannel.publish(message)
37
31
 
38
- finishChannel.publish({ req })
32
+ return res
33
+ },
34
+ err => {
35
+ if (err.name !== 'AbortError') {
36
+ message.error = err
37
+ errorChannel.publish(message)
38
+ }
39
39
 
40
- throw err
41
- }
42
- )
40
+ finishChannel.publish(message)
41
+
42
+ throw err
43
+ }
44
+ )
45
+ })
43
46
  }
44
47
  }
45
48
 
@@ -0,0 +1,54 @@
1
+ 'use strict'
2
+
3
+ // eslint-disable-next-line n/no-restricted-require
4
+ const dc = require('diagnostics_channel')
5
+
6
+ const {
7
+ filename,
8
+ loadChannel,
9
+ matchVersion
10
+ } = require('./register.js')
11
+ const hooks = require('./hooks')
12
+ const instrumentations = require('./instrumentations')
13
+ const log = require('../../../dd-trace/src/log')
14
+
15
+ const CHANNEL = 'dd-trace:bundler:load'
16
+
17
+ if (!dc.subscribe) {
18
+ dc.subscribe = (channel, cb) => {
19
+ dc.channel(channel).subscribe(cb)
20
+ }
21
+ }
22
+ if (!dc.unsubscribe) {
23
+ dc.unsubscribe = (channel, cb) => {
24
+ if (dc.channel(channel).hasSubscribers) {
25
+ dc.channel(channel).unsubscribe(cb)
26
+ }
27
+ }
28
+ }
29
+
30
+ dc.subscribe(CHANNEL, (payload) => {
31
+ try {
32
+ hooks[payload.package]()
33
+ } catch (err) {
34
+ log.error(`esbuild-wrapped ${payload.package} missing in list of hooks`)
35
+ throw err
36
+ }
37
+
38
+ if (!instrumentations[payload.package]) {
39
+ log.error(`esbuild-wrapped ${payload.package} missing in list of instrumentations`)
40
+ return
41
+ }
42
+
43
+ for (const { name, file, versions, hook } of instrumentations[payload.package]) {
44
+ if (payload.path !== filename(name, file)) continue
45
+ if (!matchVersion(payload.version, versions)) continue
46
+
47
+ try {
48
+ loadChannel.publish({ name, version: payload.version, file })
49
+ payload.module = hook(payload.module, payload.version)
50
+ } catch (e) {
51
+ log.error(e)
52
+ }
53
+ }
54
+ })
@@ -3,10 +3,9 @@
3
3
  const path = require('path')
4
4
  const iitm = require('../../../dd-trace/src/iitm')
5
5
  const ritm = require('../../../dd-trace/src/ritm')
6
- const dcitm = require('../../../dd-trace/src/dcitm')
7
6
 
8
7
  /**
9
- * This is called for every module that dd-trace supports instrumentation for.
8
+ * This is called for every package/internal-module that dd-trace supports instrumentation for
10
9
  * In practice, `modules` is always an array with a single entry.
11
10
  *
12
11
  * @param {string[]} modules list of modules to hook into
@@ -41,13 +40,11 @@ function Hook (modules, onrequire) {
41
40
  return safeHook(moduleExports, moduleName, moduleBaseDir)
42
41
  }
43
42
  })
44
- this._dcitmHook = dcitm(modules, {}, safeHook)
45
43
  }
46
44
 
47
45
  Hook.prototype.unhook = function () {
48
46
  this._ritmHook.unhook()
49
47
  this._iitmHook.unhook()
50
- this._dcitmHook.unhook()
51
48
  this._patched = Object.create(null)
52
49
  }
53
50
 
@@ -11,6 +11,8 @@ module.exports = {
11
11
  '@hapi/hapi': () => require('../hapi'),
12
12
  '@jest/core': () => require('../jest'),
13
13
  '@jest/reporters': () => require('../jest'),
14
+ '@jest/test-sequencer': () => require('../jest'),
15
+ '@jest/transform': () => require('../jest'),
14
16
  '@koa/router': () => require('../koa'),
15
17
  '@node-redis/client': () => require('../redis'),
16
18
  '@opensearch-project/opensearch': () => require('../opensearch'),
@@ -38,6 +40,7 @@ module.exports = {
38
40
  'find-my-way': () => require('../find-my-way'),
39
41
  'fs': () => require('../fs'),
40
42
  'node:fs': () => require('../fs'),
43
+ 'generic-pool': () => require('../generic-pool'),
41
44
  'graphql': () => require('../graphql'),
42
45
  'grpc': () => require('../grpc'),
43
46
  'hapi': () => require('../hapi'),
@@ -51,6 +54,7 @@ module.exports = {
51
54
  'jest-environment-jsdom': () => require('../jest'),
52
55
  'jest-jasmine2': () => require('../jest'),
53
56
  'jest-worker': () => require('../jest'),
57
+ 'knex': () => require('../knex'),
54
58
  'koa': () => require('../koa'),
55
59
  'koa-router': () => require('../koa'),
56
60
  'kafkajs': () => require('../kafkajs'),
@@ -20,7 +20,9 @@ const disabledInstrumentations = new Set(
20
20
  const loadChannel = channel('dd-trace:instrumentation:load')
21
21
 
22
22
  // Globals
23
- require('../fetch')
23
+ if (!disabledInstrumentations.has('fetch')) {
24
+ require('../fetch')
25
+ }
24
26
 
25
27
  // TODO: make this more efficient
26
28
 
@@ -30,6 +32,7 @@ for (const packageName of names) {
30
32
  Hook([packageName], (moduleExports, moduleName, moduleBaseDir, moduleVersion) => {
31
33
  moduleName = moduleName.replace(pathSepExpr, '/')
32
34
 
35
+ // This executes the integration file thus adding its entries to `instrumentations`
33
36
  hooks[packageName]()
34
37
 
35
38
  if (!instrumentations[packageName]) {
@@ -74,5 +77,7 @@ function filename (name, file) {
74
77
 
75
78
  module.exports = {
76
79
  filename,
77
- pathSepExpr
80
+ pathSepExpr,
81
+ loadChannel,
82
+ matchVersion
78
83
  }
@@ -3,18 +3,16 @@
3
3
  /* eslint-disable no-fallthrough */
4
4
 
5
5
  const url = require('url')
6
- const {
7
- channel,
8
- addHook,
9
- AsyncResource
10
- } = require('../helpers/instrument')
6
+ const { channel, addHook } = require('../helpers/instrument')
11
7
  const shimmer = require('../../../datadog-shimmer')
12
8
 
13
9
  const log = require('../../../dd-trace/src/log')
14
10
 
15
- const startClientCh = channel('apm:http:client:request:start')
16
- const finishClientCh = channel('apm:http:client:request:finish')
17
- const errorClientCh = channel('apm:http:client:request:error')
11
+ const startChannel = channel('apm:http:client:request:start')
12
+ const finishChannel = channel('apm:http:client:request:finish')
13
+ const endChannel = channel('apm:http:client:request:end')
14
+ const asyncStartChannel = channel('apm:http:client:request:asyncStart')
15
+ const errorChannel = channel('apm:http:client:request:error')
18
16
 
19
17
  addHook({ name: 'https' }, hookFn)
20
18
 
@@ -32,7 +30,7 @@ function patch (http, methodName) {
32
30
 
33
31
  function instrumentRequest (request) {
34
32
  return function () {
35
- if (!startClientCh.hasSubscribers) {
33
+ if (!startChannel.hasSubscribers) {
36
34
  return request.apply(this, arguments)
37
35
  }
38
36
 
@@ -45,57 +43,68 @@ function patch (http, methodName) {
45
43
  return request.apply(this, arguments)
46
44
  }
47
45
 
48
- const callbackResource = new AsyncResource('bound-anonymous-fn')
49
- const asyncResource = new AsyncResource('bound-anonymous-fn')
50
-
51
- return asyncResource.runInAsyncScope(() => {
52
- startClientCh.publish({ args, http })
46
+ const ctx = { args, http }
53
47
 
48
+ return startChannel.runStores(ctx, () => {
54
49
  let finished = false
55
50
  let callback = args.callback
56
51
 
57
52
  if (callback) {
58
- callback = callbackResource.bind(callback)
53
+ callback = function () {
54
+ return asyncStartChannel.runStores(ctx, () => {
55
+ return args.callback.apply(this, arguments)
56
+ })
57
+ }
59
58
  }
60
59
 
61
60
  const options = args.options
62
- const req = request.call(this, options, callback)
63
- const emit = req.emit
64
-
65
- const finish = (req, res) => {
61
+ const finish = () => {
66
62
  if (!finished) {
67
63
  finished = true
68
- finishClientCh.publish({ req, res })
64
+ finishChannel.publish(ctx)
69
65
  }
70
66
  }
71
67
 
72
- req.emit = function (eventName, arg) {
73
- asyncResource.runInAsyncScope(() => {
68
+ try {
69
+ const req = request.call(this, options, callback)
70
+ const emit = req.emit
71
+
72
+ ctx.req = req
73
+
74
+ req.emit = function (eventName, arg) {
74
75
  switch (eventName) {
75
76
  case 'response': {
76
77
  const res = arg
77
- const listener = asyncResource.bind(() => finish(req, res))
78
- res.on('end', listener)
79
- res.on('error', listener)
78
+ ctx.res = res
79
+ res.on('end', finish)
80
+ res.on('error', finish)
80
81
  break
81
82
  }
82
83
  case 'connect':
83
84
  case 'upgrade':
84
- finish(req, arg)
85
+ ctx.res = arg
86
+ finish()
85
87
  break
86
88
  case 'error':
87
89
  case 'timeout':
88
- errorClientCh.publish(arg)
90
+ ctx.error = arg
91
+ errorChannel.publish(ctx)
89
92
  case 'abort': // deprecated and replaced by `close` in node 17
90
93
  case 'close':
91
- finish(req)
94
+ finish()
92
95
  }
93
- })
94
96
 
95
- return emit.apply(this, arguments)
96
- }
97
+ return emit.apply(this, arguments)
98
+ }
97
99
 
98
- return req
100
+ return req
101
+ } catch (e) {
102
+ ctx.error = e
103
+ errorChannel.publish(ctx)
104
+ throw e
105
+ } finally {
106
+ endChannel.publish(ctx)
107
+ }
99
108
  })
100
109
  }
101
110
  }
@@ -42,6 +42,7 @@ function createWrapRequest (authority, options) {
42
42
  } catch (e) {
43
43
  ctx.error = e
44
44
  errorChannel.publish(ctx)
45
+ throw e
45
46
  } finally {
46
47
  endChannel.publish(ctx)
47
48
  }
@@ -188,6 +188,31 @@ addHook({
188
188
  versions: ['>=24.8.0']
189
189
  }, getTestEnvironment)
190
190
 
191
+ addHook({
192
+ name: '@jest/test-sequencer',
193
+ versions: ['>=24.8.0']
194
+ }, sequencerPackage => {
195
+ shimmer.wrap(sequencerPackage.default.prototype, 'shard', shard => function () {
196
+ const shardedTests = shard.apply(this, arguments)
197
+
198
+ if (!shardedTests.length) {
199
+ return shardedTests
200
+ }
201
+ // TODO: could we get the rootDir from each test?
202
+ const [test] = shardedTests
203
+ const rootDir = test && test.context && test.context.config && test.context.config.rootDir
204
+
205
+ const filteredTests = getJestSuitesToRun(skippableSuites, shardedTests, rootDir || process.cwd())
206
+
207
+ isSuitesSkipped = filteredTests.length !== shardedTests.length
208
+
209
+ skippableSuites = []
210
+
211
+ return filteredTests
212
+ })
213
+ return sequencerPackage
214
+ })
215
+
191
216
  function cliWrapper (cli, jestVersion) {
192
217
  const wrapped = shimmer.wrap(cli, 'runCLI', runCLI => async function () {
193
218
  let onDone
@@ -410,6 +435,32 @@ function jestConfigSyncWrapper (jestConfig) {
410
435
  return jestConfig
411
436
  }
412
437
 
438
+ addHook({
439
+ name: '@jest/transform',
440
+ versions: ['>=24.8.0'],
441
+ file: 'build/ScriptTransformer.js'
442
+ }, transformPackage => {
443
+ const originalCreateScriptTransformer = transformPackage.createScriptTransformer
444
+
445
+ transformPackage.createScriptTransformer = async function (config) {
446
+ const { testEnvironmentOptions, ...restOfConfig } = config
447
+ const {
448
+ _ddTestModuleId,
449
+ _ddTestSessionId,
450
+ _ddTestCommand,
451
+ ...restOfTestEnvironmentOptions
452
+ } = testEnvironmentOptions
453
+
454
+ restOfConfig.testEnvironmentOptions = restOfTestEnvironmentOptions
455
+
456
+ arguments[0] = restOfConfig
457
+
458
+ return originalCreateScriptTransformer.apply(this, arguments)
459
+ }
460
+
461
+ return transformPackage
462
+ })
463
+
413
464
  /**
414
465
  * Hook to remove the test paths (test suite) that are part of `skippableSuites`
415
466
  */
@@ -425,7 +476,18 @@ addHook({
425
476
  return getTestPaths.apply(this, arguments)
426
477
  }
427
478
 
428
- const [{ rootDir }] = arguments
479
+ const [{ rootDir, shard }] = arguments
480
+
481
+ if (shard && shard.shardIndex) {
482
+ // If the user is using jest sharding, we want to apply the filtering of tests in the shard process.
483
+ // The reason for this is the following:
484
+ // The tests for different shards are likely being run in different CI jobs so
485
+ // the requests to the skippable endpoint might be done at different times and their responses might be different.
486
+ // If the skippable endpoint is returning different suites and we filter the list of tests here,
487
+ // the base list of tests that is used for sharding might be different,
488
+ // causing the shards to potentially run the same suite.
489
+ return getTestPaths.apply(this, arguments)
490
+ }
429
491
 
430
492
  const testPaths = await getTestPaths.apply(this, arguments)
431
493
  const { tests } = testPaths
@@ -1,35 +1,30 @@
1
1
  'use strict'
2
2
 
3
3
  const HttpClientPlugin = require('../../datadog-plugin-http/src/client')
4
- const { HTTP_HEADERS } = require('../../../ext/formats')
5
4
 
6
5
  class FetchPlugin extends HttpClientPlugin {
7
6
  static get id () { return 'fetch' }
7
+ static get prefix () { return `apm:fetch:request` }
8
8
 
9
9
  addTraceSub (eventName, handler) {
10
10
  this.addSub(`apm:${this.constructor.id}:${this.operation}:${eventName}`, handler)
11
11
  }
12
12
 
13
- start (message) {
13
+ bindStart (message) {
14
14
  const req = message.req
15
15
  const options = new URL(req.url)
16
16
  const headers = options.headers = Object.fromEntries(req.headers.entries())
17
17
 
18
- const args = { options }
18
+ options.method = req.method
19
19
 
20
- super.start({ args })
20
+ message.args = { options }
21
21
 
22
- message.req = new globalThis.Request(req, { headers })
23
- }
22
+ const store = super.bindStart(message)
24
23
 
25
- _inject (span, headers) {
26
- const carrier = {}
27
-
28
- this.tracer.inject(span, HTTP_HEADERS, carrier)
24
+ message.headers = headers
25
+ message.req = new globalThis.Request(req, { headers })
29
26
 
30
- for (const name in carrier) {
31
- headers.append(name, carrier[name])
32
- }
27
+ return store
33
28
  }
34
29
  }
35
30
 
@@ -16,15 +16,11 @@ const HTTP_REQUEST_HEADERS = tags.HTTP_REQUEST_HEADERS
16
16
  const HTTP_RESPONSE_HEADERS = tags.HTTP_RESPONSE_HEADERS
17
17
 
18
18
  class HttpClientPlugin extends ClientPlugin {
19
- static get id () {
20
- return 'http'
21
- }
22
-
23
- addTraceSub (eventName, handler) {
24
- this.addSub(`apm:${this.constructor.id}:client:${this.operation}:${eventName}`, handler)
25
- }
19
+ static get id () { return 'http' }
20
+ static get prefix () { return `apm:http:client:request` }
26
21
 
27
- start ({ args, http = {} }) {
22
+ bindStart (message) {
23
+ const { args, http = {} } = message
28
24
  const store = storage.getStore()
29
25
  const options = args.options
30
26
  const agent = options.agent || options._defaultAgent || http.globalAgent || {}
@@ -55,7 +51,7 @@ class HttpClientPlugin extends ClientPlugin {
55
51
  metrics: {
56
52
  [CLIENT_PORT_KEY]: parseInt(options.port)
57
53
  }
58
- })
54
+ }, false)
59
55
 
60
56
  // TODO: Figure out a better way to do this for any span.
61
57
  if (!allowed) {
@@ -67,11 +63,19 @@ class HttpClientPlugin extends ClientPlugin {
67
63
  }
68
64
 
69
65
  analyticsSampler.sample(span, this.config.measured)
70
- this.enter(span, store)
66
+
67
+ message.span = span
68
+ message.parentStore = store
69
+ message.currentStore = { ...store, span }
70
+
71
+ return message.currentStore
71
72
  }
72
73
 
73
- finish ({ req, res }) {
74
- const span = storage.getStore().span
74
+ bindAsyncStart ({ parentStore }) {
75
+ return parentStore
76
+ }
77
+
78
+ finish ({ req, res, span }) {
75
79
  if (res) {
76
80
  const status = res.status || res.statusCode
77
81
 
@@ -87,17 +91,18 @@ class HttpClientPlugin extends ClientPlugin {
87
91
  addRequestHeaders(req, span, this.config)
88
92
 
89
93
  this.config.hooks.request(span, req, res)
90
- super.finish()
91
- }
92
94
 
93
- error (err) {
94
- const span = storage.getStore().span
95
+ this.tagPeerService(span)
96
+
97
+ span.finish()
98
+ }
95
99
 
96
- if (err) {
100
+ error ({ span, error }) {
101
+ if (error) {
97
102
  span.addTags({
98
- [ERROR_TYPE]: err.name,
99
- [ERROR_MESSAGE]: err.message || err.code,
100
- [ERROR_STACK]: err.stack
103
+ [ERROR_TYPE]: error.name,
104
+ [ERROR_MESSAGE]: error.message || error.code,
105
+ [ERROR_STACK]: error.stack
101
106
  })
102
107
  } else {
103
108
  span.setTag('error', 1)
@@ -57,7 +57,7 @@ function truncate (input) {
57
57
  }
58
58
 
59
59
  function shouldSimplify (input) {
60
- return !isObject(input)
60
+ return !isObject(input) || typeof input.toJSON === 'function'
61
61
  }
62
62
 
63
63
  function shouldHide (input) {
@@ -2,6 +2,7 @@
2
2
 
3
3
  const path = require('path')
4
4
 
5
+ const { getNodeModulesPaths } = require('../path-line')
5
6
  const Analyzer = require('./vulnerability-analyzer')
6
7
  const { WEAK_HASH } = require('../vulnerabilities')
7
8
 
@@ -11,13 +12,16 @@ const INSECURE_HASH_ALGORITHMS = new Set([
11
12
  'RSA-SHA1', 'RSA-SHA1-2', 'sha1', 'md5-sha1', 'sha1WithRSAEncryption', 'ssl3-sha1'
12
13
  ].map(algorithm => algorithm.toLowerCase()))
13
14
 
14
- const EXCLUDED_LOCATIONS = [
15
- path.join('node_modules', 'etag', 'index.js'),
16
- path.join('node_modules', 'redlock', 'dist', 'cjs'),
17
- path.join('node_modules', 'ws', 'lib', 'websocket-server.js'),
18
- path.join('node_modules', 'mysql2', 'lib', 'auth_41.js'),
19
- path.join('node_modules', '@mikro-orm', 'core', 'utils', 'Utils.js')
20
- ]
15
+ const EXCLUDED_LOCATIONS = getNodeModulesPaths(
16
+ 'etag/index.js',
17
+ '@mikro-orm/core/utils/Utils.js',
18
+ 'mongodb/lib/core/connection/connection.js',
19
+ 'mysql2/lib/auth_41.js',
20
+ 'pusher/lib/utils.js',
21
+ 'redlock/dist/cjs',
22
+ 'sqreen/lib/package-reader/index.js',
23
+ 'ws/lib/websocket-server.js'
24
+ )
21
25
 
22
26
  const EXCLUDED_PATHS_FROM_STACK = [
23
27
  path.join('node_modules', 'object-hash', path.sep)
@@ -69,7 +69,7 @@ function parseUser (login, passportUser, mode) {
69
69
  if (mode === 'safe') {
70
70
  // Remove PII in safe mode
71
71
  if (!regexUsername.test(user['usr.id'])) {
72
- user['usr.id'] = ' '
72
+ user['usr.id'] = ''
73
73
  }
74
74
  }
75
75
 
@@ -11,6 +11,7 @@ const tagger = require('./tagger')
11
11
  const { isTrue, isFalse } = require('./util')
12
12
  const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('./plugins/util/tags')
13
13
  const { getGitMetadataFromGitProperties } = require('./git_properties')
14
+ const { getIsGCPFunction, getIsAzureFunctionConsumptionPlan } = require('./serverless')
14
15
 
15
16
  const fromEntries = Object.fromEntries || (entries =>
16
17
  entries.reduce((obj, [k, v]) => Object.assign(obj, { [k]: v }), {}))
@@ -189,6 +190,7 @@ class Config {
189
190
  process.env.AWS_LAMBDA_FUNCTION_NAME ||
190
191
  process.env.FUNCTION_NAME || // Google Cloud Function Name set by deprecated runtimes
191
192
  process.env.K_SERVICE || // Google Cloud Function Name set by newer runtimes
193
+ process.env.WEBSITE_SITE_NAME || // set by Azure Functions
192
194
  pkg.name ||
193
195
  'node'
194
196
  const DD_SERVICE_MAPPING = coalesce(
@@ -227,11 +229,10 @@ class Config {
227
229
 
228
230
  const inAWSLambda = process.env.AWS_LAMBDA_FUNCTION_NAME !== undefined
229
231
 
230
- const isDeprecatedGCPFunction = process.env.FUNCTION_NAME !== undefined && process.env.GCP_PROJECT !== undefined
231
- const isNewerGCPFunction = process.env.K_SERVICE !== undefined && process.env.FUNCTION_TARGET !== undefined
232
- const isGCPFunction = isDeprecatedGCPFunction || isNewerGCPFunction
232
+ const isGCPFunction = getIsGCPFunction()
233
+ const isAzureFunctionConsumptionPlan = getIsAzureFunctionConsumptionPlan()
233
234
 
234
- const inServerlessEnvironment = inAWSLambda || isGCPFunction
235
+ const inServerlessEnvironment = inAWSLambda || isGCPFunction || isAzureFunctionConsumptionPlan
235
236
 
236
237
  const DD_TRACE_TELEMETRY_ENABLED = coalesce(
237
238
  process.env.DD_TRACE_TELEMETRY_ENABLED,
@@ -362,7 +363,7 @@ class Config {
362
363
  const DD_TRACE_STATS_COMPUTATION_ENABLED = coalesce(
363
364
  options.stats,
364
365
  process.env.DD_TRACE_STATS_COMPUTATION_ENABLED,
365
- isGCPFunction
366
+ isGCPFunction || isAzureFunctionConsumptionPlan
366
367
  )
367
368
 
368
369
  const DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED = coalesce(
@@ -678,6 +679,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
678
679
  this.traceId128BitLoggingEnabled = isTrue(DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED)
679
680
 
680
681
  this.isGCPFunction = isGCPFunction
682
+ this.isAzureFunctionConsumptionPlan = isAzureFunctionConsumptionPlan
681
683
 
682
684
  tagger.add(this.tags, {
683
685
  service: this.service,
@@ -135,7 +135,8 @@ module.exports = class PluginManager {
135
135
  site,
136
136
  url,
137
137
  dbmPropagationMode,
138
- dsmEnabled
138
+ dsmEnabled,
139
+ clientIpEnabled
139
140
  } = this._tracerConfig
140
141
 
141
142
  const sharedConfig = {}
@@ -155,6 +156,10 @@ module.exports = class PluginManager {
155
156
  sharedConfig.service = serviceMapping[name]
156
157
  }
157
158
 
159
+ if (clientIpEnabled !== undefined) {
160
+ sharedConfig.clientIpEnabled = clientIpEnabled
161
+ }
162
+
158
163
  sharedConfig.site = site
159
164
  sharedConfig.url = url
160
165
 
@@ -10,6 +10,8 @@ module.exports = {
10
10
  get '@grpc/grpc-js' () { return require('../../../datadog-plugin-grpc/src') },
11
11
  get '@hapi/hapi' () { return require('../../../datadog-plugin-hapi/src') },
12
12
  get '@jest/core' () { return require('../../../datadog-plugin-jest/src') },
13
+ get '@jest/test-sequencer' () { return require('../../../datadog-plugin-jest/src') },
14
+ get '@jest/transform' () { return require('../../../datadog-plugin-jest/src') },
13
15
  get '@koa/router' () { return require('../../../datadog-plugin-koa/src') },
14
16
  get '@node-redis/client' () { return require('../../../datadog-plugin-redis/src') },
15
17
  get '@opensearch-project/opensearch' () { return require('../../../datadog-plugin-opensearch/src') },
@@ -30,8 +30,8 @@ class Tracer extends NoopProxy {
30
30
  remoteConfig.enable(config)
31
31
  }
32
32
 
33
- if (config.isGCPFunction) {
34
- require('./serverless').maybeStartServerlessMiniAgent()
33
+ if (config.isGCPFunction || config.isAzureFunctionConsumptionPlan) {
34
+ require('./serverless').maybeStartServerlessMiniAgent(config)
35
35
  }
36
36
 
37
37
  if (config.profiling.enabled) {
@@ -1,14 +1,19 @@
1
1
  'use strict'
2
2
 
3
- function maybeStartServerlessMiniAgent () {
4
- let rustBinaryPath =
5
- '/workspace/node_modules/@datadog/sma/datadog-serverless-agent-linux-amd64/datadog-serverless-trace-mini-agent'
6
- if (process.env.DD_MINI_AGENT_PATH !== undefined) {
7
- rustBinaryPath = process.env.DD_MINI_AGENT_PATH
3
+ const log = require('./log')
4
+
5
+ function maybeStartServerlessMiniAgent (config) {
6
+ if (process.platform !== 'win32' && process.platform !== 'linux') {
7
+ log.error(`Serverless Mini Agent is only supported on Windows and Linux.`)
8
+ return
8
9
  }
9
- const log = require('./log')
10
+
11
+ const rustBinaryPath = getRustBinaryPath(config)
12
+
10
13
  const fs = require('fs')
11
14
 
15
+ log.debug(`Trying to spawn the Serverless Mini Agent at path: ${rustBinaryPath}`)
16
+
12
17
  // trying to spawn with an invalid path will return a non-descriptive error, so we want to catch
13
18
  // invalid paths and log our own error.
14
19
  if (!fs.existsSync(rustBinaryPath)) {
@@ -22,4 +27,43 @@ function maybeStartServerlessMiniAgent () {
22
27
  }
23
28
  }
24
29
 
25
- module.exports = { maybeStartServerlessMiniAgent }
30
+ function getRustBinaryPath (config) {
31
+ if (process.env.DD_MINI_AGENT_PATH !== undefined) {
32
+ return process.env.DD_MINI_AGENT_PATH
33
+ }
34
+
35
+ const rustBinaryPathRoot = config.isGCPFunction ? '/workspace' : '/home/site/wwwroot'
36
+ const rustBinaryPathOsFolder = process.platform === 'win32'
37
+ ? 'datadog-serverless-agent-windows-amd64' : 'datadog-serverless-agent-linux-amd64'
38
+
39
+ const rustBinaryExtension = process.platform === 'win32' ? '.exe' : ''
40
+
41
+ const rustBinaryPath =
42
+ `${rustBinaryPathRoot}/node_modules/@datadog/sma/${rustBinaryPathOsFolder}/\
43
+ datadog-serverless-trace-mini-agent${rustBinaryExtension}`
44
+
45
+ return rustBinaryPath
46
+ }
47
+
48
+ function getIsGCPFunction () {
49
+ const isDeprecatedGCPFunction = process.env.FUNCTION_NAME !== undefined && process.env.GCP_PROJECT !== undefined
50
+ const isNewerGCPFunction = process.env.K_SERVICE !== undefined && process.env.FUNCTION_TARGET !== undefined
51
+
52
+ return isDeprecatedGCPFunction || isNewerGCPFunction
53
+ }
54
+
55
+ function getIsAzureFunctionConsumptionPlan () {
56
+ const isAzureFunction =
57
+ process.env.FUNCTIONS_EXTENSION_VERSION !== undefined && process.env.FUNCTIONS_WORKER_RUNTIME !== undefined
58
+ const azureWebsiteSKU = process.env.WEBSITE_SKU
59
+ const isConsumptionPlan = azureWebsiteSKU === undefined || azureWebsiteSKU === 'Dynamic'
60
+
61
+ return isAzureFunction && isConsumptionPlan
62
+ }
63
+
64
+ module.exports = {
65
+ maybeStartServerlessMiniAgent,
66
+ getIsGCPFunction,
67
+ getIsAzureFunctionConsumptionPlan,
68
+ getRustBinaryPath
69
+ }
@@ -64,7 +64,7 @@ if (!Channel.prototype.runStores) {
64
64
  this._stores.set(store, transform)
65
65
  }
66
66
 
67
- Channel.prototype.unbindStore = ActiveChannelPrototype.runStores = function (store) {
67
+ Channel.prototype.unbindStore = ActiveChannelPrototype.unbindStore = function (store) {
68
68
  if (!this._stores) return
69
69
  this._stores.delete(store)
70
70
  }
@@ -1,53 +0,0 @@
1
- 'use strict'
2
-
3
- // TODO: Figure out why we can't use the internal version.
4
- // eslint-disable-next-line n/no-restricted-require
5
- const dc = require('diagnostics_channel')
6
-
7
- const CHANNEL_PREFIX = 'dd-trace:bundledModuleLoadStart'
8
-
9
- if (!dc.subscribe) {
10
- dc.subscribe = (channel, cb) => {
11
- dc.channel(channel).subscribe(cb)
12
- }
13
- }
14
- if (!dc.unsubscribe) {
15
- dc.unsubscribe = (channel, cb) => {
16
- if (dc.channel(channel).hasSubscribers) {
17
- dc.channel(channel).unsubscribe(cb)
18
- }
19
- }
20
- }
21
-
22
- module.exports = DcitmHook
23
-
24
- /**
25
- * This allows for listening to diagnostic channel events when a module is loaded.
26
- * Currently it's intended use is for situations like when code runs through a bundler.
27
- *
28
- * Unlike RITM and IITM, which have files available on a filesystem at runtime, DCITM
29
- * requires access to a package's version ahead of time as the package.json file likely
30
- * won't be available.
31
- *
32
- * This function runs many times at startup, once for every module that dd-trace may trace.
33
- * As it runs on a per-module basis we're creating per-module channels.
34
- */
35
- function DcitmHook (moduleNames, options, onrequire) {
36
- if (!(this instanceof DcitmHook)) return new DcitmHook(moduleNames, options, onrequire)
37
-
38
- function onModuleLoad (payload) {
39
- payload.module = onrequire(payload.module, payload.path, undefined, payload.version)
40
- }
41
-
42
- for (const moduleName of moduleNames) {
43
- // dc.channel(`${CHANNEL_PREFIX}:${moduleName}`).subscribe(onModuleLoad)
44
- dc.subscribe(`${CHANNEL_PREFIX}:${moduleName}`, onModuleLoad)
45
- }
46
-
47
- this.unhook = function dcitmUnload () {
48
- for (const moduleName of moduleNames) {
49
- // dc.channel(`${CHANNEL_PREFIX}:${moduleName}`).unsubscribe(onModuleLoad)
50
- dc.unsubscribe(`${CHANNEL_PREFIX}:${moduleName}`, onModuleLoad)
51
- }
52
- }
53
- }