dd-trace 5.65.0 → 5.66.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 (40) hide show
  1. package/package.json +8 -8
  2. package/packages/datadog-instrumentations/src/express.js +3 -7
  3. package/packages/datadog-instrumentations/src/graphql.js +10 -6
  4. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  5. package/packages/datadog-instrumentations/src/helpers/register.js +10 -2
  6. package/packages/datadog-instrumentations/src/playwright.js +25 -9
  7. package/packages/datadog-instrumentations/src/prisma.js +8 -10
  8. package/packages/datadog-instrumentations/src/ws.js +136 -0
  9. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +30 -3
  10. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +9 -6
  11. package/packages/datadog-plugin-aws-sdk/src/util.js +61 -1
  12. package/packages/datadog-plugin-express/src/code_origin.js +11 -0
  13. package/packages/datadog-plugin-graphql/src/index.js +3 -0
  14. package/packages/datadog-plugin-ws/src/close.js +69 -0
  15. package/packages/datadog-plugin-ws/src/index.js +26 -0
  16. package/packages/datadog-plugin-ws/src/producer.js +60 -0
  17. package/packages/datadog-plugin-ws/src/receiver.js +70 -0
  18. package/packages/datadog-plugin-ws/src/server.js +79 -0
  19. package/packages/datadog-shimmer/src/shimmer.js +11 -2
  20. package/packages/dd-trace/src/appsec/blocking.js +29 -0
  21. package/packages/dd-trace/src/appsec/channels.js +4 -2
  22. package/packages/dd-trace/src/appsec/index.js +7 -2
  23. package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +1 -0
  24. package/packages/dd-trace/src/appsec/rasp/index.js +25 -7
  25. package/packages/dd-trace/src/appsec/rasp/lfi.js +1 -1
  26. package/packages/dd-trace/src/appsec/rasp/utils.js +13 -2
  27. package/packages/dd-trace/src/config.js +12 -0
  28. package/packages/dd-trace/src/guardrails/index.js +11 -3
  29. package/packages/dd-trace/src/guardrails/telemetry.js +15 -16
  30. package/packages/dd-trace/src/llmobs/tagger.js +2 -1
  31. package/packages/dd-trace/src/log/writer.js +1 -1
  32. package/packages/dd-trace/src/plugin_manager.js +8 -2
  33. package/packages/dd-trace/src/plugins/index.js +2 -1
  34. package/packages/dd-trace/src/plugins/util/ip_extractor.js +48 -45
  35. package/packages/dd-trace/src/profiling/profilers/wall.js +9 -3
  36. package/packages/dd-trace/src/service-naming/schemas/v0/index.js +2 -1
  37. package/packages/dd-trace/src/service-naming/schemas/v0/websocket.js +30 -0
  38. package/packages/dd-trace/src/service-naming/schemas/v1/index.js +2 -1
  39. package/packages/dd-trace/src/service-naming/schemas/v1/websocket.js +30 -0
  40. package/packages/dd-trace/src/supported-configurations.json +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.65.0",
3
+ "version": "5.66.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -12,8 +12,8 @@
12
12
  "dependencies:dedupe": "yarn-deduplicate yarn.lock",
13
13
  "type:doc": "cd docs && yarn && yarn build",
14
14
  "type:test": "cd docs && yarn && yarn test",
15
- "lint": "node scripts/check_licenses.js && eslint . --max-warnings 0",
16
- "lint:fix": "node scripts/check_licenses.js && eslint . --max-warnings 0 --fix",
15
+ "lint": "node scripts/check_licenses.js && eslint . --concurrency=auto --max-warnings 0",
16
+ "lint:fix": "node scripts/check_licenses.js && eslint . --concurrency=auto --max-warnings 0 --fix",
17
17
  "lint:inspect": "npx @eslint/config-inspector@latest",
18
18
  "release:proposal": "node scripts/release/proposal",
19
19
  "services": "node ./scripts/install_plugin_modules && node packages/dd-trace/test/setup/services",
@@ -25,7 +25,7 @@
25
25
  "test:debugger": "mocha -r 'packages/dd-trace/test/setup/mocha.js' 'packages/dd-trace/test/debugger/**/*.spec.js'",
