dd-trace 6.1.0 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/index.d.ts +7 -0
  3. package/initialize.mjs +4 -2
  4. package/package.json +5 -5
  5. package/packages/datadog-instrumentations/src/cookie.js +7 -1
  6. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -1
  7. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +5 -1
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +31 -0
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/modelcontextprotocol-sdk.js +30 -54
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +19 -5
  12. package/packages/datadog-instrumentations/src/http/server.js +27 -0
  13. package/packages/datadog-instrumentations/src/mercurius.js +11 -0
  14. package/packages/datadog-instrumentations/src/modelcontextprotocol-sdk.js +248 -1
  15. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +15 -0
  16. package/packages/datadog-instrumentations/src/playwright.js +40 -3
  17. package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
  18. package/packages/datadog-instrumentations/src/vitest-util.js +0 -3
  19. package/packages/datadog-instrumentations/src/vitest-worker.js +0 -1
  20. package/packages/datadog-plugin-azure-functions/src/index.js +2 -1
  21. package/packages/datadog-plugin-graphql/src/execute.js +4 -15
  22. package/packages/datadog-plugin-graphql/src/index.js +6 -0
  23. package/packages/datadog-plugin-graphql/src/request.js +104 -0
  24. package/packages/datadog-plugin-graphql/src/utils.js +177 -0
  25. package/packages/datadog-plugin-graphql/src/validate.js +24 -1
  26. package/packages/datadog-plugin-modelcontextprotocol-sdk/src/tracing.js +144 -26
  27. package/packages/datadog-plugin-modelcontextprotocol-sdk/src/utils.js +30 -0
  28. package/packages/datadog-plugin-playwright/src/index.js +1 -1
  29. package/packages/datadog-plugin-vitest/src/index.js +0 -8
  30. package/packages/datadog-shimmer/src/shimmer.js +9 -2
  31. package/packages/dd-trace/src/appsec/channels.js +1 -0
  32. package/packages/dd-trace/src/appsec/index.js +6 -3
  33. package/packages/dd-trace/src/appsec/lambda.js +8 -8
  34. package/packages/dd-trace/src/appsec/waf/waf_manager.js +1 -1
  35. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +23 -16
  36. package/packages/dd-trace/src/config/config-types.d.ts +8 -0
  37. package/packages/dd-trace/src/config/defaults.js +20 -15
  38. package/packages/dd-trace/src/config/index.js +13 -3
  39. package/packages/dd-trace/src/config/parsers.js +79 -0
  40. package/packages/dd-trace/src/datastreams/writer.js +14 -2
  41. package/packages/dd-trace/src/encode/0.4.js +23 -2
  42. package/packages/dd-trace/src/encode/0.5.js +12 -1
  43. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +11 -1
  44. package/packages/dd-trace/src/encode/coverage-ci-visibility.js +20 -2
  45. package/packages/dd-trace/src/exporters/common/writer.js +17 -1
  46. package/packages/dd-trace/src/guardrails/index.js +3 -1
  47. package/packages/dd-trace/src/guardrails/telemetry.js +40 -3
  48. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
  49. package/packages/dd-trace/src/llmobs/tagger.js +15 -1
  50. package/packages/dd-trace/src/msgpack/chunk.js +33 -1
  51. package/packages/dd-trace/src/msgpack/index.js +6 -1
  52. package/packages/dd-trace/src/opentelemetry/span_processor.js +7 -5
  53. package/packages/dd-trace/src/opentelemetry/tracer_provider.js +32 -16
  54. package/packages/dd-trace/src/plugins/index.js +3 -0
  55. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +20 -11
  56. package/packages/dd-trace/src/service-naming/schemas/v0/graphql.js +6 -0
  57. package/packages/dd-trace/src/service-naming/schemas/v1/graphql.js +8 -0
  58. package/vendor/dist/@apm-js-collab/code-transformer/index.js +5 -6
