dd-trace 5.33.1 → 5.35.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 (25) hide show
  1. package/LICENSE-3rdparty.csv +1 -2
  2. package/package.json +2 -3
  3. package/packages/datadog-instrumentations/src/aws-sdk.js +16 -0
  4. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  5. package/packages/datadog-instrumentations/src/passport.js +45 -0
  6. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +33 -6
  7. package/packages/datadog-plugin-dd-trace-api/src/index.js +120 -0
  8. package/packages/datadog-plugin-graphql/src/execute.js +6 -0
  9. package/packages/datadog-plugin-graphql/src/utils.js +40 -0
  10. package/packages/datadog-plugin-graphql/src/validate.js +6 -0
  11. package/packages/dd-trace/src/appsec/blocking.js +4 -1
  12. package/packages/dd-trace/src/appsec/channels.js +1 -0
  13. package/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +19 -1
  14. package/packages/dd-trace/src/appsec/iast/analyzers/weak-hash-analyzer.js +2 -1
  15. package/packages/dd-trace/src/appsec/index.js +17 -0
  16. package/packages/dd-trace/src/appsec/sdk/set_user.js +9 -0
  17. package/packages/dd-trace/src/appsec/sdk/user_blocking.js +1 -0
  18. package/packages/dd-trace/src/appsec/telemetry.js +10 -0
  19. package/packages/dd-trace/src/appsec/user_tracking.js +32 -6
  20. package/packages/dd-trace/src/config.js +8 -0
  21. package/packages/dd-trace/src/debugger/devtools_client/state.js +72 -17
  22. package/packages/dd-trace/src/id.js +0 -2
  23. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +48 -3
  24. package/packages/dd-trace/src/plugin_manager.js +12 -1
  25. package/packages/dd-trace/src/plugins/index.js +1 -0
@@ -35,6 +35,7 @@ dev,@apollo/server,MIT,Copyright (c) 2016-2020 Apollo Graph, Inc. (Formerly Mete
35
35
  dev,@types/node,MIT,Copyright Authors
36
36
  dev,@eslint/eslintrc,MIT,Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
37
37
  dev,@eslint/js,MIT,Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
38
+ dev,@msgpack/msgpack,ISC,Copyright 2019 The MessagePack Community
38
39
  dev,@stylistic/eslint-plugin-js,MIT,Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
39
40
  dev,autocannon,MIT,Copyright 2016 Matteo Collina
40
41
  dev,aws-sdk,Apache 2.0,Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
@@ -58,12 +59,10 @@ dev,get-port,MIT,Copyright Sindre Sorhus
58
59
  dev,glob,ISC,Copyright Isaac Z. Schlueter and Contributors
59
60
  dev,globals,MIT,Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
60
61
  dev,graphql,MIT,Copyright 2015 Facebook Inc.
61
- dev,int64-buffer,MIT,Copyright 2015-2016 Yusuke Kawasaki
62
62
  dev,jszip,MIT,Copyright 2015-2016 Stuart Knightley and contributors
63
63
  dev,knex,MIT,Copyright (c) 2013-present Tim Griesser
64
64
  dev,mkdirp,MIT,Copyright 2010 James Halliday
65
65
  dev,mocha,MIT,Copyright 2011-2018 JS Foundation and contributors https://js.foundation
66
- dev,msgpack-lite,MIT,Copyright 2015 Yusuke Kawasaki
67
66
  dev,multer,MIT,Copyright 2014 Hage Yaapa
68
67
  dev,nock,MIT,Copyright 2017 Pedro Teixeira and other contributors
69
68
  dev,nyc,ISC,Copyright 2015 Contributors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.33.1",
3
+ "version": "5.35.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -118,6 +118,7 @@
118
118
  "@apollo/server": "^4.11.0",
119
119
  "@eslint/eslintrc": "^3.1.0",
120
120
  "@eslint/js": "^9.11.1",
121
+ "@msgpack/msgpack": "^3.0.0-beta3",
121
122
  "@stylistic/eslint-plugin-js": "^2.8.0",
122
123
  "@types/node": "^16.0.0",
123
124
  "autocannon": "^4.5.2",
@@ -142,12 +143,10 @@
142
143
  "glob": "^7.1.6",
143
144
  "globals": "^15.10.0",
144
145
  "graphql": "0.13.2",
145
- "int64-buffer": "^0.1.9",
146
146
  "jszip": "^3.5.0",
147
147
  "knex": "^2.4.2",
148
148
  "mkdirp": "^3.0.1",
149
149
  "mocha": "^10",
150
- "msgpack-lite": "^0.1.26",
151
150
  "multer": "^1.4.5-lts.1",
152
151
  "nock": "^11.3.3",
153
152
  "nyc": "^15.1.0",
@@ -40,6 +40,18 @@ function wrapRequest (send) {
40
40
  }
41
41
  }
42
42
 