26
26
  "test:debugger:ci": "nyc --no-clean --include 'packages/dd-trace/src/debugger/**/*.js' -- npm run test:debugger",
27
27
  "test:eslint-rules": "node eslint-rules/*.test.mjs",
28
- "test:trace:core": "tap packages/dd-trace/test/*.spec.js \"packages/dd-trace/test/{ci-visibility,datastreams,encode,exporters,opentelemetry,opentracing,plugins,remote_config,service-naming,standalone,telemetry}/**/*.spec.js\"",
28
+ "test:trace:core": "tap packages/dd-trace/test/*.spec.js \"packages/dd-trace/test/{ci-visibility,datastreams,encode,exporters,opentelemetry,opentracing,plugins,remote_config,service-naming,standalone,telemetry,external-logger}/**/*.spec.js\"",
29
29
  "test:trace:core:ci": "npm run test:trace:core -- --coverage --nyc-arg=--include=\"packages/dd-trace/src/**/*.js\"",
30
30
  "test:trace:guardrails": "mocha -r \"packages/dd-trace/test/setup/mocha.js\" \"packages/dd-trace/test/guardrails/**/*.spec.js\"",
31
31
  "test:trace:guardrails:ci": "nyc --no-clean --include \"packages/dd-trace/src/guardrails/**/*.js\" -- npm run test:trace:guardrails",
@@ -114,10 +114,10 @@
114
114
  ],
115
115
  "dependencies": {
116
116
  "@datadog/libdatadog": "0.7.0",
117
- "@datadog/native-appsec": "10.1.0",
117
+ "@datadog/native-appsec": "10.2.1",
118
118
  "@datadog/native-iast-taint-tracking": "4.0.0",
119
119
  "@datadog/native-metrics": "3.1.1",
120
- "@datadog/pprof": "5.9.0",
120
+ "@datadog/pprof": "5.10.0",
121
121
  "@datadog/sketches-js": "2.1.1",
122
122
  "@datadog/wasm-js-rewriter": "4.0.1",
123
123
  "@isaacs/ttlcache": "^1.4.1",
@@ -138,7 +138,7 @@
138
138
  "mutexify": "^1.4.0",
139
139
  "opentracing": ">=0.14.7",
140
140
  "path-to-regexp": "^0.1.12",
141
- "pprof-format": "^2.1.0",
141
+ "pprof-format": "^2.1.1",
142
142
  "protobufjs": "^7.5.3",
143
143
  "retry": "^0.13.1",
144
144
  "rfdc": "^1.4.1",
@@ -177,7 +177,7 @@
177
177
  "nock": "^13.5.6",
178
178
  "nyc": "^15.1.0",
179
179
  "octokit": "^5.0.3",
180
- "proxyquire": "^1.8.0",
180
+ "proxyquire": "^2.1.3",
181
181
  "rimraf": "^3.0.2",
182
182
  "semver": "^7.7.2",
183
183
  "sinon": "^18.0.1",
@@ -67,6 +67,9 @@ addHook({ name: 'express', versions: ['>=4'] }, express => {
67
67
  return express
68
68
  })
69
69
 
70
+ // Express 5 does not rely on router in the same way as v4 and should not be instrumented anymore.
71
+ // It would otherwise produce spans for router and express, and so duplicating them.
72
+ // We now fall back to router instrumentation
70
73
  addHook({ name: 'express', versions: ['4'] }, express => {
71
74
  shimmer.wrap(express.Router, 'use', wrapRouterMethod)
72
75
  shimmer.wrap(express.Router, 'route', wrapRouterMethod)
@@ -74,13 +77,6 @@ addHook({ name: 'express', versions: ['4'] }, express => {
74
77
  return express
75
78
  })
76
79
 
77
- addHook({ name: 'express', versions: ['>=5.0.0'] }, express => {
78
- shimmer.wrap(express.Router.prototype, 'use', wrapRouterMethod)
79
- shimmer.wrap(express.Router.prototype, 'route', wrapRouterMethod)
80
-
81
- return express
82
- })
83
-
84
80
  const queryParserReadCh = channel('datadog:query:read:finish')
85
81
 
86
82
  function publishQueryParsedAndNext (req, res, next) {
@@ -235,14 +235,18 @@ function callInAsyncScope (fn, thisArg, args, abortController, cb) {
235
235
  try {
236
236
  const result = fn.apply(thisArg, args)
237
237
  if (result && typeof result.then === 'function') {
238
- // bind callback to this scope
239
- result.then(
240
- res => cb(null, res),
241
- err => cb(err)
238
+ return result.then(
239
+ res => {
240
+ cb(null, res)
241
+ return res
242
+ },
243
+ err => {
244
+ cb(err)
245
+ throw err
246
+ }
242
247
  )
243
- } else {
244
- cb(null, result)
245
248
  }
249
+ cb(null, result)
246
250
  return result
247
251
  } catch (err) {
248
252
  cb(err)
@@ -136,5 +136,6 @@ module.exports = {
136
136
  vm: () => require('../vm'),
137
137
  when: () => require('../when'),
138
138
  winston: () => require('../winston'),
139
- workerpool: () => require('../mocha')
139
+ workerpool: () => require('../mocha'),
140
+ ws: () => require('../ws')
140
141
  }
@@ -146,7 +146,11 @@ for (const packageName of names) {
146
146
  `error_type:${e.constructor.name}`,
147
147
  `integration:${name}`,
148
148
  `integration_version:${version}`
149
- ])
149
+ ], {
150
+ result: 'error',
151
+ result_class: 'internal_error',
152
+ result_reason: `Error during instrumentation of ${name}@${version}: ${e.message}`
153
+ })
150
154
  }
151
155
  namesAndSuccesses[`${name}@${version}`] = true
152
156
  }
@@ -160,7 +164,11 @@ for (const packageName of names) {
160
164
  telemetry('abort.integration', [
161
165
  `integration:${name}`,
162
166
  `integration_version:${version}`
163
- ])
167
+ ], {
168
+ result: 'abort',
169
+ result_class: 'incompatible_library',
170
+ result_reason: `Incompatible integration version: ${name}@${version}`
171
+ })
164
172
  log.info('Found incompatible integration version: %s', nameVersion)
165
173
  seenCombo.add(nameVersion)
166
174
  }
