dd-trace 5.48.1 → 5.49.1

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.
@@ -20,6 +20,7 @@ require,limiter,MIT,Copyright 2011 John Hurliman
20
20
  require,lodash.sortby,MIT,Copyright JS Foundation and other contributors
21
21
  require,lru-cache,ISC,Copyright (c) 2010-2022 Isaac Z. Schlueter and Contributors
22
22
  require,module-details-from-path,MIT,Copyright 2016 Thomas Watson Steen
23
+ require,mutexify,MIT,Copyright (c) 2014 Mathias Buus
23
24
  require,opentracing,MIT,Copyright 2016 Resonance Labs Inc
24
25
  require,path-to-regexp,MIT,Copyright 2014 Blake Embrey
25
26
  require,pprof-format,MIT,Copyright 2022 Stephen Belanger
@@ -31,7 +32,6 @@ require,semifies,Apache license 2.0,Copyright Authors
31
32
  require,shell-quote,mit,Copyright (c) 2013 James Halliday
32
33
  require,source-map,BSD-3-Clause,Copyright (c) 2009-2011, Mozilla Foundation and contributors
33
34
  require,ttl-set,MIT,Copyright (c) 2024 Thomas Watson
34
- dev,@apollo/server,MIT,Copyright (c) 2016-2020 Apollo Graph, Inc. (Formerly Meteor Development Group, Inc.)
35
35
  dev,@babel/helpers,MIT,Copyright (c) 2014-present Sebastian McKenzie and other contributors
36
36
  dev,@types/node,MIT,Copyright Authors
37
37
  dev,@eslint/eslintrc,MIT,Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
@@ -39,7 +39,6 @@ dev,@eslint/js,MIT,Copyright OpenJS Foundation and other contributors, <www.open
39
39
  dev,@msgpack/msgpack,ISC,Copyright 2019 The MessagePack Community
40
40
  dev,@stylistic/eslint-plugin-js,MIT,Copyright OpenJS Foundation and other contributors, <www.openjsf.org>
41
41
  dev,autocannon,MIT,Copyright 2016 Matteo Collina
42
- dev,aws-sdk,Apache 2.0,Copyright 2012-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
43
42
  dev,axios,MIT,Copyright 2014-present Matt Zabriskie
44
43
  dev,benchmark,MIT,Copyright 2010-2016 Mathias Bynens Robert Kieffer John-David Dalton
45
44
  dev,body-parser,MIT,Copyright 2014 Jonathan Ong 2014-2015 Douglas Christopher Wilson
@@ -48,7 +47,6 @@ dev,chalk,MIT,Copyright Sindre Sorhus
48
47
  dev,checksum,MIT,Copyright Daniel D. Shaw
49
48
  dev,cli-table3,MIT,Copyright 2014 James Talmage
50
49
  dev,dotenv,BSD-2-Clause,Copyright 2015 Scott Motte
51
- dev,esbuild,MIT,Copyright (c) 2020 Evan Wallace
52
50
  dev,eslint,MIT,Copyright JS Foundation and other contributors https://js.foundation
53
51
  dev,eslint-config-standard,MIT,Copyright Feross Aboukhadijeh
54
52
  dev,eslint-plugin-import,MIT,Copyright 2015 Ben Mosher
@@ -62,7 +60,6 @@ dev,glob,ISC,Copyright Isaac Z. Schlueter and Contributors
62
60
  dev,globals,MIT,Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
63
61
  dev,graphql,MIT,Copyright 2015 Facebook Inc.
64
62
  dev,jszip,MIT,Copyright 2015-2016 Stuart Knightley and contributors
65
- dev,knex,MIT,Copyright (c) 2013-present Tim Griesser
66
63
  dev,mkdirp,MIT,Copyright 2010 James Halliday
67
64
  dev,mocha,MIT,Copyright 2011-2018 JS Foundation and contributors https://js.foundation
68
65
  dev,multer,MIT,Copyright 2014 Hage Yaapa
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
  Most of the documentation for `dd-trace` is available on these webpages:
16
16
 
