dd-trace 5.48.0 → 5.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "5.48.0",
3
+ "version": "5.49.0",
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\"",
@@ -90,7 +91,7 @@
90
91
  "@datadog/native-metrics": "^3.1.1",
91
92
  "@datadog/pprof": "5.7.1",
92
93
  "@datadog/sketches-js": "^2.1.0",
93
- "@datadog/wasm-js-rewriter": "4.0.0",
94
+ "@datadog/wasm-js-rewriter": "4.0.1",
94
95
  "@isaacs/ttlcache": "^1.4.1",
95
96
  "@opentelemetry/api": ">=1.0.0 <1.9.0",
96
97
  "@opentelemetry/core": "^1.14.0",
@@ -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
@@ -1,5 +1,54 @@
1
- const path = require('path')
2
- const fs = require('fs')
3
-
4
- // TODO this needs to be inlined to prevent issues in bundling
5
- module.exports = fs.readFileSync(path.join(__dirname, '../../orchestrion.yml'), 'utf8')
1
+ module.exports = `
2
+ version: 1
3
+ dc_module: dc-polyfill
4
+ instrumentations:
5
+ - module_name: "@langchain/core"
6
+ version_range: ">=0.1.0"
7
+ file_path: dist/runnables/base.js
8
+ function_query:
9
+ name: invoke
10
+ type: method
11
+ kind: async
12
+ class: RunnableSequence
13
+ operator: tracePromise
14
+ channel_name: "RunnableSequence_invoke"
15
+ - module_name: "@langchain/core"
16
+ version_range: ">=0.1.0"
17
+ file_path: dist/runnables/base.js
18
+ function_query:
19
+ name: batch
20
+ type: method
21
+ kind: async
22
+ class: RunnableSequence
23
+ operator: tracePromise
24
+ channel_name: "RunnableSequence_batch"
25
+ - module_name: "@langchain/core"
26
+ version_range: ">=0.1.0"
27
+ file_path: dist/language_models/chat_models.js
28
+ function_query:
29
+ name: generate
30
+ type: method
31
+ kind: async
32
+ class: BaseChatModel
33
+ operator: tracePromise
34
+ channel_name: "BaseChatModel_generate"
35
+ - module_name: "@langchain/core"
36
+ version_range: ">=0.1.0"
37
+ file_path: dist/language_models/llms.js
38
+ function_query:
39
+ name: generate
40
+ type: method
41
+ kind: async
42
+ operator: tracePromise
43
+ channel_name: "BaseLLM_generate"
44
+ - module_name: "@langchain/core"
45
+ version_range: ">=0.1.0"
46
+ file_path: dist/embeddings.js
47
+ function_query:
48
+ name: constructor
49
+ type: method
50
+ kind: sync
51
+ class: Embeddings
52
+ operator: traceSync
53
+ channel_name: "Embeddings_constructor"
54
+ `
@@ -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
@@ -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
  }
@@ -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', '')
@@ -825,7 +825,7 @@ class Config {
825
825
  this._setValue(env, 'baggageMaxBytes', DD_TRACE_BAGGAGE_MAX_BYTES)
826
826
  this._setValue(env, 'baggageMaxItems', DD_TRACE_BAGGAGE_MAX_ITEMS)
827
827
  this._setBoolean(env, 'clientIpEnabled', DD_TRACE_CLIENT_IP_ENABLED)
828
- this._setString(env, 'clientIpHeader', DD_TRACE_CLIENT_IP_HEADER)
828
+ this._setString(env, 'clientIpHeader', DD_TRACE_CLIENT_IP_HEADER?.toLowerCase())
829
829
  this._setBoolean(env, 'crashtracking.enabled', coalesce(
830
830
  DD_CRASHTRACKING_ENABLED,
831
831
  !this._isInServerlessEnvironment()
@@ -1032,7 +1032,7 @@ class Config {
1032
1032
  this._setValue(opts, 'appsec.wafTimeout', maybeInt(options.appsec.wafTimeout))
1033
1033
  this._optsUnprocessed['appsec.wafTimeout'] = options.appsec.wafTimeout
1034
1034
  this._setBoolean(opts, 'clientIpEnabled', options.clientIpEnabled)
1035
- this._setString(opts, 'clientIpHeader', options.clientIpHeader)
1035
+ this._setString(opts, 'clientIpHeader', options.clientIpHeader?.toLowerCase())
1036
1036
  this._setValue(opts, 'baggageMaxBytes', options.baggageMaxBytes)
1037
1037
  this._setValue(opts, 'baggageMaxItems', options.baggageMaxItems)
1038
1038
  this._setBoolean(opts, 'codeOriginForSpans.enabled', options.codeOriginForSpans?.enabled)
@@ -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
+ }
@@ -17,6 +17,11 @@ function messageProxy (message, holder) {
17
17
  return holder.dd
18
18
  }
19
19
 
20
+ // This is a workaround for a V8 bug that surfaced in Node.js 22
21
+ if (p === 'stack') {
22
+ return target.stack
23
+ }
24
+
20
25
  return Reflect.get(target, p, receiver)
21
26
  },
22
27
  ownKeys (target) {
@@ -115,8 +115,6 @@ class NativeWallProfiler {
115
115
  start ({ mapper } = {}) {
116
116
  if (this._started) return
117
117
 
118
- ensureChannelsActivated()
119
-
120
118
  this._mapper = mapper
121
119
  this._pprof = require('@datadog/pprof')
122
120
  kSampleCount = this._pprof.time.constants.kSampleCount
@@ -147,6 +145,8 @@ class NativeWallProfiler {
147
145
  this._profilerState = this._pprof.time.getState()
148
146
  this._lastSampleCount = 0
149
147
 
148
+ ensureChannelsActivated()
149
+
150
150
  beforeCh.subscribe(this._enter)
151
151
  enterCh.subscribe(this._enter)
152
152
  spanFinishCh.subscribe(this._spanFinished)
@@ -1,52 +0,0 @@
1
- version: 1
2
- dc_module: dc-polyfill
3
- instrumentations:
4
- - module_name: "@langchain/core"
5
- version_range: ">=0.1.0"
6
- file_path: dist/runnables/base.js
7
- function_query:
8
- name: invoke
9
- type: method
10
- kind: async
11
- class: RunnableSequence
12
- operator: tracePromise
13
- channel_name: "RunnableSequence_invoke"
14
- - module_name: "@langchain/core"
15
- version_range: ">=0.1.0"
16
- file_path: dist/runnables/base.js
17
- function_query:
18
- name: batch
19
- type: method
20
- kind: async
21
- class: RunnableSequence
22
- operator: tracePromise
23
- channel_name: "RunnableSequence_batch"
24
- - module_name: "@langchain/core"
25
- version_range: ">=0.1.0"
26
- file_path: dist/language_models/chat_models.js
27
- function_query:
28
- name: generate
29
- type: method
30
- kind: async
31
- class: BaseChatModel
32
- operator: tracePromise
33
- channel_name: "BaseChatModel_generate"
34
- - module_name: "@langchain/core"
35
- version_range: ">=0.1.0"
36
- file_path: dist/language_models/llms.js
37
- function_query:
38
- name: generate
39
- type: method
40
- kind: async
41
- operator: tracePromise
42
- channel_name: "BaseLLM_generate"
43
- - module_name: "@langchain/core"
44
- version_range: ">=0.1.0"
45
- file_path: dist/embeddings.js
46
- function_query:
47
- name: constructor
48
- type: method
49
- kind: sync
50
- class: Embeddings
51
- operator: traceSync
52
- channel_name: "Embeddings_constructor"
@@ -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