@@ -60,9 +60,15 @@ let testManagementTests = {}
60
60
  let isImpactedTestsEnabled = false
61
61
  let modifiedTests = {}
62
62
  const quarantinedOrDisabledTestsAttemptToFix = []
63
+ let quarantinedButNotAttemptToFixFqns = new Set()
63
64
  let rootDir = ''
64
65
  const MINIMUM_SUPPORTED_VERSION_RANGE_EFD = '>=1.38.0' // TODO: remove this once we drop support for v5
65
66
 
67
+ function getTestFullyQualifiedName (test) {
68
+ const fullname = getTestFullname(test)
69
+ return `${test._requireFile} ${fullname}`
70
+ }
71
+
66
72
  function getTestProperties (test) {
67
73
  const testName = getTestFullname(test)
68
74
  const testSuite = getTestSuitePath(test._requireFile, rootDir)
@@ -327,8 +333,7 @@ function testEndHandler (test, annotations, testStatus, error, isTimeout, isMain
327
333
  return
328
334
  }
329
335
 
330
- const testFullName = getTestFullname(test)
331
- const testFqn = `${testSuiteAbsolutePath} ${testFullName}`
336
+ const testFqn = getTestFullyQualifiedName(test)
332
337
  const testStatuses = testsToTestStatuses.get(testFqn) || []
333
338
 
334
339
  if (testStatuses.length === 0) {
@@ -618,19 +623,25 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
618
623
  if (isTestManagementTestsEnabled && sessionStatus === 'failed') {
619
624
  let totalFailedTestCount = 0
620
625
  let totalAttemptToFixFailedTestCount = 0
626
+ let totalPureQuarantinedFailedTestCount = 0
621
627
 
622
- for (const testStatuses of testsToTestStatuses.values()) {
623
- totalFailedTestCount += testStatuses.filter(status => status === 'fail').length
628
+ for (const [fqn, testStatuses] of testsToTestStatuses.entries()) {
629
+ const failedCount = testStatuses.filter(status => status === 'fail').length
630
+ totalFailedTestCount += failedCount
631
+ if (quarantinedButNotAttemptToFixFqns.has(fqn)) {
632
+ totalPureQuarantinedFailedTestCount += failedCount
633
+ }
624
634
  }
625
635
 
626
636
  for (const test of quarantinedOrDisabledTestsAttemptToFix) {
627
- const fullname = getTestFullname(test)
628
- const fqn = `${test._requireFile} ${fullname}`
629
- const testStatuses = testsToTestStatuses.get(fqn)
637
+ const testFqn = getTestFullyQualifiedName(test)
638
+ const testStatuses = testsToTestStatuses.get(testFqn)
630
639
  totalAttemptToFixFailedTestCount += testStatuses.filter(status => status === 'fail').length
631
640
  }
632
641
 
633
- if (totalFailedTestCount > 0 && totalFailedTestCount === totalAttemptToFixFailedTestCount) {
642
+ const totalIgnorableFailures = totalAttemptToFixFailedTestCount + totalPureQuarantinedFailedTestCount
643
+
644
+ if (totalFailedTestCount > 0 && totalFailedTestCount === totalIgnorableFailures) {
634
645
  runAllTestsReturn = 'passed'
635
646
  }
636
647
  }
@@ -648,6 +659,7 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
648
659
 
649
660
  startedSuites = []
650
661
  remainingTestsByFile = {}
662
+ quarantinedButNotAttemptToFixFqns = new Set()
651
663
 
652
664
  // TODO: we can trick playwright into thinking the session passed by returning
653
665
  // 'passed' here. We might be able to use this for both EFD and Test Management tests.
@@ -776,8 +788,12 @@ addHook({
776
788
  if (testProperties.disabled || testProperties.quarantined) {
777
789
  quarantinedOrDisabledTestsAttemptToFix.push(test)
778
790
  }
779
- } else if (testProperties.disabled || testProperties.quarantined) {
791
+ } else if (testProperties.disabled) {
780
792
  test.expectedStatus = 'skipped'
793
+ } else if (testProperties.quarantined) {
794
+ // Do not skip quarantined tests, let them run and overwrite results post-run if they fail
795
+ const testFqn = getTestFullyQualifiedName(test)
796
+ quarantinedButNotAttemptToFixFqns.add(testFqn)
781
797
  }
782
798
  }
783
799
  }
@@ -66,23 +66,21 @@ class TracingHelper {
66
66
  addHook({ name: '@prisma/client', versions: ['>=6.1.0'] }, (prisma, version) => {
67
67
  const tracingHelper = new TracingHelper()
68
68
 
69
- /*
70
- * This is a custom PrismaClient that extends the original PrismaClient
71
- * This allows us to grab additional information from the PrismaClient such as DB connection strings
72
- */
73
- class PrismaClient extends prisma.PrismaClient {
74
- constructor (...args) {
75
- super(...args)
76
-
77
- const datasources = this._engine?.config.inlineDatasources?.db.url?.value
69
+ // we need to patch the prototype to get db config since this works for ESM and CJS alike.
70
+ const originalRequest = prisma.PrismaClient.prototype._request
71
+ prisma.PrismaClient.prototype._request = function () {
72
+ if (!tracingHelper.dbConfig) {
73
+ const inlineDatasources = this._engine?.config.inlineDatasources
74
+ const overrideDatasources = this._engine?.config.overrideDatasources
75
+ const datasources = inlineDatasources?.db.url?.value ?? overrideDatasources?.db?.url
78
76
  if (datasources) {
79
77
  const result = parseDBString(datasources)
80
78
  tracingHelper.setDbString(result)
81
79
  }
82
80
  }
81
+ return originalRequest.apply(this, arguments)
83
82
  }
84
83
 
85
- prisma.PrismaClient = PrismaClient
86
84
  /*
87
85
  * This is taking advantage of the built in tracing support from Prisma.
88
86
  * The below variable is setting a global tracing helper that Prisma uses
@@ -0,0 +1,136 @@
1
+ 'use strict'
2
+
3
+ const {
4
+ addHook,
5
+ channel
6
+ } = require('./helpers/instrument')
7
+ const shimmer = require('../../datadog-shimmer')
8
+
9
+ const tracingChannel = require('dc-polyfill').tracingChannel
10
+ const serverCh = tracingChannel('ws:server:connect')
11
+ const producerCh = tracingChannel('ws:send')
12
+ const receiverCh = tracingChannel('ws:receive')
13
+ const closeCh = tracingChannel('ws:close')
14
+ const emitCh = channel('tracing:ws:server:connect:emit')
15
+
16
+ function wrapHandleUpgrade (handleUpgrade) {
17
+ return function () {
18
+ const [req, socket, , cb] = arguments
19
+ if (!serverCh.start.hasSubscribers || typeof cb !== 'function') {
20
+ return handleUpgrade.apply(this, arguments)
21
+ }
22
+
23
+ const ctx = { req, socket }
24
+
25
+ arguments[3] = function () {
26
+ return serverCh.asyncStart.runStores(ctx, () => {
27
+ try {
28
+ return cb.apply(this, arguments)
29
+ } finally {
30
+ serverCh.asyncEnd.publish(ctx)
31
+ }
32
+ }, this, ...arguments)
33
+ }
34
+ return serverCh.traceSync(handleUpgrade, ctx, this, ...arguments)
35
+ }
36
+ }
37
+
38
+ function wrapSend (send) {
39
+ return function wrappedSend (...args) {
40
+ if (!producerCh.start.hasSubscribers) return send.apply(this, arguments)
41
+
42
+ const [data, options, cb] = arguments
43
+
44
+ const ctx = { data, socket: this._sender._socket }
45
+
46
+ return typeof cb === 'function'
47
+ ? producerCh.traceCallback(send, undefined, ctx, this, data, options, cb)
48
+ : producerCh.traceSync(send, ctx, this, data, options, cb)
49
+ }
50
+ }
51
+
52
+ function createWrapEmit (emit) {
53
+ return function (title, headers, req) {
54
+ if (!serverCh.start.hasSubscribers || title !== 'headers') return emit.apply(this, arguments)
55
+
56
+ const ctx = { req }
57
+ ctx.req.resStatus = headers[0].split(' ')[1]
58
+
59
+ emitCh.runStores(ctx, () => {
60
+ try {
61
+ return emit.apply(this, arguments)
62
+ } finally {
63
+ emitCh.publish(ctx)
64
+ }
65
+ })
66
+ }
67
+ }
68
+
69
+ function createWrappedHandler (handler) {
70
+ return function wrappedMessageHandler (data, binary) {
71
+ const byteLength = dataLength(data)
72
+
73
+ const ctx = { data, binary, socket: this._sender._socket, byteLength }
74
+
75
+ return receiverCh.traceSync(handler, ctx, this, data, binary)
76
+ }
77
+ }
78
+
79
+ function wrapListener (originalOn) {
80
+ return function (eventName, handler) {
81
+ if (eventName === 'message') {
82
+ return originalOn.call(this, eventName, createWrappedHandler(handler))
83
+ }
84
+ return originalOn.apply(this, arguments)
85
+ }
86
+ }
87
+
88
+ function wrapClose (close) {
89
+ return function (code, data) {
90
+ // _closeFrameReceived is set to true when receiver receives a close frame from a peer
91
+ // _closeFrameSent is set to true when a close frame is sent
92
+ // in the case that a close frame is received and not yet sent then connection is closed by peer
93
+ // if both are true then the self is sending the close event
94
+ const isPeerClose = this._closeFrameReceived === true && this._closeFrameSent === false
95
+
96
+ const ctx = { code, data, socket: this._sender._socket, isPeerClose }
97
+
98
+ return closeCh.traceSync(close, ctx, this, ...arguments)
99
+ }
100
+ }
101
+
102
+ addHook({
103
+ name: 'ws',
104
+ file: 'lib/websocket-server.js',
105
+ versions: ['>=8.0.0']
106
+ }, ws => {
107
+ shimmer.wrap(ws.prototype, 'handleUpgrade', wrapHandleUpgrade)
108
+ shimmer.wrap(ws.prototype, 'emit', createWrapEmit)
109
+ return ws
110
+ })
111
+
112
+ addHook({
113
+ name: 'ws',
114
+ file: 'lib/websocket.js',
115
+ versions: ['>=8.0.0']
116
+ }, ws => {
117
+ shimmer.wrap(ws.prototype, 'send', wrapSend)
118
+ shimmer.wrap(ws.prototype, 'on', wrapListener)
119
+ shimmer.wrap(ws.prototype, 'close', wrapClose)
120
+ return ws
121
+ })
122
+
123
+ function detectType (data) {
124
+ if (typeof Blob !== 'undefined' && data instanceof Blob) return 'Blob'
125
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(data)) return 'Buffer'
126
+ if (typeof data === 'string') return 'string'
127
+ return 'Unknown'
128
+ }
129
+
130
+ function dataLength (data) {
131
+ const type = detectType(data)
132
+ if (type === 'Blob') return data.size
133
+ if (type === 'Buffer') return data.length
134
+ if (type === 'string') return Buffer.byteLength(data)
135
+ return 0
136
+ }
@@ -149,12 +149,24 @@ function extractRequestParams (params, provider) {
149
149
  })
150
150
  }
151
151
  case PROVIDER.ANTHROPIC: {
152
- const prompt = requestBody.prompt || requestBody.messages
152
+ let prompt = requestBody.prompt
153
+ if (Array.isArray(requestBody.messages)) { // newer claude models
154
+ for (let idx = requestBody.messages.length - 1; idx >= 0; idx--) {
155
+ const message = requestBody.messages[idx]
156
+ if (message.role === 'user') {
157
+ prompt = message.content?.filter(block => block.type === 'text')
158
+ .map(block => block.text)
159
+ .join('')
160
+ break
161
+ }
162
+ }
163
+ }
164
+
153
165
  return new RequestParams({
154
166
  prompt,
155
167
  temperature: requestBody.temperature,
156
168
  topP: requestBody.top_p,
157
- maxTokens: requestBody.max_tokens_to_sample,
169
+ maxTokens: requestBody.max_tokens_to_sample ?? requestBody.max_tokens,
158
170
  stopSequences: requestBody.stop_sequences
159
171
  })
160
172
  }
@@ -253,7 +265,13 @@ function extractTextAndResponseReason (response, provider, modelName) {
253
265
  break
254
266
  }
255
267
  case PROVIDER.ANTHROPIC: {
256
- return new Generation({ message: body.completion || body.content, finishReason: body.stop_reason })
268
+ let message = body.completion
269
+ if (Array.isArray(body.content)) { // newer claude models
270
+ message = body.content.find(item => item.type === 'text')?.text ?? body.content
271
+ } else if (body.content) {
272
+ message = body.content
273
+ }
274
+ return new Generation({ message, finishReason: body.stop_reason })
257
275
  }
258
276
  case PROVIDER.COHERE: {
259
277
  if (modelName.includes('embed')) {
@@ -262,6 +280,15 @@ function extractTextAndResponseReason (response, provider, modelName) {
262
280
  return new Generation({ message: embeddings[0] })
263
281
  }
264
282
  }
283
+
284
+ if (body.text) {
285
+ return new Generation({
286
+ message: body.text,
287
+ finishReason: body.finish_reason,
288
+ choiceId: shouldSetChoiceIds ? body.response_id : undefined
289
+ })
290
+ }
291
+
265
292
  const generations = body.generations || []
266
293
  if (generations.length > 0) {
267
294
  const generation = generations[0]
@@ -3,6 +3,7 @@
3
3
  const log = require('../../../dd-trace/src/log')
4
4
  const BaseAwsSdkPlugin = require('../base')
5
5
  const { DsmPathwayCodec, getHeadersSize } = require('../../../dd-trace/src/datastreams')
6
+ const { extractQueueMetadata } = require('../util')
6
7
 
7
8
  class Sqs extends BaseAwsSdkPlugin {
8
9
  static id = 'sqs'
@@ -92,16 +93,18 @@ class Sqs extends BaseAwsSdkPlugin {
92
93
 
93
94
  generateTags (params, operation, response) {
94
95
  if (!params || (!params.QueueName && !params.QueueUrl)) return {}
95
- // 'https://sqs.us-east-1.amazonaws.com/123456789012/my-queue';
96
- let queueName = params.QueueName
97
- if (params.QueueUrl) {
98
- queueName = params.QueueUrl.split('/').at(-1)
99
- }
96
+
97
+ const queueMetadata = extractQueueMetadata(params.QueueUrl)
98
+ const queueName = queueMetadata?.queueName || params.QueueName
100
99
 
101
100
  const tags = {
102
101
  'resource.name': `${operation} ${params.QueueName || params.QueueUrl}`,
103
102
  'aws.sqs.queue_name': params.QueueName || params.QueueUrl,
104
- queuename: queueName
103
+ queuename: queueName,
104
+ }
105
+
106
+ if (queueMetadata?.arn) {
107
+ tags['cloud.resource_id'] = queueMetadata.arn
105
108
  }
106
109
 
107
110
  switch (operation) {
@@ -84,8 +84,68 @@ const extractPrimaryKeys = (keyNames, keyValuePairs) => {
84
84
  }
85
85
  }
86
86
 
87
+ /**
88
+ * Extracts queue metadata from an SQS queue URL for span tagging.
89
+ * Handles modern and legacy AWS endpoint formats, with or without schemes.
90
+ * Automatically detects AWS partitions (standard, China, GovCloud) from region.
91
+ *
92
+ * @param {string} queueURL - SQS queue URL in any supported format
93
+ * @returns {Object|null} Object with queueName and arn, or null if URL format is invalid
94
+ *
95
+ * @example
96
+ * // Modern AWS SQS URLs
97
+ * extractQueueMetadata('https://sqs.us-east-1.amazonaws.com/123456789012/my-queue')
98
+ * // Returns { queueName: 'my-queue', arn: 'arn:aws:sqs:us-east-1:123456789012:my-queue' }
99
+ *
100
+ * extractQueueMetadata('sqs.eu-west-1.amazonaws.com/123456789012/my-queue') // no scheme
101
+ * // Returns { queueName: 'my-queue', arn: 'arn:aws:sqs:eu-west-1:123456789012:my-queue' }
102
+ *
103
+ * // Legacy AWS SQS URLs
104
+ * extractQueueMetadata('https://us-west-2.queue.amazonaws.com/123456789012/legacy-queue')
105
+ * // Returns { queueName: 'legacy-queue', arn: 'arn:aws:sqs:us-west-2:123456789012:legacy-queue' }
106
+ *
107
+ * extractQueueMetadata('https://queue.amazonaws.com/123456789012/global-legacy-queue')
108
+ * // Returns { queueName: 'global-legacy-queue', arn: 'arn:aws:sqs:us-east-1:123456789012:global-legacy-queue' }
109
+ */
110
+ const extractQueueMetadata = queueURL => {
111
+ if (!queueURL) {
112
+ return null
113
+ }
114
+
115
+ const parts = queueURL.split('/').filter(Boolean)
116
+
117
+ // Check if URL has scheme
118
+ const hasScheme = Boolean(parts[0]?.startsWith('http'))
119
+ const minParts = hasScheme ? 4 : 3
120
+
121
+ if (parts.length < minParts) return null
122
+
123
+ const accountId = parts[parts.length - 2]
124
+ const queueName = parts[parts.length - 1]
125
+ const host = hasScheme ? parts[1] : parts[0]
126
+
127
+ let region = 'us-east-1' // Default region if not found in URL
128
+ if (host.includes('.amazonaws.com') && !host.startsWith('queue')) {
129
+ // sqs.{region}.amazonaws.com or {region}.queue.amazonaws.com
130
+ const startFrom = host.startsWith('sqs.') ? 4 : 0
131
+ const nextDot = host.indexOf('.', startFrom)
132
+ region = host.slice(startFrom, nextDot)
133
+ }
134
+
135
+ let partition = 'aws'
136
+ if (region.startsWith('cn-')) {
137
+ partition = 'aws-cn'
138
+ } else if (region.startsWith('us-gov')) {
139
+ partition = 'aws-us-gov'
140
+ }
141
+
142
+ const arn = `arn:${partition}:sqs:${region}:${accountId}:${queueName}`
143
+ return { queueName, arn }
144
+ }
145
+
87
146
  module.exports = {
88
147
  generatePointerHash,
89
148
  encodeValue,
90
- extractPrimaryKeys
149
+ extractPrimaryKeys,
150
+ extractQueueMetadata
91
151
  }
@@ -22,6 +22,17 @@ class ExpressCodeOriginForSpansPlugin extends Plugin {
22
22
  if (layerTags.has(layer)) return
23
23
  layerTags.set(layer, entryTags(topOfStackFunc))
24
24
  })
25
+
26
+ this.addSub('apm:router:middleware:enter', ({ req, layer }) => {
27
+ const tags = layerTags.get(layer)
28
+ if (!tags) return
29
+ web.getContext(req).span?.addTags(tags)
30
+ })
31
+
32
+ this.addSub('apm:router:route:added', ({ topOfStackFunc, layer }) => {
33
+ if (layerTags.has(layer)) return
34
+ layerTags.set(layer, entryTags(topOfStackFunc))
35
+ })
25
36
  }
26
37
  }
27
38
 
@@ -19,6 +19,9 @@ class GraphQLPlugin extends CompositePlugin {
19
19
  }
20
20
  }
21
21
 
22
+ /**
23
+ * @override
24
+ */
22
25
  configure (config) {
23
26
  return super.configure(validateConfig(config))
24
27
  }
@@ -0,0 +1,69 @@
1
+ 'use strict'
2
+
3
+ const TracingPlugin = require('../../dd-trace/src/plugins/tracing.js')
4
+
5
+ class WSClosePlugin extends TracingPlugin {
6
+ static get id () { return 'ws' }
7
+ static get prefix () { return 'tracing:ws:close' }
8
+ static get type () { return 'websocket' }
9
+ static get kind () { return 'close' }
10
+
11
+ bindStart (ctx) {
12
+ const {
13
+ traceWebsocketMessagesEnabled,
14
+ traceWebsocketMessagesInheritSampling,
15
+ traceWebsocketMessagesSeparateTraces
16
+ } = this.config
17
+ if (!traceWebsocketMessagesEnabled) return
18
+
19
+ const { code, data, socket, isPeerClose } = ctx
20
+ if (!socket.spanContext) return
21
+
22
+ const spanKind = isPeerClose ? 'consumer' : 'producer'
23
+ const spanTags = socket.spanContext.spanTags
24
+ const path = spanTags['resource.name'].split(' ')[1]
25
+ const service = this.serviceName({ pluginConfig: this.config })
26
+ const span = this.startSpan(this.operationName(), {
27
+ service,
28
+ meta: {
29
+ 'resource.name': `websocket ${path}`,
30
+ 'span.type': 'websocket',
31
+ 'span.kind': spanKind,
32
+ 'websocket.close.code': code
33
+
34
+ }
35
+ }, ctx)
36
+
37
+ if (data?.toString().length > 0) {
38
+ span.setTag('websocket.close.reason', data.toString())
39
+ }
40
+
41
+ if (isPeerClose && traceWebsocketMessagesInheritSampling && traceWebsocketMessagesSeparateTraces) {
42
+ span.setTag('_dd.dm.service', spanTags['service.name'] || service)
43
+ span.setTag('_dd.dm.resource', spanTags['resource.name'] || `websocket ${path}`)
44
+ span.setTag('_dd.dm.inherited', 1)
45
+ }
46
+
47
+ ctx.span = span
48
+ return ctx.currentStore
49
+ }
50
+
51
+ bindAsyncStart (ctx) {
52
+ if (!ctx.isPeerClose) ctx.span.finish()
53
+ return ctx.parentStore
54
+ }
55
+
56
+ asyncStart (ctx) {
57
+ ctx.span.finish()
58
+ }
59
+
60
+ end (ctx) {
61
+ if (!Object.hasOwn(ctx, 'result')) return
62
+
63
+ if (ctx.socket.spanContext) ctx.span.addLink(ctx.socket.spanContext)
64
+
65
+ ctx.span.finish()
66
+ }
67
+ }
68
+
69
+ module.exports = WSClosePlugin