17
17
  - [Tracing Node.js Applications](https://docs.datadoghq.com/tracing/languages/nodejs/) - most project documentation, including setup instructions
18
- - [Configuring the NodeJS Tracing Library](https://docs.datadoghq.com/tracing/trace_collection/library_config/nodejs) - environment variables and config options
18
+ - [Configuring the Node.js Tracing Library](https://docs.datadoghq.com/tracing/trace_collection/library_config/nodejs) - environment variables and config options
19
19
  - [API Documentation](https://datadog.github.io/dd-trace-js) - method signatures, plugin list, and some usage examples
20
20
  - [APM Terms and Concepts](https://docs.datadoghq.com/tracing/visualization/) - a glossary of concepts applicable across all languages
21
21
 
@@ -59,7 +59,7 @@ When a new release line is introduced the previous release line then enters main
59
59
  Once that year is up the release line enters End of Life and will not receive new updates.
60
60
  The library also follows the Node.js LTS lifecycle wherein new release lines drop compatibility with Node.js versions that reach end-of-life (with the maintenance release line still receiving updates for a year).
61
61
 
62
- For more information about library versioning and compatibility, see the [NodeJS Compatibility Requirements](https://docs.datadoghq.com/tracing/trace_collection/compatibility/nodejs/#releases) page.
62
+ For more information about library versioning and compatibility, see the [Node.js Compatibility Requirements](https://docs.datadoghq.com/tracing/trace_collection/compatibility/nodejs/#releases) page.
63
63
 
64
64
  Changes associated with each individual release are documented on the [GitHub Releases](https://github.com/DataDog/dd-trace-js/releases) screen.
65
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.48.1",
3
+ "version": "5.49.1",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -48,6 +48,7 @@
48
48
  "test:integration:cucumber": "mocha --timeout 60000 -r \"packages/dd-trace/test/setup/core.js\" \"integration-tests/cucumber/*.spec.js\"",
49
49
  "test:integration:cypress": "mocha --timeout 60000 -r \"packages/dd-trace/test/setup/core.js\" \"integration-tests/cypress/*.spec.js\"",
50
50
  "test:integration:debugger": "mocha --timeout 60000 -r \"packages/dd-trace/test/setup/core.js\" \"integration-tests/debugger/*.spec.js\"",
51
+ "test:integration:esbuild": "mocha --timeout 60000 -r \"packages/dd-trace/test/setup/core.js\" \"integration-tests/esbuild/*.spec.js\"",
51
52
  "test:integration:jest": "mocha --timeout 60000 -r \"packages/dd-trace/test/setup/core.js\" \"integration-tests/jest/*.spec.js\"",
52
53
  "test:integration:mocha": "mocha --timeout 60000 -r \"packages/dd-trace/test/setup/core.js\" \"integration-tests/mocha/*.spec.js\"",
53
54
  "test:integration:playwright": "mocha --timeout 60000 -r \"packages/dd-trace/test/setup/core.js\" \"integration-tests/playwright/*.spec.js\"",
@@ -105,6 +106,7 @@
105
106
  "lodash.sortby": "^4.7.0",
106
107
  "lru-cache": "^7.14.0",
107
108
  "module-details-from-path": "^1.0.3",
109
+ "mutexify": "^1.4.0",
108
110
  "opentracing": ">=0.12.1",
109
111
  "path-to-regexp": "^0.1.12",
110
112
  "pprof-format": "^2.1.0",
@@ -118,7 +120,6 @@
118
120
  "ttl-set": "^1.0.0"
119
121
  },
120
122
  "devDependencies": {
121
- "@apollo/server": "^4.11.0",
122
123
  "@babel/helpers": "^7.26.10",
123
124
  "@eslint/eslintrc": "^3.2.0",
124
125
  "@eslint/js": "^9.19.0",
@@ -126,7 +127,6 @@
126
127
  "@stylistic/eslint-plugin-js": "^3.0.1",
127
128
  "@types/node": "^16.0.0",
128
129
  "autocannon": "^4.5.2",
129
- "aws-sdk": "^2.1446.0",
130
130
  "axios": "^1.8.2",
131
131
  "benchmark": "^2.1.4",
132
132
  "body-parser": "^1.20.3",
@@ -135,7 +135,6 @@
135
135
  "checksum": "^1.0.0",
136
136
  "cli-table3": "^0.6.3",
137
137
  "dotenv": "16.3.1",
138
- "esbuild": "^0.25.0",
139
138
  "eslint": "^9.19.0",
140
139
  "eslint-config-standard": "^17.1.0",
141
140
  "eslint-plugin-import": "^2.31.0",
@@ -144,12 +143,11 @@
144
143
  "eslint-plugin-promise": "^7.2.1",
145
144
  "eslint-plugin-unicorn": "^57.0.0",
146
145
  "express": "^4.21.2",
147
- "get-port": "^3.2.0",
146
+ "get-port": "^5.1.1",
148
147
  "glob": "^7.1.6",
149
148
  "globals": "^15.10.0",
150
149
  "graphql": "0.13.2",
151
150
  "jszip": "^3.5.0",
152
- "knex": "^2.4.2",
153
151
  "mkdirp": "^3.0.1",
154
152
  "mocha": "^10",
155
153
  "multer": "^1.4.5-lts.1",
@@ -47,8 +47,7 @@ const DEBUG = !!process.env.DD_TRACE_DEBUG
47
47
  // Those packages will still be handled via RITM
48
48
  // Attempting to instrument them would fail as they have no package.json file
49
49
  for (const pkg of INSTRUMENTED) {
50
- if (builtins.has(pkg)) continue
51
- if (pkg.startsWith('node:')) continue
50
+ if (builtins.has(pkg) || pkg.startsWith('node:')) continue
52
51
  modulesOfInterest.add(pkg)
53
52
  }
54
53
 
@@ -71,7 +70,9 @@ module.exports.setup = function (build) {
71
70
  }
72
71
 
73
72
  // TODO: Should this also check for namespace === 'file'?
74
- if (args.path.startsWith('@') && !args.importer.includes('node_modules/')) {
73
+ if (!modulesOfInterest.has(args.path) &&
74
+ args.path.startsWith('@') &&
75
+ !args.importer.includes('node_modules/')) {
75
76
  // This is the Next.js convention for loading local files
76
77
  if (DEBUG) console.log(`@LOCAL: ${args.path}`)
77
78
  return
@@ -761,9 +761,17 @@ addHook({
761
761
  return rootSuite
762
762
  }
763
763
 
764
- loadUtilsPackage.createRootSuite = newCreateRootSuite
764
+ // We need to proxy the createRootSuite function because the function is not configurable
765
+ const proxy = new Proxy(loadUtilsPackage, {
766
+ get (target, prop) {
767
+ if (prop === 'createRootSuite') {
768
+ return newCreateRootSuite
769
+ }
770
+ return target[prop]
771
+ }
772
+ })
765
773
 
766
- return loadUtilsPackage
774
+ return proxy
767
775
  })
768
776
 
769
777
  // main process hook
@@ -805,19 +813,25 @@ addHook({
805
813
 
806
814
  const page = this
807
815
 
808
- const isRumActive = await page.evaluate(() => {
809
- if (window.DD_RUM && window.DD_RUM.getInternalContext) {
810
- return !!window.DD_RUM.getInternalContext()
811
- } else {
812
- return false
813
- }
814
- })
816
+ try {
817
+ if (page) {
818
+ const isRumActive = await page.evaluate(() => {
819
+ if (window.DD_RUM && window.DD_RUM.getInternalContext) {
820
+ return !!window.DD_RUM.getInternalContext()
821
+ } else {
822
+ return false
823
+ }
824
+ })
815
825
 
816
- if (isRumActive) {
817
- testPageGotoCh.publish({
818
- isRumActive,
819
- page
820
- })
826
+ if (isRumActive) {
827
+ testPageGotoCh.publish({
828
+ isRumActive,
829
+ page
830
+ })
831
+ }
832
+ }
833
+ } catch (e) {
834
+ // ignore errors such as redirects, context destroyed, etc
821
835
  }
822
836
 
823
837
  return response
@@ -164,7 +164,6 @@ class DynamoDb extends BaseAwsSdkPlugin {
164
164
  return
165
165
  }
166
166
  if (!primaryKeyConfig) {
167
- log.warn('Missing DD_TRACE_DYNAMODB_TABLE_PRIMARY_KEYS env variable')
168
167
  return
169
168
  }
170
169
  const primaryKeySet = primaryKeyConfig[tableName]
@@ -1,7 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const log = require('../../dd-trace/src/log')
4
-
5
3
  function copyProperties (original, wrapped) {
6
4
  // TODO getPrototypeOf is not fast. Should we instead do this in specific
7
5
  // instrumentations where needed?
@@ -24,9 +22,7 @@ function copyProperties (original, wrapped) {
24
22
  function wrapFunction (original, wrapper) {
25
23
  if (typeof original === 'function') assertNotClass(original)
26
24
 
27
- const wrapped = safeMode
28
- ? safeWrapper(original, wrapper)
29
- : wrapper(original)
25
+ const wrapped = wrapper(original)
30
26
 
31
27
  if (typeof original === 'function') copyProperties(original, wrapped)
32
28
 
@@ -37,36 +33,6 @@ const wrapFn = function (original, delegate) {
37
33
  throw new Error('calling `wrap()` with 2 args is deprecated. Use wrapFunction instead.')
38
34
  }
39
35
 
40
- // This is only used in safe mode. It's a simple state machine to track if the
41
- // original method was called and if it returned. We need this to determine if
42
- // an error was thrown by the original method, or by us. We'll use one of these
43
- // per call to a wrapped method.
44
- class CallState {
45
- constructor () {
46
- this.called = false
47
- this.completed = false
48
- this.retVal = undefined
49
- }
50
-
51
- startCall () {
52
- this.called = true
53
- }
54
-
55
- endCall (retVal) {
56
- this.completed = true
57
- this.retVal = retVal
58
- }
59
- }
60
-
61
- function isPromise (obj) {
62
- return obj && typeof obj === 'object' && typeof obj.then === 'function'
63
- }
64
-
65
- let safeMode = !!process.env.DD_INEJCTION_ENABLED
66
- function setSafe (value) {
67
- safeMode = value
68
- }
69
-
70
36
  function wrapMethod (target, name, wrapper, noAssert) {
71
37
  if (!noAssert) {
72
38
  assertMethod(target, name)
@@ -74,9 +40,7 @@ function wrapMethod (target, name, wrapper, noAssert) {
74
40
  }
75
41
 
76
42
  const original = target[name]
77
- const wrapped = safeMode && original
78
- ? safeWrapper(original, wrapper)
79
- : wrapper(original)
43
+ const wrapped = wrapper(original)
80
44
 
81
45
  const descriptor = Object.getOwnPropertyDescriptor(target, name)
82
46
 
@@ -110,94 +74,6 @@ function wrapMethod (target, name, wrapper, noAssert) {
110
74
  return target
111
75
  }
112
76
 
113
- function safeWrapper (original, wrapper) {
114
- // In this mode, we make a best-effort attempt to handle errors that are thrown
115
- // by us, rather than wrapped code. With such errors, we log them, and then attempt
116
- // to return the result as if no wrapping was done at all.
117
- //
118
- // Caveats:
119
- // * If the original function is called in a later iteration of the event loop,
120
- // and we throw _then_, then it won't be caught by this. In practice, we always call
121
- // the original function synchronously, so this is not a problem.
122
- // * While async errors are dealt with here, errors in callbacks are not. This
123
- // is because we don't necessarily know _for sure_ that any function arguments
124
- // are wrapped by us. We could wrap them all anyway and just make that assumption,
125
- // or just assume that the last argument is always a callback set by us if it's a
126
- // function, but those don't seem like things we can rely on. We could add a
127
- // `shimmer.markCallbackAsWrapped()` function that's a no-op outside safe-mode,
128
- // but that means modifying every instrumentation. Even then, the complexity of
129
- // this code increases because then we'd need to effectively do the reverse of
130
- // what we're doing for synchronous functions. This is a TODO.
131
-
132
- // We're going to hold on to current callState in this variable in this scope,
133
- // which is fine because any time we reference it, we're referencing it synchronously.
134
- // We'll use it in the our wrapper (which, again, is called syncrhonously), and in the
135
- // errorHandler, which will already have been bound to this callState.
136
- let currentCallState
137
-
138
- // Rather than calling the original function directly from the shim wrapper, we wrap
139
- // it again so that we can track if it was called and if it returned. This is because
140
- // we need to know if an error was thrown by the original function, or by us.
141
- // We could do this inside the `wrapper` function defined below, which would simplify
142
- // managing the callState, but then we'd be calling `wrapper` on each invocation, so
143
- // instead we do it here, once.
144
- const innerWrapped = wrapper(function (...args) {
145
- // We need to stash the callState here because of recursion.
146
- const callState = currentCallState
147
- callState.startCall()
148
- const retVal = original.apply(this, args)
149
- if (isPromise(retVal)) {
150
- retVal.then(callState.endCall.bind(callState))
151
- } else {
152
- callState.endCall(retVal)
153
- }
154
- return retVal
155
- })
156
-
157
- // This is the crux of what we're doing in safe mode. It handles errors
158
- // that _we_ cause, by logging them, and transparently providing results
159
- // as if no wrapping was done at all. That means detecting (via callState)
160
- // whether the function has already run or not, and if it has, returning
161
- // the result, and otherwise calling the original function unwrapped.
162
- const handleError = function (args, callState, e) {
163
- if (callState.completed) {
164
- // error was thrown after original function returned/resolved, so
165
- // it was us. log it.
166
- log.error('Shimmer error was thrown after original function returned/resolved', e)
167
- // original ran and returned something. return it.
168
- return callState.retVal
169
- }
170
-
171
- if (!callState.called) {
172
- // error was thrown before original function was called, so
173
- // it was us. log it.
174
- log.error('Shimmer error was thrown before original function was called', e)
175
- // original never ran. call it unwrapped.
176
- return original.apply(this, args)
177
- }
178
-
179
- // error was thrown during original function execution, so
180
- // it was them. throw.
181
- throw e
182
- }
183
-
184
- // The wrapped function is the one that will be called by the user.
185
- // It calls our version of the original function, which manages the
186
- // callState. That way when we use the errorHandler, it can tell where
187
- // the error originated.
188
- return function (...args) {
189
- currentCallState = new CallState()
190
- const errorHandler = handleError.bind(this, args, currentCallState)
191
-
192
- try {
193
- const retVal = innerWrapped.apply(this, args)
194
- return isPromise(retVal) ? retVal.catch(errorHandler) : retVal
195
- } catch (e) {
196
- return errorHandler(e)
197
- }
198
- }
199
- }
200
-
201
77
  function wrap (target, name, wrapper) {
202
78
  return typeof name === 'function'
203
79
  ? wrapFn(target, name)
@@ -256,6 +132,5 @@ function assertNotClass (target) {
256
132
  module.exports = {
257
133
  wrap,
258
134
  wrapFunction,
259
- massWrap,
260
- setSafe
135
+ massWrap
261
136
  }
@@ -13,7 +13,7 @@ module.exports = function extractSensitiveRanges (evidence) {
13
13
  let regexResult = pattern.exec(evidence.value)
14
14
  while (regexResult != null) {
15
15
  if (!regexResult.groups.LITERAL) continue
16
- // Computing indices manually since NodeJs 12 does not support d flag on regular expressions
16
+ // Computing indices manually since Node.js 12 does not support d flag on regular expressions
17
17
  // TODO Get indices from group by adding d flag in regular expression
18
18
  const start = regexResult.index + (regexResult[0].length - regexResult.groups.LITERAL.length - 1)
19
19
  const end = start + regexResult.groups.LITERAL.length
@@ -530,7 +530,7 @@ class Config {
530
530
  this._setValue(defaults, 'isManualApiEnabled', false)
531
531
  this._setValue(defaults, 'langchain.spanCharLimit', 128)
532
532
  this._setValue(defaults, 'langchain.spanPromptCompletionSampleRate', 1.0)
533
- this._setValue(defaults, 'llmobs.agentlessEnabled', false)
533
+ this._setValue(defaults, 'llmobs.agentlessEnabled', undefined)
534
534
  this._setValue(defaults, 'llmobs.enabled', false)
535
535
  this._setValue(defaults, 'llmobs.mlApp', undefined)
536
536
  this._setValue(defaults, 'ciVisibilityTestSessionName', '')
@@ -594,10 +594,10 @@ class Config {
594
594
  this._setValue(defaults, 'url', undefined)
595
595
  this._setValue(defaults, 'version', pkg.version)
596
596
  this._setValue(defaults, 'instrumentation_config_id', undefined)
597
- this._setValue(defaults, 'aws.dynamoDb.tablePrimaryKeys', undefined)
598
597
  this._setValue(defaults, 'vertexai.spanCharLimit', 128)
599
598
  this._setValue(defaults, 'vertexai.spanPromptCompletionSampleRate', 1.0)
600
599
  this._setValue(defaults, 'trace.aws.addSpanPointers', true)
600
+ this._setValue(defaults, 'trace.dynamoDb.tablePrimaryKeys', undefined)
601
601
  this._setValue(defaults, 'trace.nativeSpanEvents', false)
602
602
  }
603
603
 
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const lock = require('mutexify/promise')()
3
4
  const { getGeneratedPosition } = require('./source-maps')
4
- const lock = require('./lock')()
5
5
  const session = require('./session')
6
6
  const { compile: compileCondition, compileSegments, templateRequiresEvaluation } = require('./condition')
7
7
  const { MAX_SNAPSHOTS_PER_SECOND_PER_PROBE, MAX_NON_SNAPSHOTS_PER_SECOND_PER_PROBE } = require('./defaults')
@@ -64,14 +64,14 @@ async function addBreakpoint (probe) {
64
64
  const release = await lock()
65
65
 
66
66
  try {
67
- log.debug(
68
- '[debugger:devtools_client] Adding breakpoint at %s:%d:%d (probe: %s, version: %d)',
69
- url, lineNumber, columnNumber, probe.id, probe.version
70
- )
71
-
72
67
  const locationKey = generateLocationKey(scriptId, lineNumber, columnNumber)
73
68
  const breakpoint = locationToBreakpoint.get(locationKey)
74
69
 
70
+ log.debug(
71
+ '[debugger:devtools_client] %s breakpoint at %s:%d:%d (probe: %s, version: %d)',
72
+ breakpoint ? 'Updating' : 'Adding', url, lineNumber, columnNumber, probe.id, probe.version
73
+ )
74
+
75
75
  if (breakpoint) {
76
76
  // A breakpoint already exists at this location, so we need to add the probe to the existing breakpoint
77
77
  await updateBreakpoint(breakpoint, probe)
@@ -82,10 +82,15 @@ async function addBreakpoint (probe) {
82
82
  lineNumber: lineNumber - 1, // Beware! lineNumber is zero-indexed
83
83
  columnNumber
84
84
  }
85
- const result = await session.post('Debugger.setBreakpoint', {
86
- location,
87
- condition: probe.condition
88
- })
85
+ let result
86
+ try {
87
+ result = await session.post('Debugger.setBreakpoint', {
88
+ location,
89
+ condition: probe.condition
90
+ })
91
+ } catch (err) {
92
+ throw new Error(`Error setting breakpoint for probe ${probe.id}`, { cause: err })
93
+ }
89
94
  probeToLocation.set(probe.id, locationKey)
90
95
  locationToBreakpoint.set(locationKey, { id: result.breakpointId, location, locationKey })
91
96
  breakpointToProbes.set(result.breakpointId, new Map([[probe.id, probe]]))
@@ -120,7 +125,11 @@ async function removeBreakpoint ({ id }) {
120
125
  if (breakpointToProbes.size === 0) {
121
126
  await stop() // TODO: Will this actually delete the breakpoint?
122
127
  } else {
123
- await session.post('Debugger.removeBreakpoint', { breakpointId: breakpoint.id })
128
+ try {
129
+ await session.post('Debugger.removeBreakpoint', { breakpointId: breakpoint.id })
130
+ } catch (err) {
131
+ throw new Error(`Error removing breakpoint for probe ${id}`, { cause: err })
132
+ }
124
133
  }
125
134
  } else {
126
135
  await updateBreakpoint(breakpoint)
@@ -144,12 +153,21 @@ async function updateBreakpoint (breakpoint, probe) {
144
153
  const condition = compileCompoundCondition(Array.from(probesAtLocation.values()))
145
154
 
146
155
  if (condition || conditionBeforeNewProbe !== condition) {
147
- await session.post('Debugger.removeBreakpoint', { breakpointId: breakpoint.id })
156
+ try {
157
+ await session.post('Debugger.removeBreakpoint', { breakpointId: breakpoint.id })
158
+ } catch (err) {
159
+ throw new Error(`Error removing breakpoint for probe ${probe.id}`, { cause: err })
160
+ }
148
161
  breakpointToProbes.delete(breakpoint.id)
149
- const result = await session.post('Debugger.setBreakpoint', {
150
- location: breakpoint.location,
151
- condition
152
- })
162
+ let result
163
+ try {
164
+ result = await session.post('Debugger.setBreakpoint', {
165
+ location: breakpoint.location,
166
+ condition
167
+ })
168
+ } catch (err) {
169
+ throw new Error(`Error setting breakpoint for probe ${probe.id}`, { cause: err })
170
+ }
153
171
  breakpoint.id = result.breakpointId
154
172
  breakpointToProbes.set(result.breakpointId, probesAtLocation)
155
173
  }
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { workerData: { rcPort } } = require('node:worker_threads')
4
- const lock = require('./lock')()
4
+ const lock = require('mutexify/promise')()
5
5
  const { addBreakpoint, removeBreakpoint } = require('./breakpoints')
6
6
  const { ackReceived, ackInstalled, ackError } = require('./status')
7
7
  const log = require('../../log')
@@ -26,7 +26,7 @@ const ddtags = [
26
26
  ['host_name', hostname],
27
27
  [GIT_COMMIT_SHA, config.commitSHA],
28
28
  [GIT_REPOSITORY_URL, config.repositoryUrl]
29
- ].map((pair) => pair.join(':')).join(',')
29
+ ].filter(([, value]) => value !== undefined).map((pair) => pair.join(':')).join(',')
30
30
 
31
31
  const path = `/debugger/v1/input?${stringify({ ddtags })}`
32
32
 
@@ -1,12 +1,16 @@
1
1
  'use strict'
2
2
 
3
3
  module.exports = {
4
- EVP_PROXY_AGENT_BASE_PATH: 'evp_proxy/v2',
5
- EVP_PROXY_AGENT_ENDPOINT: 'evp_proxy/v2/api/v2/llmobs',
4
+ EVP_PROXY_AGENT_BASE_PATH: '/evp_proxy/v2/',
6
5
  EVP_SUBDOMAIN_HEADER_NAME: 'X-Datadog-EVP-Subdomain',
7
- EVP_SUBDOMAIN_HEADER_VALUE: 'llmobs-intake',
8
- AGENTLESS_SPANS_ENDPOINT: '/api/v2/llmobs',
9
- AGENTLESS_EVALULATIONS_ENDPOINT: '/api/intake/llm-obs/v1/eval-metric',
6
+
7
+ SPANS_EVENT_TYPE: 'span',
8
+ SPANS_INTAKE: 'llmobs-intake',
9
+ SPANS_ENDPOINT: '/api/v2/llmobs',
10
+
11
+ EVALUATIONS_INTAKE: 'api',
12
+ EVALUATIONS_EVENT_TYPE: 'evaluation_metric',
13
+ EVALUATIONS_ENDPOINT: '/api/intake/llm-obs/v1/eval-metric',
10
14
 
11
15
  EVP_PAYLOAD_SIZE_LIMIT: 5 << 20, // 5MB (actual limit is 5.1MB)
12
16
  EVP_EVENT_SIZE_LIMIT: (1 << 20) - 1024 // 999KB (actual limit is 1MB)
@@ -13,9 +13,9 @@ const evalMetricAppendCh = channel('llmobs:eval-metric:append')
13
13
  const flushCh = channel('llmobs:writers:flush')
14
14
  const injectCh = channel('dd-trace:span:inject')
15
15
 
16
- const LLMObsAgentlessSpanWriter = require('./writers/spans/agentless')
17
- const LLMObsAgentProxySpanWriter = require('./writers/spans/agentProxy')
18
16
  const LLMObsEvalMetricsWriter = require('./writers/evaluations')
17
+ const LLMObsSpanWriter = require('./writers/spans')
18
+ const { setAgentStrategy } = require('./writers/util')
19
19
 
20
20
  /**
21
21
  * Setting writers and processor globally when LLMObs is enabled
@@ -25,8 +25,14 @@ const LLMObsEvalMetricsWriter = require('./writers/evaluations')
25
25
  * if the tracer is `init`ed. But, in those cases, we don't want to start writers or subscribe
26
26
  * to channels.
27
27
  */
28
+
29
+ /** @type {LLMObsSpanProcessor | null} */
28
30
  let spanProcessor
31
+
32
+ /** @type {LLMObsSpanWriter | null} */
29
33
  let spanWriter
34
+
35
+ /** @type {LLMObsEvalMetricsWriter | null} */
30
36
  let evalWriter
31
37
 
32
38
  function enable (config) {
@@ -34,7 +40,7 @@ function enable (config) {
34
40
  // create writers and eval writer append and flush channels
35
41
  // span writer append is handled by the span processor
36
42
  evalWriter = new LLMObsEvalMetricsWriter(config)
37
- spanWriter = createSpanWriter(config)
43
+ spanWriter = new LLMObsSpanWriter(config)
38
44
 
39
45
  evalMetricAppendCh.subscribe(handleEvalMetricAppend)
40
46
  flushCh.subscribe(handleFlush)
@@ -46,7 +52,20 @@ function enable (config) {
46
52
 
47
53
  // distributed tracing for llmobs
48
54
  injectCh.subscribe(handleLLMObsParentIdInjection)
49
- telemetry.recordLLMObsEnabled(startTime, config)
55
+
56
+ setAgentStrategy(config, useAgentless => {
57
+ if (useAgentless && !(config.apiKey && config.site)) {
58
+ throw new Error(
59
+ 'Cannot send LLM Observability data without a running agent or without both a Datadog API key and site.\n' +
60
+ 'Ensure these configurations are set before running your application.'
61
+ )
62
+ }
63
+
64
+ evalWriter?.setAgentless(useAgentless)
65
+ spanWriter?.setAgentless(useAgentless)
66
+
67
+ telemetry.recordLLMObsEnabled(startTime, config)
68
+ })
50
69
  }
51
70
 
52
71
  function disable () {
@@ -74,11 +93,6 @@ function handleLLMObsParentIdInjection ({ carrier }) {
74
93
  carrier['x-datadog-tags'] += `,${PROPAGATED_PARENT_ID_KEY}=${parentId}`
75
94
  }
76
95
 
77
- function createSpanWriter (config) {
78
- const SpanWriter = config.llmobs.agentlessEnabled ? LLMObsAgentlessSpanWriter : LLMObsAgentProxySpanWriter
79
- return new SpanWriter(config)
80
- }
81
-
82
96
  function handleFlush () {
83
97
  try {
84
98
  spanWriter.flush()
@@ -287,13 +287,6 @@ class LLMObs extends NoopLLMObs {
287
287
  submitEvaluation (llmobsSpanContext, options = {}) {
288
288
  if (!this.enabled) return
289
289
 
290
- if (!this._config.apiKey) {
291
- throw new Error(
292
- 'DD_API_KEY is required for sending evaluation metrics. Evaluation metric data will not be sent.\n' +
293
- 'Ensure this configuration is set before running your application.'
294
- )
295
- }
296
-
297
290
  const { traceId, spanId } = llmobsSpanContext
298
291
  if (!traceId || !spanId) {
299
292
  throw new Error(
@@ -1,16 +1,22 @@
1
1
  'use strict'
2
2
 
3
3
  const request = require('../../exporters/common/request')
4
- const { URL, format } = require('url')
4
+ const { URL, format } = require('node:url')
5
+ const path = require('node:path')
5
6
 
6
7
  const logger = require('../../log')
7
8
 
8
9
  const { encodeUnicode } = require('../util')
9
10
  const telemetry = require('../telemetry')
10
11
  const log = require('../../log')
12
+ const {
13
+ EVP_SUBDOMAIN_HEADER_NAME,
14
+ EVP_PROXY_AGENT_BASE_PATH
15
+ } = require('../constants/writers')
16
+ const { parseResponseAndLog } = require('./util')
11
17
 
12
18
  class BaseLLMObsWriter {
13
- constructor ({ interval, timeout, endpoint, intake, eventType, protocol, port }) {
19
+ constructor ({ interval, timeout, eventType, config, endpoint, intake }) {
14
20
  this._interval = interval || 1000 // 1s
15
21
  this._timeout = timeout || 5000 // 5s
16
22
  this._eventType = eventType
@@ -19,28 +25,20 @@ class BaseLLMObsWriter {
19
25
  this._bufferLimit = 1000
20
26
  this._bufferSize = 0
21
27
 
22
- this._url = new URL(format({
23
- protocol: protocol || 'https:',
24
- hostname: intake,
25
- port: port || 443,
26
- pathname: endpoint
27
- }))
28
-
29
- this._headers = {
30
- 'Content-Type': 'application/json'
31
- }
28
+ this._config = config
29
+ this._endpoint = endpoint
30
+ this._intake = intake
32
31
 
33
32
  this._periodic = setInterval(() => {
34
33
  this.flush()
35
34
  }, this._interval).unref()
36
35
 
37
- process.once('beforeExit', () => {
36
+ this._beforeExitHandler = () => {
38
37
  this.destroy()
39
- })
38
+ }
39
+ process.once('beforeExit', this._beforeExitHandler)
40
40
 
41
41
  this._destroyed = false
42
-
43
- logger.debug(`Started ${this.constructor.name} to ${this._url}`)
44
42
  }
45
43
 
46
44
  append (event, byteLength) {
@@ -55,7 +53,9 @@ class BaseLLMObsWriter {
55
53
  }
56
54
 
57
55
  flush () {
58
- if (this._buffer.length === 0) {
56
+ const noAgentStrategy = this._agentless == null
57
+
58
+ if (this._buffer.length === 0 || noAgentStrategy) {
59
59
  return
60
60
  }
61
61
 
@@ -64,29 +64,12 @@ class BaseLLMObsWriter {
64
64
  this._bufferSize = 0
65
65
  const payload = this._encode(this.makePayload(events))
66
66
 
67
- const options = {
68
- headers: this._headers,
69
- method: 'POST',
70
- url: this._url,
71
- timeout: this._timeout
72
- }
73
-
74
67
  log.debug(`Encoded LLMObs payload: ${payload}`)
75
68
 
69
+ const options = this._getOptions()
70
+
76
71
  request(payload, options, (err, resp, code) => {
77
- if (err) {
78
- logger.error(
79
- 'Error sending %d LLMObs %s events to %s: %s', events.length, this._eventType, this._url, err.message, err
80
- )
81
- telemetry.recordDroppedPayload(events.length, this._eventType, 'request_error')
82
- } else if (code >= 300) {
83
- logger.error(
84
- 'Error sending %d LLMObs %s events to %s: %s', events.length, this._eventType, this._url, code
85
- )
86
- telemetry.recordDroppedPayload(events.length, this._eventType, 'http_error')
87
- } else {
88
- logger.debug(`Sent ${events.length} LLMObs ${this._eventType} events to ${this._url}`)
89
- }
72
+ parseResponseAndLog(err, code, events.length, options.url.href, this._eventType)
90
73
  })
91
74
  }
92
75
 
@@ -96,12 +79,57 @@ class BaseLLMObsWriter {
96
79
  if (!this._destroyed) {
97
80
  logger.debug(`Stopping ${this.constructor.name}`)
98
81
  clearInterval(this._periodic)
99
- process.removeListener('beforeExit', this.destroy)
82
+ process.removeListener('beforeExit', this._beforeExitHandler)
100
83
  this.flush()
101
84
  this._destroyed = true
102
85
  }
103
86
  }
104
87
 
88
+ setAgentless (agentless) {
89
+ this._agentless = agentless
90
+ this._url = this._getUrl()
91
+ logger.debug(`Configuring ${this.constructor.name} to ${this._url.href}`)
92
+ }
93
+
94
+ _getUrl () {
95
+ if (this._agentless) {
96
+ return new URL(format({
97
+ protocol: 'https:',
98
+ hostname: `${this._intake}.${this._config.site}`,
99
+ pathname: this._endpoint
100
+ }))
101
+ }
102
+
103
+ const { hostname, port } = this._config
104
+ const base = this._config.url || new URL(format({
105
+ protocol: 'http:',
106
+ hostname,
107
+ port
108
+ }))
109
+
110
+ const proxyPath = path.join(EVP_PROXY_AGENT_BASE_PATH, this._endpoint)
111
+ return new URL(proxyPath, base)
112
+ }
113
+
114
+ _getOptions () {
115
+ const options = {
116
+ headers: {
117
+ 'Content-Type': 'application/json'
118
+ },
119
+ method: 'POST',
120
+ timeout: this._timeout,
121
+ url: this._url
122
+ }
123
+
124
+ if (this._agentless) {
125
+ options.headers['DD-API-KEY'] = this._config.apiKey || ''
126
+ } else {
127
+ options.headers[EVP_SUBDOMAIN_HEADER_NAME] = this._intake
128
+ }
129
+
130
+ return options
131
+ }
132
+
105
133
  _encode (payload) {
106
134
  return JSON.stringify(payload, (key, value) => {
107
135
  if (typeof value === 'string') {
@@ -1,17 +1,20 @@
1
1
  'use strict'
2
2
 
3
- const { AGENTLESS_EVALULATIONS_ENDPOINT } = require('../constants/writers')
3
+ const {
4
+ EVALUATIONS_ENDPOINT,
5
+ EVALUATIONS_EVENT_TYPE,
6
+ EVALUATIONS_INTAKE
7
+ } = require('../constants/writers')
4
8
  const BaseWriter = require('./base')
5
9
 
6
10
  class LLMObsEvalMetricsWriter extends BaseWriter {
7
11
  constructor (config) {
8
12
  super({
9
- endpoint: AGENTLESS_EVALULATIONS_ENDPOINT,
10
- intake: `api.${config.site}`,
11
- eventType: 'evaluation_metric'
13
+ config,
14
+ intake: EVALUATIONS_INTAKE,
15
+ eventType: EVALUATIONS_EVENT_TYPE,
16
+ endpoint: EVALUATIONS_ENDPOINT
12
17
  })
13
-
14
- this._headers['DD-API-KEY'] = config.apiKey
15
18
  }
16
19
 
17
20
  makePayload (events) {
@@ -1,19 +1,27 @@
1
1
  'use strict'
2
2
 
3
- const { EVP_EVENT_SIZE_LIMIT, EVP_PAYLOAD_SIZE_LIMIT } = require('../../constants/writers')
4
- const { DROPPED_VALUE_TEXT } = require('../../constants/text')
5
- const { DROPPED_IO_COLLECTION_ERROR } = require('../../constants/tags')
6
- const BaseWriter = require('../base')
7
- const telemetry = require('../../telemetry')
8
- const logger = require('../../../log')
3
+ const {
4
+ EVP_EVENT_SIZE_LIMIT,
5
+ EVP_PAYLOAD_SIZE_LIMIT,
6
+ SPANS_ENDPOINT,
7
+ SPANS_EVENT_TYPE,
8
+ SPANS_INTAKE
9
+ } = require('../constants/writers')
10
+ const { DROPPED_VALUE_TEXT } = require('../constants/text')
11
+ const { DROPPED_IO_COLLECTION_ERROR } = require('../constants/tags')
12
+ const BaseWriter = require('./base')
13
+ const telemetry = require('../telemetry')
14
+ const logger = require('../../log')
9
15
 
10
- const tracerVersion = require('../../../../../../package.json').version
16
+ const tracerVersion = require('../../../../../package.json').version
11
17
 
12
18
  class LLMObsSpanWriter extends BaseWriter {
13
- constructor (options) {
19
+ constructor (config) {
14
20
  super({
15
- ...options,
16
- eventType: 'span'
21
+ config,
22
+ eventType: SPANS_EVENT_TYPE,
23
+ intake: SPANS_INTAKE,
24
+ endpoint: SPANS_ENDPOINT
17
25
  })
18
26
  }
19
27
 
@@ -33,11 +41,11 @@ class LLMObsSpanWriter extends BaseWriter {
33
41
  telemetry.recordLLMObsSpanSize(event, processedEventSizeBytes, shouldTruncate)
34
42
 
35
43
  if (this._bufferSize + eventSizeBytes > EVP_PAYLOAD_SIZE_LIMIT) {
36
- logger.debug('Flusing queue because queing next event will exceed EvP payload limit')
44
+ logger.debug('Flushing queue because queuing next event will exceed EvP payload limit')
37
45
  this.flush()
38
46
  }
39
47
 
40
- super.append(event, eventSizeBytes)
48
+ super.append(event, processedEventSizeBytes)
41
49
  }
42
50
 
43
51
  makePayload (events) {
@@ -0,0 +1,60 @@
1
+ 'use strict'
2
+
3
+ const logger = require('../../log')
4
+ const { EVP_PROXY_AGENT_BASE_PATH } = require('../constants/writers')
5
+ const telemetry = require('../telemetry')
6
+
7
+ const AgentInfoExporter = require('../../exporters/common/agent-info-exporter')
8
+ /** @type {AgentInfoExporter} */
9
+ let agentInfoExporter
10
+
11
+ function setAgentStrategy (config, setWritersAgentlessValue) {
12
+ const agentlessEnabled = config.llmobs.agentlessEnabled
13
+
14
+ if (agentlessEnabled != null) {
15
+ setWritersAgentlessValue(agentlessEnabled)
16
+ return
17
+ }
18
+
19
+ if (!agentInfoExporter) {
20
+ agentInfoExporter = new AgentInfoExporter(config)
21
+ }
22
+
23
+ agentInfoExporter.getAgentInfo((err, agentInfo) => {
24
+ if (err) {
25
+ setWritersAgentlessValue(true)
26
+ return
27
+ }
28
+
29
+ const endpoints = agentInfo.endpoints
30
+ const hasEndpoint = Array.isArray(endpoints) && endpoints.some(endpoint => endpoint === EVP_PROXY_AGENT_BASE_PATH)
31
+ setWritersAgentlessValue(!hasEndpoint)
32
+ })
33
+ }
34
+
35
+ function parseResponseAndLog (err, code, eventsLength, url, eventType) {
36
+ if (code === 403 && err.message.includes('API key is invalid')) {
37
+ logger.error(
38
+ '[LLMObs] The provided Datadog API key is invalid (likely due to an API key and DD_SITE mismatch). ' +
39
+ 'Please verify your API key and DD_SITE are correct.'
40
+ )
41
+ telemetry.recordDroppedPayload(eventsLength, eventType, 'request_error')
42
+ } else if (err) {
43
+ logger.error(
44
+ 'Error sending %d LLMObs %s events to %s: %s', eventsLength, eventType, url, err.message, err
45
+ )
46
+ telemetry.recordDroppedPayload(eventsLength, eventType, 'request_error')
47
+ } else if (code >= 300) {
48
+ logger.error(
49
+ 'Error sending %d LLMObs %s events to %s: %s', eventsLength, eventType, url, code
50
+ )
51
+ telemetry.recordDroppedPayload(eventsLength, eventType, 'http_error')
52
+ } else {
53
+ logger.debug(`Sent ${eventsLength} LLMObs ${eventType} events to ${url}`)
54
+ }
55
+ }
56
+
57
+ module.exports = {
58
+ setAgentStrategy,
59
+ parseResponseAndLog
60
+ }
@@ -1,8 +0,0 @@
1
- 'use strict'
2
-
3
- module.exports = () => async function lock () {
4
- if (lock.p) await lock.p
5
- let resolve
6
- lock.p = new Promise((_resolve) => { resolve = _resolve }).then(() => { lock.p = null })
7
- return resolve
8
- }
@@ -1,23 +0,0 @@
1
- 'use strict'
2
-
3
- const {
4
- EVP_SUBDOMAIN_HEADER_NAME,
5
- EVP_SUBDOMAIN_HEADER_VALUE,
6
- EVP_PROXY_AGENT_ENDPOINT
7
- } = require('../../constants/writers')
8
- const LLMObsBaseSpanWriter = require('./base')
9
-
10
- class LLMObsAgentProxySpanWriter extends LLMObsBaseSpanWriter {
11
- constructor (config) {
12
- super({
13
- intake: config.url?.hostname || config.hostname || 'localhost',
14
- protocol: config.url?.protocol || 'http:',
15
- endpoint: EVP_PROXY_AGENT_ENDPOINT,
16
- port: config.url?.port || config.port
17
- })
18
-
19
- this._headers[EVP_SUBDOMAIN_HEADER_NAME] = EVP_SUBDOMAIN_HEADER_VALUE
20
- }
21
- }
22
-
23
- module.exports = LLMObsAgentProxySpanWriter
@@ -1,17 +0,0 @@
1
- 'use strict'
2
-
3
- const { AGENTLESS_SPANS_ENDPOINT } = require('../../constants/writers')
4
- const LLMObsBaseSpanWriter = require('./base')
5
-
6
- class LLMObsAgentlessSpanWriter extends LLMObsBaseSpanWriter {
7
- constructor (config) {
8
- super({
9
- intake: `llmobs-intake.${config.site}`,
10
- endpoint: AGENTLESS_SPANS_ENDPOINT
11
- })
12
-
13
- this._headers['DD-API-KEY'] = config.apiKey
14
- }
15
- }
16
-
17
- module.exports = LLMObsAgentlessSpanWriter