43
+ function wrapDeserialize (deserialize, channelSuffix) {
44
+ const headersCh = channel(`apm:aws:response:deserialize:${channelSuffix}`)
45
+
46
+ return function (response) {
47
+ if (headersCh.hasSubscribers) {
48
+ headersCh.publish({ headers: response.headers })
49
+ }
50
+
51
+ return deserialize.apply(this, arguments)
52
+ }
53
+ }
54
+
43
55
  function wrapSmithySend (send) {
44
56
  return function (command, ...args) {
45
57
  const cb = args[args.length - 1]
@@ -61,6 +73,10 @@ function wrapSmithySend (send) {
61
73
  const responseStartChannel = channel(`apm:aws:response:start:${channelSuffix}`)
62
74
  const responseFinishChannel = channel(`apm:aws:response:finish:${channelSuffix}`)
63
75
 
76
+ if (typeof command.deserialize === 'function') {
77
+ shimmer.wrap(command, 'deserialize', deserialize => wrapDeserialize(deserialize, channelSuffix))
78
+ }
79
+
64
80
  return innerAr.runInAsyncScope(() => {
65
81
  startCh.publish({
66
82
  serviceIdentifier,
@@ -102,6 +102,7 @@ module.exports = {
102
102
  oracledb: () => require('../oracledb'),
103
103
  openai: () => require('../openai'),
104
104
  paperplane: () => require('../paperplane'),
105
+ passport: () => require('../passport'),
105
106
  'passport-http': () => require('../passport-http'),
106
107
  'passport-local': () => require('../passport-local'),
107
108
  pg: () => require('../pg'),
@@ -0,0 +1,45 @@
1
+ 'use strict'
2
+
3
+ const shimmer = require('../../datadog-shimmer')
4
+ const { channel, addHook } = require('./helpers/instrument')
5
+
6
+ const onPassportDeserializeUserChannel = channel('datadog:passport:deserializeUser:finish')
7
+
8
+ function wrapDone (done) {
9
+ return function wrappedDone (err, user) {
10
+ if (!err && user) {
11
+ const abortController = new AbortController()
12
+
13
+ onPassportDeserializeUserChannel.publish({ user, abortController })
14
+
15
+ if (abortController.signal.aborted) return
16
+ }
17
+
18
+ return done.apply(this, arguments)
19
+ }
20
+ }
21
+
22
+ function wrapDeserializeUser (deserializeUser) {
23
+ return function wrappedDeserializeUser (fn, req, done) {
24
+ if (typeof fn === 'function') return deserializeUser.apply(this, arguments)
25
+
26
+ if (typeof req === 'function') {
27
+ done = req
28
+ arguments[1] = wrapDone(done)
29
+ } else {
30
+ arguments[2] = wrapDone(done)
31
+ }
32
+
33
+ return deserializeUser.apply(this, arguments)
34
+ }
35
+ }
36
+
37
+ addHook({
38
+ name: 'passport',
39
+ file: 'lib/authenticator.js',
40
+ versions: ['>=0.3.0']
41
+ }, Authenticator => {
42
+ shimmer.wrap(Authenticator.prototype, 'deserializeUser', wrapDeserializeUser)
43
+
44
+ return Authenticator
45
+ })
@@ -24,11 +24,23 @@ const PROVIDER = {
24
24
  }
25
25
 
26
26
  class Generation {
27
- constructor ({ message = '', finishReason = '', choiceId = '' } = {}) {
27
+ constructor ({
28
+ message = '',
29
+ finishReason = '',
30
+ choiceId = '',
31
+ role,
32
+ inputTokens,
33
+ outputTokens
34
+ } = {}) {
28
35
  // stringify message as it could be a single generated message as well as a list of embeddings
29
36
  this.message = typeof message === 'string' ? message : JSON.stringify(message) || ''
30
37
  this.finishReason = finishReason || ''
31
38
  this.choiceId = choiceId || undefined
39
+ this.role = role
40
+ this.usage = {
41
+ inputTokens,
42
+ outputTokens
43
+ }
32
44
  }
33
45
  }
34
46
 
@@ -202,9 +214,12 @@ function extractTextAndResponseReason (response, provider, modelName) {
202
214
  if (generations.length > 0) {
203
215
  const generation = generations[0]
204
216
  return new Generation({
205
- message: generation.message,
217
+ message: generation.message.content,
206
218
  finishReason: generation.finish_reason,
207
- choiceId: shouldSetChoiceIds ? generation.id : undefined
219
+ choiceId: shouldSetChoiceIds ? generation.id : undefined,
220
+ role: generation.message.role,
221
+ inputTokens: body.usage?.prompt_tokens,
222
+ outputTokens: body.usage?.completion_tokens
208
223
  })
209
224
  }
210
225
  }
@@ -214,7 +229,9 @@ function extractTextAndResponseReason (response, provider, modelName) {
214
229
  return new Generation({
215
230
  message: completion.data?.text,
216
231
  finishReason: completion?.finishReason,
217
- choiceId: shouldSetChoiceIds ? completion?.id : undefined
232
+ choiceId: shouldSetChoiceIds ? completion?.id : undefined,
233
+ inputTokens: body.usage?.prompt_tokens,
234
+ outputTokens: body.usage?.completion_tokens
218
235
  })
219
236
  }
220
237
  return new Generation()
@@ -226,7 +243,12 @@ function extractTextAndResponseReason (response, provider, modelName) {
226
243
  const results = body.results || []
227
244
  if (results.length > 0) {
228
245
  const result = results[0]
229
- return new Generation({ message: result.outputText, finishReason: result.completionReason })
246
+ return new Generation({
247
+ message: result.outputText,
248
+ finishReason: result.completionReason,
249
+ inputTokens: body.inputTextTokenCount,
250
+ outputTokens: result.tokenCount
251
+ })
230
252
  }
231
253
  break
232
254
  }
@@ -252,7 +274,12 @@ function extractTextAndResponseReason (response, provider, modelName) {
252
274
  break
253
275
  }
254
276
  case PROVIDER.META: {
255
- return new Generation({ message: body.generation, finishReason: body.stop_reason })
277
+ return new Generation({
278
+ message: body.generation,
279
+ finishReason: body.stop_reason,
280
+ inputTokens: body.prompt_token_count,
281
+ outputTokens: body.generation_token_count
282
+ })
256
283
  }
257
284
  case PROVIDER.MISTRAL: {
258
285
  const mistralGenerations = body.outputs || []
@@ -0,0 +1,120 @@
1
+ 'use strict'
2
+
3
+ const Plugin = require('../../dd-trace/src/plugins/plugin')
4
+ const telemetryMetrics = require('../../dd-trace/src/telemetry/metrics')
5
+ const apiMetrics = telemetryMetrics.manager.namespace('tracers')
6
+
7
+ // api ==> here
8
+ const objectMap = new WeakMap()
9
+
10
+ const injectionEnabledTag =
11
+ `injection_enabled:${process.env.DD_INJECTION_ENABLED ? 'yes' : 'no'}`
12
+
13
+ module.exports = class DdTraceApiPlugin extends Plugin {
14
+ static get id () {
15
+ return 'dd-trace-api'
16
+ }
17
+
18
+ constructor (...args) {
19
+ super(...args)
20
+
21
+ const tracer = this._tracer
22
+
23
+ this.addSub('datadog-api:v1:tracerinit', ({ proxy }) => {
24
+ const proxyVal = proxy()
25
+ objectMap.set(proxyVal, tracer)
26
+ objectMap.set(proxyVal.appsec, tracer.appsec)
27
+ objectMap.set(proxyVal.dogstatsd, tracer.dogstatsd)
28
+ })
29
+
30
+ const handleEvent = (name) => {
31
+ const counter = apiMetrics.count('dd_trace_api.called', [
32
+ `name:${name.replaceAll(':', '.')}`,
33
+ 'api_version:v1',
34
+ injectionEnabledTag
35
+ ])
36
+
37
+ // For v1, APIs are 1:1 with their internal equivalents, so we can just
38
+ // call the internal method directly. That's what we do here unless we
39
+ // want to override. As the API evolves, this may change.
40
+ this.addSub(`datadog-api:v1:${name}`, ({ self, args, ret, proxy, revProxy }) => {
41
+ counter.inc()
42
+
43
+ if (name.includes(':')) {
44
+ name = name.split(':').pop()
45
+ }
46
+
47
+ if (objectMap.has(self)) {
48
+ self = objectMap.get(self)
49
+ }
50
+
51
+ for (let i = 0; i < args.length; i++) {
52
+ if (objectMap.has(args[i])) {
53
+ args[i] = objectMap.get(args[i])
54
+ }
55
+ if (typeof args[i] === 'function') {
56
+ const orig = args[i]
57
+ args[i] = (...fnArgs) => {
58
+ for (let j = 0; j < fnArgs.length; j++) {
59
+ if (revProxy && revProxy[j]) {
60
+ const proxyVal = revProxy[j]()
61
+ objectMap.set(proxyVal, fnArgs[j])
62
+ fnArgs[j] = proxyVal
63
+ }
64
+ }
65
+ // TODO do we need to apply(this, ...) here?
66
+ return orig(...fnArgs)
67
+ }
68
+ }
69
+ }
70
+
71
+ try {
72
+ ret.value = self[name](...args)
73
+ if (proxy) {
74
+ const proxyVal = proxy()
75
+ objectMap.set(proxyVal, ret.value)
76
+ ret.value = proxyVal
77
+ } else if (ret.value && typeof ret.value === 'object') {
78
+ throw new TypeError(`Objects need proxies when returned via API (${name})`)
79
+ }
80
+ } catch (e) {
81
+ ret.error = e
82
+ }
83
+ })
84
+ }
85
+
86
+ // handleEvent('configure')
87
+ handleEvent('startSpan')
88
+ handleEvent('wrap')
89
+ handleEvent('trace')
90
+ handleEvent('inject')
91
+ handleEvent('extract')
92
+ handleEvent('getRumData')
93
+ handleEvent('profilerStarted')
94
+ handleEvent('context:toTraceId')
95
+ handleEvent('context:toSpanId')
96
+ handleEvent('context:toTraceparent')
97
+ handleEvent('span:context')
98
+ handleEvent('span:setTag')
99
+ handleEvent('span:addTags')
100
+ handleEvent('span:finish')
101
+ handleEvent('span:addLink')
102
+ handleEvent('scope')
103
+ handleEvent('scope:activate')
104
+ handleEvent('scope:active')
105
+ handleEvent('scope:bind')
106
+ handleEvent('appsec:blockRequest')
107
+ handleEvent('appsec:isUserBlocked')
108
+ handleEvent('appsec:setUser')
109
+ handleEvent('appsec:trackCustomEvent')
110
+ handleEvent('appsec:trackUserLoginFailureEvent')
111
+ handleEvent('appsec:trackUserLoginSuccessEvent')
112
+ handleEvent('dogstatsd:decrement')
113
+ handleEvent('dogstatsd:distribution')
114
+ handleEvent('dogstatsd:flush')
115
+ handleEvent('dogstatsd:gauge')
116
+ handleEvent('dogstatsd:histogram')
117
+ handleEvent('dogstatsd:increment')
118
+ handleEvent('use')
119
+ }
120
+ }
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4
+ const { extractErrorIntoSpanEvent } = require('./utils')
4
5
 
5
6
  let tools
6
7
 
@@ -34,6 +35,11 @@ class GraphQLExecutePlugin extends TracingPlugin {
34
35
  finish ({ res, args }) {
35
36
  const span = this.activeSpan
36
37
  this.config.hooks.execute(span, args, res)
38
+ if (res?.errors) {
39
+ for (const err of res.errors) {
40
+ extractErrorIntoSpanEvent(this._tracerConfig, span, err)
41
+ }
42
+ }
37
43
  super.finish()
38
44
  }
39
45
  }
@@ -0,0 +1,40 @@
1
+ function extractErrorIntoSpanEvent (config, span, exc) {
2
+ const attributes = {}
3
+
4
+ if (exc.name) {
5
+ attributes.type = exc.name
6
+ }
7
+
8
+ if (exc.stack) {
9
+ attributes.stacktrace = exc.stack
10
+ }
11
+
12
+ if (exc.locations) {
13
+ attributes.locations = []
14
+ for (const location of exc.locations) {
15
+ attributes.locations.push(`${location.line}:${location.column}`)
16
+ }
17
+ }
18
+
19
+ if (exc.path) {
20
+ attributes.path = exc.path.map(String)
21
+ }
22
+
23
+ if (exc.message) {
24
+ attributes.message = exc.message
25
+ }
26
+
27
+ if (config.graphqlErrorExtensions) {
28
+ for (const ext of config.graphqlErrorExtensions) {
29
+ if (exc.extensions?.[ext]) {
30
+ attributes[`extensions.${ext}`] = exc.extensions[ext].toString()
31
+ }
32
+ }
33
+ }
34
+
35
+ span.addEvent('dd.graphql.query.error', attributes, Date.now())
36
+ }
37
+
38
+ module.exports = {
39
+ extractErrorIntoSpanEvent
40
+ }
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4
+ const { extractErrorIntoSpanEvent } = require('./utils')
4
5
 
5
6
  class GraphQLValidatePlugin extends TracingPlugin {
6
7
  static get id () { return 'graphql' }
@@ -21,6 +22,11 @@ class GraphQLValidatePlugin extends TracingPlugin {
21
22
  finish ({ document, errors }) {
22
23
  const span = this.activeSpan
23
24
  this.config.hooks.validate(span, document, errors)
25
+ if (errors) {
26
+ for (const err of errors) {
27
+ extractErrorIntoSpanEvent(this._tracerConfig, span, err)
28
+ }
29
+ }
24
30
  super.finish()
25
31
  }
26
32
  }
@@ -115,7 +115,10 @@ function block (req, res, rootSpan, abortController, actionParameters = defaultB
115
115
  res.removeHeader(headerName)
116
116
  }
117
117
 
118
- res.writeHead(statusCode, headers).end(body)
118
+ res.writeHead(statusCode, headers)
119
+
120
+ // this is needed to call the original end method, since express-session replaces it
121
+ res.constructor.prototype.end.call(res, body)
119
122
 
120
123
  responseBlockedSet.add(res)
121
124
 
@@ -14,6 +14,7 @@ module.exports = {
14
14
  incomingHttpRequestStart: dc.channel('dd-trace:incomingHttpRequestStart'),
15
15
  incomingHttpRequestEnd: dc.channel('dd-trace:incomingHttpRequestEnd'),
16
16
  passportVerify: dc.channel('datadog:passport:verify:finish'),
17
+ passportUser: dc.channel('datadog:passport:deserializeUser:finish'),
17
18
  queryParser: dc.channel('datadog:query:read:finish'),
18
19
  setCookieChannel: dc.channel('datadog:iast:set-cookie'),
19
20
  nextBodyParsed: dc.channel('apm:next:body-parsed'),
@@ -2,14 +2,32 @@
2
2
 
3
3
  const InjectionAnalyzer = require('./injection-analyzer')
4
4
  const { CODE_INJECTION } = require('../vulnerabilities')
5
+ const { INSTRUMENTED_SINK } = require('../telemetry/iast-metric')
6
+ const { storage } = require('../../../../../datadog-core')
7
+ const { getIastContext } = require('../iast-context')
5
8
 
6
9
  class CodeInjectionAnalyzer extends InjectionAnalyzer {
7
10
  constructor () {
8
11
  super(CODE_INJECTION)
12
+ this.evalInstrumentedInc = false
9
13
  }
10
14
 
11
15
  onConfigure () {
12
- this.addSub('datadog:eval:call', ({ script }) => this.analyze(script))
16
+ this.addSub('datadog:eval:call', ({ script }) => {
17
+ if (!this.evalInstrumentedInc) {
18
+ const store = storage.getStore()
19
+ const iastContext = getIastContext(store)
20
+ const tags = INSTRUMENTED_SINK.formatTags(CODE_INJECTION)
21
+
22
+ for (const tag of tags) {
23
+ INSTRUMENTED_SINK.inc(iastContext, tag)
24
+ }
25
+
26
+ this.evalInstrumentedInc = true
27
+ }
28
+
29
+ this.analyze(script)
30
+ })
13
31
  this.addSub('datadog:vm:run-script:start', ({ code }) => this.analyze(code))
14
32
  this.addSub('datadog:vm:source-text-module:start', ({ code }) => this.analyze(code))
15
33
  }
@@ -22,7 +22,8 @@ const EXCLUDED_LOCATIONS = getNodeModulesPaths(
22
22
  'sqreen/lib/package-reader/index.js',
23
23
  'ws/lib/websocket-server.js',
24
24
  'google-gax/build/src/grpc.js',
25
- 'cookie-signature/index.js'
25
+ 'cookie-signature/index.js',
26
+ 'express-session/index.js'
26
27
  )
27
28
 
28
29
  const EXCLUDED_PATHS_FROM_STACK = [
@@ -10,6 +10,7 @@ const {
10
10
  incomingHttpRequestStart,
11
11
  incomingHttpRequestEnd,
12
12
  passportVerify,
13
+ passportUser,
13
14
  queryParser,
14
15
  nextBodyParsed,
15
16
  nextQueryParsed,
@@ -67,6 +68,7 @@ function enable (_config) {
67
68
  incomingHttpRequestStart.subscribe(incomingHttpStartTranslator)
68
69
  incomingHttpRequestEnd.subscribe(incomingHttpEndTranslator)
69
70
  passportVerify.subscribe(onPassportVerify) // possible optimization: only subscribe if collection mode is enabled
71
+ passportUser.subscribe(onPassportDeserializeUser)
70
72
  queryParser.subscribe(onRequestQueryParsed)
71
73
  nextBodyParsed.subscribe(onRequestBodyParsed)
72
74
  nextQueryParsed.subscribe(onRequestQueryParsed)
@@ -197,6 +199,20 @@ function onPassportVerify ({ framework, login, user, success, abortController })
197
199
  handleResults(results, store.req, store.req.res, rootSpan, abortController)
198
200
  }
199
201
 
202
+ function onPassportDeserializeUser ({ user, abortController }) {
203
+ const store = storage.getStore()
204
+ const rootSpan = store?.req && web.root(store.req)
205
+
206
+ if (!rootSpan) {
207
+ log.warn('[ASM] No rootSpan found in onPassportDeserializeUser')
208
+ return
209
+ }
210
+
211
+ const results = UserTracking.trackUser(user, rootSpan)
212
+
213
+ handleResults(results, store.req, store.req.res, rootSpan, abortController)
214
+ }
215
+
200
216
  function onRequestQueryParsed ({ req, res, query, abortController }) {
201
217
  if (!query || typeof query !== 'object') return
202
218
 
@@ -310,6 +326,7 @@ function disable () {
310
326
  if (incomingHttpRequestStart.hasSubscribers) incomingHttpRequestStart.unsubscribe(incomingHttpStartTranslator)
311
327
  if (incomingHttpRequestEnd.hasSubscribers) incomingHttpRequestEnd.unsubscribe(incomingHttpEndTranslator)
312
328
  if (passportVerify.hasSubscribers) passportVerify.unsubscribe(onPassportVerify)
329
+ if (passportUser.hasSubscribers) passportUser.unsubscribe(onPassportDeserializeUser)
313
330
  if (queryParser.hasSubscribers) queryParser.unsubscribe(onRequestQueryParsed)
314
331
  if (nextBodyParsed.hasSubscribers) nextBodyParsed.unsubscribe(onRequestBodyParsed)
315
332
  if (nextQueryParsed.hasSubscribers) nextQueryParsed.unsubscribe(onRequestQueryParsed)
@@ -2,6 +2,8 @@
2
2
 
3
3
  const { getRootSpan } = require('./utils')
4
4
  const log = require('../../log')
5
+ const waf = require('../waf')
6
+ const addresses = require('../addresses')
5
7
 
6
8
  function setUserTags (user, rootSpan) {
7
9
  for (const k of Object.keys(user)) {
@@ -22,6 +24,13 @@ function setUser (tracer, user) {
22
24
  }
23
25
 
24
26
  setUserTags(user, rootSpan)
27
+ rootSpan.setTag('_dd.appsec.user.collection_mode', 'sdk')
28
+
29
+ waf.run({
30
+ persistent: {
31
+ [addresses.USER_ID]: '' + user.id
32
+ }
33
+ })
25
34
  }
26
35
 
27
36
  module.exports = {
@@ -23,6 +23,7 @@ function checkUserAndSetUser (tracer, user) {
23
23
  if (rootSpan) {
24
24
  if (!rootSpan.context()._tags['usr.id']) {
25
25
  setUserTags(user, rootSpan)
26
+ rootSpan.setTag('_dd.appsec.user.collection_mode', 'sdk')
26
27
  }
27
28
  } else {
28
29
  log.warn('[ASM] Root span not available in isUserBlocked')
@@ -186,6 +186,15 @@ function incrementMissingUserLoginMetric (framework, eventType) {
186
186
  }).inc()
187
187
  }
188
188
 
189
+ function incrementMissingUserIdMetric (framework, eventType) {
190
+ if (!enabled) return
191
+
192
+ appsecMetrics.count('instrum.user_auth.missing_user_id', {
193
+ framework,
194
+ event_type: eventType
195
+ }).inc()
196
+ }
197
+
189
198
  function getRequestMetrics (req) {
190
199
  if (req) {
191
200
  const store = getStore(req)
@@ -203,6 +212,7 @@ module.exports = {
203
212
  incrementWafUpdatesMetric,
204
213
  incrementWafRequestsMetric,
205
214
  incrementMissingUserLoginMetric,
215
+ incrementMissingUserIdMetric,
206
216
 
207
217
  getRequestMetrics
208
218
  }
@@ -53,6 +53,8 @@ function obfuscateIfNeeded (str) {
53
53
  function getUserId (user) {
54
54
  if (!user) return
55
55
 
56
+ // should we iterate on user keys instead to be case insensitive ?
57
+ // but if we iterate over user then we're missing the inherited props ?
56
58
  for (const field of USER_ID_FIELDS) {
57
59
  let id = user[field]
58
60
 
@@ -73,11 +75,6 @@ function getUserId (user) {
73
75
  function trackLogin (framework, login, user, success, rootSpan) {
74
76
  if (!collectionMode || collectionMode === 'disabled') return
75
77
 
76
- if (!rootSpan) {
77
- log.error('[ASM] No rootSpan found in AppSec trackLogin')
78
- return
79
- }
80
-
81
78
  if (typeof login !== 'string') {
82
79
  log.error('[ASM] Invalid login provided to AppSec trackLogin')
83
80
 
@@ -162,7 +159,36 @@ function trackLogin (framework, login, user, success, rootSpan) {
162
159
  return waf.run({ persistent })
163
160
  }
164
161
 
162
+ function trackUser (user, rootSpan) {
163
+ if (!collectionMode || collectionMode === 'disabled') return
164
+
165
+ const userId = getUserId(user)
166
+ if (!userId) {
167
+ log.error('[ASM] No valid user ID found in AppSec trackUser')
168
+ telemetry.incrementMissingUserIdMetric('passport', 'authenticated_request')
169
+ return
170
+ }
171
+
172
+ rootSpan.setTag('_dd.appsec.usr.id', userId)
173
+
174
+ const isSdkCalled = rootSpan.context()._tags['_dd.appsec.user.collection_mode'] === 'sdk'
175
+ // do not override SDK
176
+ if (!isSdkCalled) {
177
+ rootSpan.addTags({
178
+ 'usr.id': userId,
179
+ '_dd.appsec.user.collection_mode': collectionMode
180
+ })
181
+
182
+ return waf.run({
183
+ persistent: {
184
+ [addresses.USER_ID]: userId
185
+ }
186
+ })
187
+ }
188
+ }
189
+
165
190
  module.exports = {
166
191
  setCollectionMode,
167
- trackLogin
192
+ trackLogin,
193
+ trackUser
168
194
  }
@@ -482,6 +482,7 @@ class Config {
482
482
  this._setValue(defaults, 'flushInterval', 2000)
483
483
  this._setValue(defaults, 'flushMinSpans', 1000)
484
484
  this._setValue(defaults, 'gitMetadataEnabled', true)
485
+ this._setValue(defaults, 'graphqlErrorExtensions', [])
485
486
  this._setValue(defaults, 'grpc.client.error.statuses', GRPC_CLIENT_ERROR_STATUSES)
486
487
  this._setValue(defaults, 'grpc.server.error.statuses', GRPC_SERVER_ERROR_STATUSES)
487
488
  this._setValue(defaults, 'headerTags', [])
@@ -521,6 +522,7 @@ class Config {
521
522
  this._setValue(defaults, 'lookup', undefined)
522
523
  this._setValue(defaults, 'inferredProxyServicesEnabled', false)
523
524
  this._setValue(defaults, 'memcachedCommandEnabled', false)
525
+ this._setValue(defaults, 'middlewareTracingEnabled', true)
524
526
  this._setValue(defaults, 'openAiLogsEnabled', false)
525
527
  this._setValue(defaults, 'openai.spanCharLimit', 128)
526
528
  this._setValue(defaults, 'peerServiceMapping', {})
@@ -669,9 +671,11 @@ class Config {
669
671
  DD_TRACE_EXPERIMENTAL_RUNTIME_ID_ENABLED,
670
672
  DD_TRACE_GIT_METADATA_ENABLED,
671
673
  DD_TRACE_GLOBAL_TAGS,
674
+ DD_TRACE_GRAPHQL_ERROR_EXTENSIONS,
672
675
  DD_TRACE_HEADER_TAGS,
673
676
  DD_TRACE_LEGACY_BAGGAGE_ENABLED,
674
677
  DD_TRACE_MEMCACHED_COMMAND_ENABLED,
678
+ DD_TRACE_MIDDLEWARE_TRACING_ENABLED,
675
679
  DD_TRACE_OBFUSCATION_QUERY_STRING_REGEXP,
676
680
  DD_TRACE_PARTIAL_FLUSH_MIN_SPANS,
677
681
  DD_TRACE_PEER_SERVICE_MAPPING,
@@ -804,6 +808,7 @@ class Config {
804
808
  this._setBoolean(env, 'logInjection', DD_LOGS_INJECTION)
805
809
  // Requires an accompanying DD_APM_OBFUSCATION_MEMCACHED_KEEP_COMMAND=true in the agent
806
810
  this._setBoolean(env, 'memcachedCommandEnabled', DD_TRACE_MEMCACHED_COMMAND_ENABLED)
811
+ this._setBoolean(env, 'middlewareTracingEnabled', DD_TRACE_MIDDLEWARE_TRACING_ENABLED)
807
812
  this._setBoolean(env, 'openAiLogsEnabled', DD_OPENAI_LOGS_ENABLED)
808
813
  this._setValue(env, 'openai.spanCharLimit', maybeInt(DD_OPENAI_SPAN_CHAR_LIMIT))
809
814
  this._envUnprocessed.openaiSpanCharLimit = DD_OPENAI_SPAN_CHAR_LIMIT
@@ -895,6 +900,7 @@ class Config {
895
900
  this._setString(env, 'version', DD_VERSION || tags.version)
896
901
  this._setBoolean(env, 'inferredProxyServicesEnabled', DD_TRACE_INFERRED_PROXY_SERVICES_ENABLED)
897
902
  this._setString(env, 'aws.dynamoDb.tablePrimaryKeys', DD_AWS_SDK_DYNAMODB_TABLE_PRIMARY_KEYS)
903
+ this._setArray(env, 'graphqlErrorExtensions', DD_TRACE_GRAPHQL_ERROR_EXTENSIONS)
898
904
  }
899
905
 
900
906
  _applyOptions (options) {
@@ -986,6 +992,7 @@ class Config {
986
992
  this._setString(opts, 'llmobs.mlApp', options.llmobs?.mlApp)
987
993
  this._setBoolean(opts, 'logInjection', options.logInjection)
988
994
  this._setString(opts, 'lookup', options.lookup)
995
+ this._setBoolean(opts, 'middlewareTracingEnabled', options.middlewareTracingEnabled)
989
996
  this._setBoolean(opts, 'openAiLogsEnabled', options.openAiLogsEnabled)
990
997
  this._setValue(opts, 'peerServiceMapping', options.peerServiceMapping)
991
998
  this._setBoolean(opts, 'plugins', options.plugins)
@@ -1020,6 +1027,7 @@ class Config {
1020
1027
  this._setBoolean(opts, 'traceId128BitLoggingEnabled', options.traceId128BitLoggingEnabled)
1021
1028
  this._setString(opts, 'version', options.version || tags.version)
1022
1029
  this._setBoolean(opts, 'inferredProxyServicesEnabled', options.inferredProxyServicesEnabled)
1030
+ this._setBoolean(opts, 'graphqlErrorExtensions', options.graphqlErrorExtensions)
1023
1031
 
1024
1032
  // For LLMObs, we want the environment variable to take precedence over the options.
1025
1033
  // This is reliant on environment config being set before options.
@@ -2,6 +2,8 @@
2
2
 
3
3
  const session = require('./session')
4
4
 
5
+ const WINDOWS_DRIVE_LETTER_REGEX = /[a-zA-Z]/
6
+
5
7
  const scriptIds = []
6
8
  const scriptUrls = new Map()
7
9
 
@@ -10,26 +12,79 @@ module.exports = {
10
12
  breakpoints: new Map(),
11
13
 
12
14
  /**
13
- * Find the matching script that can be inspected based on a partial path.
14
- *
15
- * Algorithm: Find the sortest url that ends in the requested path.
16
- *
17
- * Will identify the correct script as long as Node.js doesn't load a module from a `node_modules` folder outside the
18
- * project root. If so, there's a risk that this path is shorter than the expected path inside the project root.
19
- * Example of mismatch where path = `index.js`:
20
- *
21
- * Expected match: /www/code/my-projects/demo-project1/index.js
22
- * Actual shorter match: /www/node_modules/dd-trace/index.js
23
- *
24
- * To fix this, specify a more unique file path, e.g `demo-project1/index.js` instead of `index.js`
15
+ * Find the script to inspect based on a partial or absolute path. Handles both Windows and POSIX paths.
25
16
  *
26
- * @param {string} path
27
- * @returns {[string, string] | undefined}
17
+ * @param {string} path - Partial or absolute path to match against loaded scripts
18
+ * @returns {[string, string, string | undefined] | null} - Array containing [url, scriptId, sourceMapURL]
19
+ * or null if no match
28
20
  */
29
21
  findScriptFromPartialPath (path) {
30
- return scriptIds
31
- .filter(([url]) => url.endsWith(path))
32
- .sort(([a], [b]) => a.length - b.length)[0]
22
+ if (!path) return null // This shouldn't happen, but better safe than sorry
23
+
24
+ path = path.toLowerCase()
25
+
26
+ const bestMatch = new Array(3)
27
+ let maxMatchLength = -1
28
+
29
+ for (const [url, scriptId, sourceMapURL] of scriptIds) {
30
+ let i = url.length - 1
31
+ let j = path.length - 1
32
+ let matchLength = 0
33
+ let lastBoundaryPos = -1
34
+ let atBoundary = false
35
+
36
+ // Compare characters from the end
37
+ while (i >= 0 && j >= 0) {
38
+ const urlChar = url[i].toLowerCase()
39
+ const pathChar = path[j]
40
+
41
+ // Check if both characters is a path boundary
42
+ const isBoundary = (urlChar === '/' || urlChar === '\\') && (pathChar === '/' || pathChar === '\\' ||
43
+ (j === 1 && pathChar === ':' && WINDOWS_DRIVE_LETTER_REGEX.test(path[0])))
44
+
45
+ // If both are boundaries, or if characters match exactly
46
+ if (isBoundary || urlChar === pathChar) {
47
+ if (isBoundary) {
48
+ atBoundary = true
49
+ lastBoundaryPos = matchLength
50
+ } else {
51
+ atBoundary = false
52
+ }
53
+ matchLength++
54
+ i--
55
+ j--
56
+ } else {
57
+ break
58
+ }
59
+ }
60
+
61
+ // If we've matched the entire path pattern, ensure it starts at a path boundary
62
+ if (j === -1) {
63
+ if (i >= 0) {
64
+ // If there are more characters in the URL, the next one must be a slash
65
+ if (url[i] === '/' || url[i] === '\\') {
66
+ atBoundary = true
67
+ lastBoundaryPos = matchLength
68
+ }
69
+ } else {
70
+ atBoundary = true
71
+ lastBoundaryPos = matchLength
72
+ }
73
+ }
74
+
75
+ // If we found a valid match and it's better than our previous best
76
+ if (atBoundary && (
77
+ lastBoundaryPos > maxMatchLength ||
78
+ (lastBoundaryPos === maxMatchLength && url.length < bestMatch[0].length) // Prefer shorter paths
79
+ )) {
80
+ maxMatchLength = lastBoundaryPos
81
+ bestMatch[0] = url
82
+ bestMatch[1] = scriptId
83
+ bestMatch[2] = sourceMapURL
84
+ }
85
+ }
86
+
87
+ return maxMatchLength > -1 ? bestMatch : null
33
88
  },
34
89
 
35
90
  getStackFromCallFrames (callFrames) {
@@ -15,7 +15,6 @@ let batch = 0
15
15
  // Internal representation of a trace or span ID.
16
16
  class Identifier {
17
17
  constructor (value, radix = 16) {
18
- this._isUint64BE = true // msgpack-lite compatibility
19
18
  this._buffer = radix === 16
20
19
  ? createBuffer(value)
21
20
  : fromString(value, radix)
@@ -31,7 +30,6 @@ class Identifier {
31
30
  return this._buffer
32
31
  }
33
32
 
34
- // msgpack-lite compatibility
35
33
  toArray () {
36
34
  if (this._buffer.length === 8) {
37
35
  return this._buffer
@@ -8,7 +8,9 @@ const {
8
8
  parseModelId
9
9
  } = require('../../../../datadog-plugin-aws-sdk/src/services/bedrockruntime/utils')
10
10
 
11
- const enabledOperations = ['invokeModel']
11
+ const ENABLED_OPERATIONS = ['invokeModel']
12
+
13
+ const requestIdsToTokens = {}
12
14
 
13
15
  class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
14
16
  constructor () {
@@ -18,7 +20,7 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
18
20
  const request = response.request
19
21
  const operation = request.operation
20
22
  // avoids instrumenting other non supported runtime operations
21
- if (!enabledOperations.includes(operation)) {
23
+ if (!ENABLED_OPERATIONS.includes(operation)) {
22
24
  return
23
25
  }
24
26
  const { modelProvider, modelName } = parseModelId(request.params.modelId)
@@ -30,6 +32,17 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
30
32
  const span = storage.getStore()?.span
31
33
  this.setLLMObsTags({ request, span, response, modelProvider, modelName })
32
34
  })
35
+
36
+ this.addSub('apm:aws:response:deserialize:bedrockruntime', ({ headers }) => {
37
+ const requestId = headers['x-amzn-requestid']
38
+ const inputTokenCount = headers['x-amzn-bedrock-input-token-count']
39
+ const outputTokenCount = headers['x-amzn-bedrock-output-token-count']
40
+
41
+ requestIdsToTokens[requestId] = {
42
+ inputTokensFromHeaders: inputTokenCount && parseInt(inputTokenCount),
43
+ outputTokensFromHeaders: outputTokenCount && parseInt(outputTokenCount)
44
+ }
45
+ })
33
46
  }
34
47
 
35
48
  setLLMObsTags ({ request, span, response, modelProvider, modelName }) {
@@ -52,7 +65,39 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
52
65
  })
53
66
 
54
67
  // add I/O tags
55
- this._tagger.tagLLMIO(span, requestParams.prompt, textAndResponseReason.message)
68
+ this._tagger.tagLLMIO(
69
+ span,
70
+ requestParams.prompt,
71
+ [{ content: textAndResponseReason.message, role: textAndResponseReason.role }]
72
+ )
73
+
74
+ // add token metrics
75
+ const { inputTokens, outputTokens, totalTokens } = extractTokens({
76
+ requestId: response.$metadata.requestId,
77
+ usage: textAndResponseReason.usage
78
+ })
79
+ this._tagger.tagMetrics(span, {
80
+ inputTokens,
81
+ outputTokens,
82
+ totalTokens
83
+ })
84
+ }
85
+ }
86
+
87
+ function extractTokens ({ requestId, usage }) {
88
+ const {
89
+ inputTokensFromHeaders,
90
+ outputTokensFromHeaders
91
+ } = requestIdsToTokens[requestId] || {}
92
+ delete requestIdsToTokens[requestId]
93
+
94
+ const inputTokens = usage.inputTokens || inputTokensFromHeaders || 0
95
+ const outputTokens = usage.outputTokens || outputTokensFromHeaders || 0
96
+
97
+ return {
98
+ inputTokens,
99
+ outputTokens,
100
+ totalTokens: inputTokens + outputTokens
56
101
  }
57
102
  }
58
103
 
@@ -31,6 +31,9 @@ loadChannel.subscribe(({ name }) => {
31
31
  // Globals
32
32
  maybeEnable(require('../../datadog-plugin-fetch/src'))
33
33
 
34
+ // Always enabled
35
+ maybeEnable(require('../../datadog-plugin-dd-trace-api/src'))
36
+
34
37
  function maybeEnable (Plugin) {
35
38
  if (!Plugin || typeof Plugin !== 'function') return
36
39
  if (!pluginClasses[Plugin.id]) {
@@ -139,7 +142,8 @@ module.exports = class PluginManager {
139
142
  memcachedCommandEnabled,
140
143
  ciVisibilityTestSessionName,
141
144
  ciVisAgentlessLogSubmissionEnabled,
142
- isTestDynamicInstrumentationEnabled
145
+ isTestDynamicInstrumentationEnabled,
146
+ middlewareTracingEnabled
143
147
  } = this._tracerConfig
144
148
 
145
149
  const sharedConfig = {
@@ -170,6 +174,13 @@ module.exports = class PluginManager {
170
174
  sharedConfig.clientIpEnabled = clientIpEnabled
171
175
  }
172
176
 
177
+ // For the global setting, we use the name `middlewareTracingEnabled`, but
178
+ // for the plugin-specific setting, we use `middleware`. They mean the same
179
+ // to an individual plugin, so we normalize them here.
180
+ if (middlewareTracingEnabled !== undefined) {
181
+ sharedConfig.middleware = middlewareTracingEnabled
182
+ }
183
+
173
184
  return sharedConfig
174
185
  }
175
186
  }
@@ -34,6 +34,7 @@ module.exports = {
34
34
  get couchbase () { return require('../../../datadog-plugin-couchbase/src') },
35
35
  get cypress () { return require('../../../datadog-plugin-cypress/src') },
36
36
  get dns () { return require('../../../datadog-plugin-dns/src') },
37
+ get 'dd-trace-api' () { return require('../../../datadog-plugin-dd-trace-api/src') },
37
38
  get elasticsearch () { return require('../../../datadog-plugin-elasticsearch/src') },
38
39
  get express () { return require('../../../datadog-plugin-express/src') },
39
40
  get fastify () { return require('../../../datadog-plugin-fastify/src') },