package/README.md CHANGED
@@ -28,7 +28,7 @@ Most of the documentation for `dd-trace` is available on these webpages:
28
28
  | [`v2`](https://github.com/DataDog/dd-trace-js/tree/v2.x) | ![npm v2](https://img.shields.io/npm/v/dd-trace/latest-node12?color=white&label=%20&style=flat-square) | `>= v12` | NO | NO | **EOL** | 2022-01-28 | 2023-08-15 |
29
29
  | [`v3`](https://github.com/DataDog/dd-trace-js/tree/v3.x) | ![npm v3](https://img.shields.io/npm/v/dd-trace/latest-node14?color=white&label=%20&style=flat-square) | `>= v14` | NO | YES | **EOL** | 2022-08-15 | 2024-05-15 |
30
30
  | [`v4`](https://github.com/DataDog/dd-trace-js/tree/v4.x) | ![npm v4](https://img.shields.io/npm/v/dd-trace/latest-node16?color=white&label=%20&style=flat-square) | `>= v16` | YES | YES | **EOL** | 2023-05-12 | 2025-01-11 |
31
- | [`v5`](https://github.com/DataDog/dd-trace-js/tree/v5.x) | ![npm v5](https://img.shields.io/npm/v/dd-trace/latest-node18?color=white&label=%20&style=flat-square) | `>= v18` | YES | YES | **Maintenance** | 2024-01-11 | Unknown |
31
+ | [`v5`](https://github.com/DataDog/dd-trace-js/tree/v5.x) | ![npm v5](https://img.shields.io/npm/v/dd-trace/latest-node18?color=white&label=%20&style=flat-square) | `>= v18` | YES | YES | **Maintenance** | 2024-01-11 | 2027-07-02 |
32
32
  | [`v6`](https://github.com/DataDog/dd-trace-js/tree/v6.x) | ![npm v6](https://img.shields.io/npm/v/dd-trace/latest?color=white&label=%20&style=flat-square) | `>= v22` | YES | YES | **Current** | 2026-07-02 | Unknown |
33
33
 
34
34
  * EOL = End-of-life
package/index.d.ts CHANGED
@@ -2514,6 +2514,13 @@ declare namespace tracer {
2514
2514
  * This plugin automatically instruments the
2515
2515
  * [graphql](https://github.com/graphql/graphql-js) module.
2516
2516
  *
2517
+ * It also instruments [mercurius](https://github.com/mercurius-js/mercurius)
2518
+ * (the Fastify GraphQL adapter): every request through `app.graphql` /
2519
+ * `reply.graphql` opens a top-level `graphql.request` span that parents the
2520
+ * `graphql.parse`/`graphql.validate`/`graphql.execute` spans and carries the
2521
+ * request text. This span is produced even when mercurius serves the query
2522
+ * from its JIT-compiled path, where `graphql.execute` does not run.
2523
+ *
2517
2524
  * The `graphql` integration uses the operation name as the span resource name.
2518
2525
  * If no operation name is set, the resource name will always be just `query`,
2519
2526
  * `mutation` or `subscription`.
package/initialize.mjs CHANGED
@@ -75,8 +75,10 @@ export const resolve = brokenLoaders ? undefined : hookResolve
75
75
 
76
76
  if (isMainThread) {
77
77
  const require = Module.createRequire(import.meta.url)
78
- require('./init.js')
79
- if (Module.register) {
78
+ const initialized = require('./init.js')
79
+ // Only register the loader hook when instrumentation initialized. On a bailout the
80
+ // loader has nothing to instrument and can keep a short-lived process from exiting.
81
+ if (Module.register && initialized) {
80
82
  // The loader builds its own include/exclude matcher in `initialize`, so no
81
83
  // options need to cross the registration boundary.
82
84
  Module.register('./loader-hook.mjs', import.meta.url)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "6.1.0",
3
+ "version": "6.3.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -23,7 +23,7 @@
23
23
  "lint:inspect": "npx @eslint/config-inspector@latest",
24
24
  "lint:codeowners": "codeowners-audit",
25
25
  "lint:codeowners:ci": "codeowners-audit --glob='**/*.spec.js' --glob='benchmark/sirun/**' --glob='.agents/**' --glob='.claude/**' --glob='integration-tests/**'",
26
- "lint:editorconfig": "editorconfig-checker",
26
+ "lint:editorconfig": "docker build -q -t ec .github/editorconfig-checker && docker run --rm --volume=$PWD:/check ec",
27
27
  "release:proposal": "node scripts/release/proposal",
28
28
  "services": "node ./scripts/install_plugin_modules && node packages/dd-trace/test/setup/services",
29
29
  "test": "echo '\nError: The root \"npm test\" command is intentionally disabled.\n\nInstead, run specific test suites:\n - npm run test:trace:core\n - npm run test:appsec\n - etc.\n\nOr run individual test files:\n npx mocha path/to/test.spec.js\n\nSee CONTRIBUTING.md (Testing section) for more details.\n' && exit 1",
@@ -191,18 +191,17 @@
191
191
  "@stylistic/eslint-plugin": "^5.10.0",
192
192
  "@types/mocha": "^10.0.10",
193
193
  "@types/node": "^18.19.106",
194
- "@types/sinon": "^21.0.1",
194
+ "@types/sinon": "^22.0.0",
195
195
  "axios": "^1.18.1",
196
196
  "benchmark": "^2.1.4",
197
197
  "body-parser": "^2.3.0",
198
198
  "bun": "1.3.14",
199
199
  "c8": "^11.0.0",
200
200
  "codeowners-audit": "^2.9.0",
201
- "editorconfig-checker": "^6.1.1",
202
201
  "eslint": "^9.39.2",
203
202
  "eslint-plugin-cypress": "^6.4.2",
204
203
  "eslint-plugin-import": "^2.32.0",
205
- "eslint-plugin-jsdoc": "^63.0.10",
204
+ "eslint-plugin-jsdoc": "^63.0.12",
206
205
  "eslint-plugin-mocha": "^11.3.0",
207
206
  "eslint-plugin-n": "^18.2.1",
208
207
  "eslint-plugin-promise": "^7.3.0",
@@ -230,6 +229,7 @@
230
229
  "semifies": "^1.0.0",
231
230
  "semver": "^7.8.5",
232
231
  "sinon": "^22.0.0",
232
+ "source-map-support": "^0.5.21",
233
233
  "test-exclude": "^8.0.0",
234
234
  "tiktoken": "^1.0.21",
235
235
  "typescript": "^6.0.3",
@@ -15,7 +15,13 @@ function wrapParse (originalParse) {
15
15
  }
16
16
  }
17
17
 
18
+ /** @param {typeof import('cookie')} cookie */
18
19
  addHook({ name: 'cookie', versions: ['>=0.4'] }, cookie => {
19
- shimmer.wrap(cookie, 'parse', wrapParse)
20
+ for (const name of ['parse', 'parseCookie']) {
21
+ if (typeof cookie[name] === 'function') {
22
+ // shimmer returns a mutable replacement when an ESM namespace export is non-configurable.
23
+ cookie = shimmer.wrap(cookie, name, wrapParse)
24
+ }
25
+ }
20
26
  return cookie
21
27
  })
@@ -20,7 +20,7 @@ module.exports = {
20
20
  '@apollo/server': () => require('../apollo-server'),
21
21
  '@apollo/gateway': () => require('../apollo'),
22
22
  '@langchain/langgraph': { esmFirst: true, fn: () => require('../langgraph') },
23
- '@modelcontextprotocol/sdk': () => require('../modelcontextprotocol-sdk'),
23
+ '@modelcontextprotocol/sdk': { esmFirst: true, fn: () => require('../modelcontextprotocol-sdk') },
24
24
  'apollo-server-core': () => require('../apollo-server-core'),
25
25
  '@aws-sdk/smithy-client': () => require('../aws-sdk'),
26
26
  '@aws/durable-execution-sdk-js': () => require('../aws-durable-execution-sdk-js'),
@@ -48,6 +48,7 @@ module.exports = {
48
48
  '@langchain/core': { esmFirst: true, fn: () => require('../langchain') },
49
49
  '@node-redis/client': () => require('../redis'),
50
50
  '@opensearch-project/opensearch': () => require('../opensearch'),
51
+ '@opentelemetry/sdk-trace': () => require('../otel-sdk-trace'),
51
52
  '@opentelemetry/sdk-trace-node': () => require('../otel-sdk-trace'),
52
53
  '@prisma/client': { esmFirst: true, fn: () => require('../prisma') },
53
54
  './runtime/library.js': () => require('../prisma'),
@@ -105,6 +106,7 @@ module.exports = {
105
106
  lodash: () => require('../lodash'),
106
107
  mariadb: () => require('../mariadb'),
107
108
  memcached: () => require('../memcached'),
109
+ mercurius: () => require('../mercurius'),
108
110
  'microgateway-core': () => require('../microgateway-core'),
109
111
  mocha: () => require('../mocha'),
110
112
  'mocha-each': () => require('../mocha'),
@@ -37,6 +37,10 @@ for (const matcher of [matcherCjs, matcherEsm]) {
37
37
  matcher.addTransform('waitForAsyncEnd', waitForAsyncEnd)
38
38
  }
39
39
 
40
+ // Keep the marker split: source-map scanners can read a contiguous token in
41
+ // string literals as this file's own inline map.
42
+ const SOURCE_MAP_PREFIX = '//# sourceMapping' + 'URL=data:application/json;base64,'
43
+
40
44
  function rewrite (content, filename, format) {
41
45
  if (!content) return content
42
46
  if (!filename.includes('node_modules')) return content
@@ -66,7 +70,7 @@ function rewrite (content, filename, format) {
66
70
 
67
71
  const inlineMap = Buffer.from(map).toString('base64')
68
72
 
69
- return code + '\n' + `//# sourceMappingURL=data:application/json;base64,${inlineMap}`
73
+ return code + '\n' + SOURCE_MAP_PREFIX + inlineMap
70
74
  } catch (e) {
71
75
  log.error(e)
72
76
  }
@@ -8,6 +8,7 @@ module.exports = [
8
8
  ...require('./graphql'),
9
9
  ...require('./langchain'),
10
10
  ...require('./langgraph'),
11
+ ...require('./mercurius'),
11
12
  ...require('./modelcontextprotocol-sdk'),
12
13
  ...require('./playwright'),
13
14
  ...require('./aws-durable-execution-sdk-js'),
@@ -0,0 +1,31 @@
1
+ 'use strict'
2
+
3
+ // Mercurius funnels every GraphQL request through the named async function
4
+ // `fastifyGraphQl` (decorated as both `app.graphql` and, via `reply.graphql`,
5
+ // the per-request entry point). Wrapping that one function gives a single
6
+ // top-level span per operation regardless of how the query reaches mercurius
7
+ // — HTTP POST/GET, batched queries, or a programmatic `app.graphql()` call —
8
+ // and regardless of whether the query runs through graphql's `execute` (cold)
9
+ // or a JIT-compiled query (warm), where no `graphql.execute` span fires.
10
+ //
11
+ // The function name and signature `(source, context, variables, operationName)`
12
+ // are stable across the supported major range (verified against 10.x and 16.x),
13
+ // so a `functionName` match needs no per-version file paths.
14
+ module.exports = [
15
+ {
16
+ module: {
17
+ // Floor at 13: it is the oldest major whose fastify-plugin peer (^4)
18
+ // accepts fastify 4, which installs and runs on the oldest supported Node
19
+ // (18). 15+ requires Node 20 and fastify 5, covered on the latest-Node CI
20
+ // leg. The `fastifyGraphQl` funnel is unchanged across this whole range.
21
+ name: 'mercurius',
22
+ versionRange: '>=13',
23
+ filePath: 'index.js',
24
+ },
25
+ functionQuery: {
26
+ functionName: 'fastifyGraphQl',
27
+ kind: 'Async',
28
+ },
29
+ channelName: 'apm:graphql:request',
30
+ },
31
+ ]
@@ -1,59 +1,35 @@
1
1
  'use strict'
2
2
 
3
- // NOTE: Protocol.request (dist/esm|cjs/shared/protocol.js) is intentionally not instrumented here.
4
- // It will be used for distributed tracing header injection when server-side coverage is added.
3
+ const CLIENT_METHODS = [
4
+ 'callTool',
5
+ 'listTools',
6
+ 'listResources',
7
+ 'readResource',
8
+ 'listPrompts',
9
+ 'getPrompt',
10
+ ]
11
+
12
+ const SERVER_METHODS = [
13
+ 'executeToolHandler',
14
+ ]
15
+
16
+ function clientEntries (methodName) {
17
+ return ['dist/esm/client/index.js', 'dist/cjs/client/index.js'].map(filePath => ({
18
+ module: { name: '@modelcontextprotocol/sdk', versionRange: '>=1.27.1', filePath },
19
+ functionQuery: { methodName, className: 'Client', kind: 'Async' },
20
+ channelName: `Client_${methodName}`,
21
+ }))
22
+ }
23
+
24
+ function serverEntries (methodName) {
25
+ return ['dist/esm/server/mcp.js', 'dist/cjs/server/mcp.js'].map(filePath => ({
26
+ module: { name: '@modelcontextprotocol/sdk', versionRange: '>=1.27.1', filePath },
27
+ functionQuery: { methodName, className: 'McpServer', kind: 'Async' },
28
+ channelName: `McpServer_${methodName}`,
29
+ }))
30
+ }
5
31
 
6
32
  module.exports = [
7
- {
8
- module: {
9
- name: '@modelcontextprotocol/sdk',
10
- versionRange: '>=1.27.1',
11
- filePath: 'dist/esm/client/index.js',
12
- },
13
- functionQuery: {
14
- methodName: 'callTool',
15
- className: 'Client',
16
- kind: 'Async',
17
- },
18
- channelName: 'Client_callTool',
19
- },
20
- {
21
- module: {
22
- name: '@modelcontextprotocol/sdk',
23
- versionRange: '>=1.27.1',
24
- filePath: 'dist/cjs/client/index.js',
25
- },
26
- functionQuery: {
27
- methodName: 'callTool',
28
- className: 'Client',
29
- kind: 'Async',
30
- },
31
- channelName: 'Client_callTool',
32
- },
33
- {
34
- module: {
35
- name: '@modelcontextprotocol/sdk',
36
- versionRange: '>=1.27.1',
37
- filePath: 'dist/esm/client/index.js',
38
- },
39
- functionQuery: {
40
- methodName: 'listTools',
41
- className: 'Client',
42
- kind: 'Async',
43
- },
44
- channelName: 'Client_listTools',
45
- },
46
- {
47
- module: {
48
- name: '@modelcontextprotocol/sdk',
49
- versionRange: '>=1.27.1',
50
- filePath: 'dist/cjs/client/index.js',
51
- },
52
- functionQuery: {
53
- methodName: 'listTools',
54
- className: 'Client',
55
- kind: 'Async',
56
- },
57
- channelName: 'Client_listTools',
58
- },
33
+ ...CLIENT_METHODS.flatMap(clientEntries),
34
+ ...SERVER_METHODS.flatMap(serverEntries),
59
35
  ]
@@ -8,6 +8,10 @@
8
8
  // the library, replace the custom registration with the built-in option and
9
9
  // remove the entry here.
10
10
 
11
+ const assert = require('node:assert')
12
+
13
+ const clone = require('../../../../../vendor/dist/rfdc')({ proto: false, circles: false })
14
+
11
15
  const { parse, query } = require('./compiler')
12
16
 
13
17
  module.exports = { waitForAsyncEnd }
@@ -28,20 +32,30 @@ function waitForAsyncEnd (_state, node) {
28
32
  return
29
33
  }
30
34
 
31
- const returnIndex = statements.findIndex(statement => (
32
- statement.type === 'ReturnStatement' && statement.argument?.name === 'result'
33
- ))
35
+ const returnIndex = statements.findIndex(statement =>
36
+ statement.type === 'ReturnStatement' && statement.argument
37
+ )
34
38
 
35
- if (returnIndex === -1) return
39
+ // The generated fulfillment handler always ends in a return; a miss means the
40
+ // upstream template changed and the caller's try/catch falls back to the
41
+ // unwrapped source.
42
+ assert(returnIndex !== -1, 'waitForAsyncEnd: no return statement to wait on')
36
43
 
37
44
  const waitStatements = parse(`
38
45
  function wrapper () {
39
46
  const __apm$asyncEndPromise = __apm$ctx.asyncEndPromise;
40
47
  if (__apm$asyncEndPromise && typeof __apm$asyncEndPromise.then === 'function') {
41
- return __apm$asyncEndPromise.then(() => result, () => result);
48
+ return __apm$asyncEndPromise.then(() => __apm$result, () => __apm$result);
42
49
  }
43
50
  }
44
51
  `).body[0].body.body
45
52
 
53
+ // Resolve to whatever the fulfillment handler returns (its return argument),
54
+ // so a subscriber that reassigned `__apm$ctx.result` in `asyncEnd` still wins.
55
+ const returnArgument = statements[returnIndex].argument
56
+ const { arguments: onSettled } = waitStatements[1].consequent.body[0].argument
57
+ onSettled[0].body = clone(returnArgument)
58
+ onSettled[1].body = clone(returnArgument)
59
+
46
60
  statements.splice(returnIndex, 0, ...waitStatements)
47
61
  }
@@ -13,6 +13,7 @@ const finishServerCh = channel('apm:http:server:request:finish')
13
13
  const startWriteHeadCh = channel('apm:http:server:response:writeHead:start')
14
14
  const finishSetHeaderCh = channel('datadog:http:server:response:set-header:finish')
15
15
  const startSetHeaderCh = channel('datadog:http:server:response:set-header:start')
16
+ const startInformationalResponseCh = channel('datadog:http:server:informational-response:start')
16
17
 
17
18
  const requestFinishedSet = new WeakSet()
18
19
 
@@ -24,10 +25,19 @@ addHook({ name: 'http' }, http => {
24
25
  shimmer.wrap(http.ServerResponse.prototype, 'end', wrapEnd)
25
26
  shimmer.wrap(http.ServerResponse.prototype, 'setHeader', wrapSetHeader)
26
27
  shimmer.wrap(http.ServerResponse.prototype, 'removeHeader', wrapAppendOrRemoveHeader)
28
+ if (http.ServerResponse.prototype.writeContinue) {
29
+ shimmer.wrap(http.ServerResponse.prototype, 'writeContinue', wrapInformationalResponse)
30
+ }
31
+ if (http.ServerResponse.prototype.writeProcessing) {
32
+ shimmer.wrap(http.ServerResponse.prototype, 'writeProcessing', wrapInformationalResponse)
33
+ }
27
34
  // Added in node v16.17.0
28
35
  if (http.ServerResponse.prototype.appendHeader) {
29
36
  shimmer.wrap(http.ServerResponse.prototype, 'appendHeader', wrapAppendOrRemoveHeader)
30
37
  }
38
+ if (http.ServerResponse.prototype.writeEarlyHints) {
39
+ shimmer.wrap(http.ServerResponse.prototype, 'writeEarlyHints', wrapInformationalResponse)
40
+ }
31
41
  return http
32
42
  })
33
43
 
@@ -212,6 +222,23 @@ function wrapAppendOrRemoveHeader (originalMethod) {
212
222
  }
213
223
  }
214
224
 
225
+ function wrapInformationalResponse (originalMethod) {
226
+ return function wrappedInformationalResponse (...args) {
227
+ if (!startInformationalResponseCh.hasSubscribers) {
228
+ return Reflect.apply(originalMethod, this, args)
229
+ }
230
+
231
+ const abortController = new AbortController()
232
+ startInformationalResponseCh.publish({ res: this, abortController })
233
+
234
+ if (abortController.signal.aborted) {
235
+ return
236
+ }
237
+
238
+ return Reflect.apply(originalMethod, this, args)
239
+ }
240
+ }
241
+
215
242
  function wrapEnd (end) {
216
243
  return function wrappedEnd (...args) {
217
244
  if (!startWriteHeadCh.hasSubscribers) {
@@ -0,0 +1,11 @@
1
+ // AUTO-GENERATED by dd-compile
2
+ // Orchestrion rewriter handles the actual instrumentation via JSON config.
3
+ // This file registers the module hooks for the rewriter to process.
4
+
5
+ 'use strict'
6
+
7
+ const { addHook, getHooks } = require('./helpers/instrument')
8
+
9
+ for (const hook of getHooks('mercurius')) {
10
+ addHook(hook, exports => exports)
11
+ }
@@ -1,7 +1,254 @@
1
1
  'use strict'
2
2
 
3
- const { addHook, getHooks } = require('./helpers/instrument')
3
+ const { tracingChannel } = require('dc-polyfill')
4
+ const shimmer = require('../../datadog-shimmer')
5
+ const { addHook, channel, getHooks } = require('./helpers/instrument')
4
6
 
5
7
  for (const hook of getHooks('@modelcontextprotocol/sdk')) {
6
8
  addHook(hook, exports => exports)
7
9
  }
10
+
11
+ const serverRequestCh = tracingChannel('apm:mcp:server:request')
12
+ const clientRequestInjectCh = channel('apm:mcp:client:request:inject')
13
+ const serverToolRegisteredCh = channel('apm:mcp:server:tool:registered')
14
+
15
+ const DISTRIBUTED_TRACE_META_KEY = '_dd_trace_context'
16
+ const TRACED_METHOD_PREFIX = /^(?:tools|resources|prompts)\//
17
+
18
+ // Maps Protocol instance → Map<requestId, ctx>. Shares the ctx object between
19
+ // _onrequest (span start, in the correct HTTP async context), the SDK request
20
+ // handler wrapper (result/error capture), and the SDK cleanup path (span finish).
21
+ const pendingContexts = new WeakMap()
22
+ const wrappedAbortControllerMaps = new WeakSet()
23
+ const wrappedRequestHandlerMaps = new WeakSet()
24
+ const wrappedRequestHandlers = new WeakSet()
25
+
26
+ function addTraceContextToRequest (request, traceContext) {
27
+ return {
28
+ ...request,
29
+ params: {
30
+ ...request.params,
31
+ _meta: {
32
+ ...request.params?._meta,
33
+ [DISTRIBUTED_TRACE_META_KEY]: traceContext,
34
+ },
35
+ },
36
+ }
37
+ }
38
+
39
+ function getPendingContext (protocol, requestId) {
40
+ return pendingContexts.get(protocol)?.get(requestId)
41
+ }
42
+
43
+ function finishServerRequest (protocol, requestId) {
44
+ const pending = pendingContexts.get(protocol)
45
+ const ctx = pending?.get(requestId)
46
+ if (!ctx) return
47
+
48
+ pending.delete(requestId)
49
+ serverRequestCh.asyncEnd.publish(ctx)
50
+ }
51
+
52
+ function wrapRequestHandler (protocol, handler) {
53
+ if (wrappedRequestHandlers.has(handler)) return handler
54
+
55
+ const wrappedHandler = function requestHandlerWithTrace (request, extra) {
56
+ const ctx = getPendingContext(protocol, extra?.requestId)
57
+ if (!ctx) return handler.apply(this, arguments)
58
+
59
+ let result
60
+ try {
61
+ result = handler.apply(this, arguments)
62
+ } catch (err) {
63
+ ctx.error = err
64
+ throw err
65
+ }
66
+
67
+ if (result && typeof result.then === 'function') {
68
+ result.then(result => {
69
+ ctx.result = result
70
+ }, err => {
71
+ ctx.error = err
72
+ })
73
+ return result
74
+ }
75
+
76
+ ctx.result = result
77
+ return result
78
+ }
79
+
80
+ wrappedRequestHandlers.add(wrappedHandler)
81
+ return wrappedHandler
82
+ }
83
+
84
+ function wrapRequestHandlers (protocol) {
85
+ const handlers = protocol._requestHandlers
86
+ if (!handlers || wrappedRequestHandlerMaps.has(handlers)) return
87
+
88
+ wrappedRequestHandlerMaps.add(handlers)
89
+ shimmer.wrap(handlers, 'set', function (original) {
90
+ return function setRequestHandlerWithTrace (method, handler) {
91
+ if (typeof handler === 'function') {
92
+ return original.call(this, method, wrapRequestHandler(protocol, handler))
93
+ }
94
+
95
+ return original.apply(this, arguments)
96
+ }
97
+ })
98
+ }
99
+
100
+ function wrapAbortControllers (protocol) {
101
+ const controllers = protocol._requestHandlerAbortControllers
102
+ if (!controllers || wrappedAbortControllerMaps.has(controllers)) return
103
+
104
+ wrappedAbortControllerMaps.add(controllers)
105
+ shimmer.wrap(controllers, 'delete', function (original) {
106
+ return function deleteAbortControllerWithTrace (requestId) {
107
+ finishServerRequest(protocol, requestId)
108
+ return original.apply(this, arguments)
109
+ }
110
+ })
111
+ shimmer.wrap(controllers, 'clear', function (original) {
112
+ return function clearAbortControllersWithTrace () {
113
+ for (const requestId of this.keys()) {
114
+ finishServerRequest(protocol, requestId)
115
+ }
116
+ return original.apply(this, arguments)
117
+ }
118
+ })
119
+ }
120
+
121
+ function wrapProtocol (Protocol) {
122
+ // Inject trace context into MCP request metadata so out-of-process MCP servers
123
+ // can parent server spans to the client operation span.
124
+ shimmer.wrap(Protocol.prototype, 'request', function (original) {
125
+ return function requestWithTraceContext (request, resultSchema, options) {
126
+ if (!clientRequestInjectCh.hasSubscribers || !TRACED_METHOD_PREFIX.test(request?.method)) {
127
+ return original.apply(this, arguments)
128
+ }
129
+
130
+ const ctx = {}
131
+ clientRequestInjectCh.publish(ctx)
132
+
133
+ if (!ctx.traceContext) {
134
+ return original.apply(this, arguments)
135
+ }
136
+
137
+ return original.call(this, addTraceContextToRequest(request, ctx.traceContext), resultSchema, options)
138
+ }
139
+ })
140
+
141
+ // Start spans in _onrequest — this runs inside the express POST /mcp handler's
142
+ // async context, so ALS correctly parents server spans under the HTTP span.
143
+ shimmer.wrap(Protocol.prototype, '_onrequest', function (original) {
144
+ return function _onrequestWithTrace (request, extra) {
145
+ if (!serverRequestCh.hasSubscribers || !TRACED_METHOD_PREFIX.test(request.method)) {
146
+ return original.call(this, request, extra)
147
+ }
148
+
149
+ wrapAbortControllers(this)
150
+ const ctx = { request, extra }
151
+ serverRequestCh.start.runStores(ctx, () => {
152
+ let pending = pendingContexts.get(this)
153
+ if (!pending) {
154
+ pending = new Map()
155
+ pendingContexts.set(this, pending)
156
+ }
157
+ pending.set(request.id, ctx)
158
+
159
+ try {
160
+ original.call(this, request, extra)
161
+ } catch (err) {
162
+ ctx.error = err
163
+ finishServerRequest(this, request.id)
164
+ throw err
165
+ }
166
+
167
+ // The SDK registers an AbortController only when it actually dispatches a handler.
168
+ // If none was registered (MethodNotFound path), no handler will run and our
169
+ // SDK cleanup hook will never fire — finish the span immediately.
170
+ if (!this._requestHandlerAbortControllers?.has(request.id)) {
171
+ ctx.error = { name: 'McpError', message: 'Method not found' }
172
+ finishServerRequest(this, request.id)
173
+ }
174
+ })
175
+ }
176
+ })
177
+
178
+ // The SDK's handler closure Zod-parses the request, stripping the JSON-RPC `id`.
179
+ // Use `extra.requestId` (the SDK's fullExtra field) to correlate with the pending ctx.
180
+ shimmer.wrap(Protocol.prototype, 'setRequestHandler', function (original) {
181
+ return function setRequestHandlerWithTrace () {
182
+ wrapRequestHandlers(this)
183
+ return original.apply(this, arguments)
184
+ }
185
+ })
186
+
187
+ return Protocol
188
+ }
189
+
190
+ function wrapMcpServer (McpServer) {
191
+ // Both public registration methods (tool/registerTool) delegate here — one hook covers both.
192
+ // Publishes to serverToolRegisteredCh so tracing.js can build a WeakMap of tool → name
193
+ // for O(1) lookup in McpServerToolCallPlugin (executeToolHandler receives the tool object,
194
+ // not the name string).
195
+ shimmer.wrap(McpServer.prototype, '_createRegisteredTool', function (original) {
196
+ return function (name) {
197
+ const result = original.apply(this, arguments)
198
+ if (serverToolRegisteredCh.hasSubscribers) {
199
+ const tool = this._registeredTools?.[name]
200
+ if (tool) {
201
+ serverToolRegisteredCh.publish({ tool, name })
202
+ shimmer.wrap(tool, 'update', function (update) {
203
+ return function updateWithTrace (updates) {
204
+ const result = update.apply(this, arguments)
205
+ if (updates && Object.hasOwn(updates, 'name')) {
206
+ serverToolRegisteredCh.publish({ tool, name: updates.name || undefined })
207
+ }
208
+ return result
209
+ }
210
+ })
211
+ }
212
+ }
213
+ return result
214
+ }
215
+ })
216
+
217
+ return McpServer
218
+ }
219
+
220
+ addHook({
221
+ name: '@modelcontextprotocol/sdk',
222
+ versions: ['>=1.27.1'],
223
+ file: 'dist/cjs/shared/protocol.js',
224
+ }, exports => {
225
+ wrapProtocol(exports.Protocol)
226
+ return exports
227
+ })
228
+
229
+ addHook({
230
+ name: '@modelcontextprotocol/sdk',
231
+ versions: ['>=1.27.1'],
232
+ file: 'dist/esm/shared/protocol.js',
233
+ }, exports => {
234
+ wrapProtocol(exports.Protocol)
235
+ return exports
236
+ })
237
+
238
+ addHook({
239
+ name: '@modelcontextprotocol/sdk',
240
+ versions: ['>=1.27.1'],
241
+ file: 'dist/cjs/server/mcp.js',
242
+ }, exports => {
243
+ wrapMcpServer(exports.McpServer)
244
+ return exports
245
+ })
246
+
247
+ addHook({
248
+ name: '@modelcontextprotocol/sdk',
249
+ versions: ['>=1.27.1'],
250
+ file: 'dist/esm/server/mcp.js',
251
+ }, exports => {
252
+ wrapMcpServer(exports.McpServer)
253
+ return exports
254
+ })
@@ -16,6 +16,21 @@ if (isOtelSdkEnabled()) {
16
16
  })
17
17
  return mod
18
18
  })
19
+
20
+ // As of @opentelemetry/sdk-node 0.220.0, NodeSDK builds its provider from
21
+ // @opentelemetry/sdk-trace's TracerProvider instead of sdk-trace-node's
22
+ // NodeTracerProvider, so the hook above no longer intercepts it. Wrap this
23
+ // export too, otherwise DD_TRACE_OTEL_ENABLED spans never reach the tracer.
24
+ addHook({
25
+ name: '@opentelemetry/sdk-trace',
26
+ file: 'build/src/TracerProvider.js',
27
+ versions: ['*'],
28
+ }, (mod) => {
29
+ shimmer.wrap(mod, 'TracerProvider', () => {
30
+ return tracer.TracerProvider
31
+ })
32
+ return mod
33
+ })
19
34
  }
20
35
 
21
36
  function isOtelSdkEnabled () {