dd-trace 5.123.0 → 5.124.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 (97) hide show
  1. package/ci/test-optimization-validation/framework-adapters/cypress.js +9 -11
  2. package/ci/test-optimization-validation/scenarios/ci-wiring.js +5 -4
  3. package/index.d.ts +128 -26
  4. package/package.json +10 -10
  5. package/packages/datadog-instrumentations/src/bunyan.js +4 -2
  6. package/packages/datadog-instrumentations/src/helpers/bunyan.js +12 -3
  7. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +1 -0
  8. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +28 -0
  9. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  10. package/packages/datadog-instrumentations/src/jest.js +140 -18
  11. package/packages/datadog-instrumentations/src/mariadb-bundle.js +1597 -0
  12. package/packages/datadog-instrumentations/src/mariadb.js +7 -0
  13. package/packages/datadog-instrumentations/src/pino.js +15 -4
  14. package/packages/datadog-instrumentations/src/playwright.js +34 -20
  15. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +7 -4
  16. package/packages/datadog-instrumentations/src/vitest-worker.js +11 -0
  17. package/packages/datadog-instrumentations/src/webdriverio.js +26 -0
  18. package/packages/datadog-instrumentations/src/winston.js +6 -3
  19. package/packages/datadog-plugin-cypress/src/support.js +1 -0
  20. package/packages/datadog-plugin-http/src/client.js +2 -15
  21. package/packages/datadog-plugin-http2/src/client.js +2 -15
  22. package/packages/datadog-plugin-openai-agents/src/integration.js +15 -11
  23. package/packages/datadog-plugin-playwright/src/index.js +9 -1
  24. package/packages/datadog-plugin-undici/src/index.js +2 -15
  25. package/packages/dd-trace/src/aiguard/evaluation.js +94 -20
  26. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -3
  27. package/packages/dd-trace/src/aiguard/messages/anthropic.js +21 -9
  28. package/packages/dd-trace/src/aiguard/noop.js +9 -1
  29. package/packages/dd-trace/src/aiguard/redaction.js +183 -0
  30. package/packages/dd-trace/src/aiguard/sdk.js +7 -2
  31. package/packages/dd-trace/src/aiguard/tags.js +2 -0
  32. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +10 -9
  33. package/packages/dd-trace/src/appsec/rule_manager.js +2 -0
  34. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +51 -26
  35. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -11
  36. package/packages/dd-trace/src/ci-visibility/final-flush.js +20 -0
  37. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +196 -29
  38. package/packages/dd-trace/src/config/generated-config-types.d.ts +6 -0
  39. package/packages/dd-trace/src/config/index.js +2 -1
  40. package/packages/dd-trace/src/config/supported-configurations.json +27 -0
  41. package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -3
  42. package/packages/dd-trace/src/dogstatsd.js +45 -18
  43. package/packages/dd-trace/src/encode/agentless-json.js +5 -4
  44. package/packages/dd-trace/src/exporters/agent/index.js +22 -2
  45. package/packages/dd-trace/src/exporters/agent/writer.js +4 -4
  46. package/packages/dd-trace/src/exporters/common/writer.js +24 -2
  47. package/packages/dd-trace/src/exporters/span-stats/index.js +23 -4
  48. package/packages/dd-trace/src/external-logger/src/index.js +4 -3
  49. package/packages/dd-trace/src/flush.js +102 -0
  50. package/packages/dd-trace/src/llmobs/experiments/client.js +4 -0
  51. package/packages/dd-trace/src/llmobs/experiments/dataset.js +51 -2
  52. package/packages/dd-trace/src/llmobs/experiments/experiment.js +420 -105
  53. package/packages/dd-trace/src/llmobs/experiments/index.js +54 -42
  54. package/packages/dd-trace/src/llmobs/experiments/noop.js +39 -8
  55. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  56. package/packages/dd-trace/src/llmobs/experiments/util.js +21 -0
  57. package/packages/dd-trace/src/llmobs/index.js +80 -17
  58. package/packages/dd-trace/src/llmobs/noop.js +2 -1
  59. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +8 -4
  60. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +6 -3
  61. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +13 -6
  62. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +5 -7
  63. package/packages/dd-trace/src/llmobs/writers/base.js +60 -24
  64. package/packages/dd-trace/src/noop/span.js +5 -0
  65. package/packages/dd-trace/src/openfeature/writers/exposures.js +8 -1
  66. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +45 -3
  67. package/packages/dd-trace/src/opentelemetry/logs/index.js +4 -1
  68. package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +7 -4
  69. package/packages/dd-trace/src/opentelemetry/metrics/index.js +3 -1
  70. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +8 -0
  71. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +4 -2
  72. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +4 -2
  73. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +25 -5
  74. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +61 -25
  75. package/packages/dd-trace/src/opentelemetry/tracer.js +26 -2
  76. package/packages/dd-trace/src/opentracing/propagation/text_map.js +410 -342
  77. package/packages/dd-trace/src/opentracing/span.js +28 -0
  78. package/packages/dd-trace/src/otel-thread-ctx.js +23 -5
  79. package/packages/dd-trace/src/plugin_manager.js +2 -0
  80. package/packages/dd-trace/src/plugins/util/status-validator.js +98 -0
  81. package/packages/dd-trace/src/plugins/util/test.js +74 -21
  82. package/packages/dd-trace/src/plugins/util/web.js +2 -65
  83. package/packages/dd-trace/src/profiling/oom.js +4 -3
  84. package/packages/dd-trace/src/profiling/profiler.js +7 -0
  85. package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
  86. package/packages/dd-trace/src/profiling/profilers/wall.js +14 -14
  87. package/packages/dd-trace/src/proxy.js +36 -8
  88. package/packages/dd-trace/src/runtime_metrics/index.js +5 -0
  89. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -0
  90. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +17 -6
  91. package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +55 -0
  92. package/packages/dd-trace/src/serverless/vercel.js +122 -0
  93. package/packages/dd-trace/src/serverless.js +37 -20
  94. package/packages/dd-trace/src/span_stats.js +40 -3
  95. package/packages/dd-trace/src/tracer.js +22 -0
  96. package/packages/dd-trace/src/web-tags-cache.js +67 -13
  97. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
@@ -7,12 +7,12 @@ const { getEnvironmentVariable } = require('./config/helper')
7
7
  const runtimeMetrics = require('./runtime_metrics')
8
8
  const log = require('./log')
9
9
  const { setStartupLogPluginManager, startupLog } = require('./startup-log')
10
- const DynamicInstrumentation = require('./debugger')
11
10
  const telemetry = require('./telemetry')
12
11
  const nomenclature = require('./service-naming')
13
12
  const PluginManager = require('./plugin_manager')
14
13
  const NoopDogStatsDClient = require('./noop/dogstatsd')
15
- const { IS_SERVERLESS } = require('./serverless')
14
+ const { IS_SERVERLESS, initializeServerlessTelemetry, supportsServerlessTelemetryRetention } = require('./serverless')
15
+ const { flushServerlessTelemetry, registerTelemetryFlusher } = require('./flush')
16
16
  const processTags = require('./process-tags')
17
17
  const { isTrue } = require('./util')
18
18
  const {
@@ -41,6 +41,13 @@ const OPENFEATURE_STATE_NOOP = 0
41
41
  const OPENFEATURE_STATE_LAZY = 1
42
42
  const OPENFEATURE_STATE_ACTIVE = 2
43
43
 
44
+ let dynamicInstrumentation
45
+
46
+ function getDynamicInstrumentation () {
47
+ dynamicInstrumentation ??= require('./debugger')
48
+ return dynamicInstrumentation
49
+ }
50
+
44
51
  class LazyModule {
45
52
  constructor (provider) {
46
53
  this.provider = provider
@@ -100,6 +107,15 @@ class Tracer extends NoopProxy {
100
107
  this._pluginManager = new PluginManager(this)
101
108
  this.dogstatsd = new NoopDogStatsDClient()
102
109
  this._tracingInitialized = false
110
+ // Logs and metrics can need retention even when tracing is disabled.
111
+ if (supportsServerlessTelemetryRetention()) {
112
+ this._serverlessTelemetry = {
113
+ flushAll: (done, options) => {
114
+ if (typeof this._tracer?.flushAll === 'function') this._tracer.flushAll(done, options)
115
+ else flushServerlessTelemetry(done, options)
116
+ },
117
+ }
118
+ }
103
119
  this._flare = new LazyModule(() => require('./flare'))
104
120
  this.setBaggageItem = setBaggageItem
105
121
  this.getBaggageItem = getBaggageItem
@@ -205,7 +221,7 @@ class Tracer extends NoopProxy {
205
221
  }
206
222
 
207
223
  if (config.dynamicInstrumentation.enabled) {
208
- DynamicInstrumentation.start(config, rc)
224
+ getDynamicInstrumentation().start(config, rc)
209
225
  }
210
226
 
211
227
  const openfeatureRemoteConfig = require('./openfeature/remote_config')
@@ -255,13 +271,20 @@ class Tracer extends NoopProxy {
255
271
 
256
272
  if (config.runtimeMetrics.enabled) {
257
273
  runtimeMetrics.start(config)
274
+ // Agent trace response metrics are recorded asynchronously, so drain
275
+ // runtime metrics after the trace export has completed.
276
+ registerTelemetryFlusher(done => runtimeMetrics.flush(done), { afterTrace: true })
258
277
  }
259
278
 
260
279
  this.#updateTracing(config)
261
280
 
262
- this._modules.rewriter.enable(config)
281
+ if (config.iast.enabled) {
282
+ this._modules.rewriter.enable(config)
283
+ }
263
284
 
264
- if (config.DD_TRACE_ENABLED && config.testOptimization.DD_CIVISIBILITY_MANUAL_API_ENABLED) {
285
+ if (config.isCiVisibility &&
286
+ config.DD_TRACE_ENABLED &&
287
+ config.testOptimization.DD_CIVISIBILITY_MANUAL_API_ENABLED) {
265
288
  const TestApiManualPlugin = require('./ci-visibility/test-api-manual/test-api-manual-plugin')
266
289
  this._testApiManualPlugin = new TestApiManualPlugin(this)
267
290
  // `shouldGetEnvironmentData` is passed as false so that we only lazily calculate it
@@ -269,7 +292,7 @@ class Tracer extends NoopProxy {
269
292
  // are lazily configured when the library is imported.
270
293
  this._testApiManualPlugin.configure({ ...config, enabled: true }, false)
271
294
  }
272
- if (config.DD_AGENTLESS_LOG_SUBMISSION_ENABLED) {
295
+ if (config.isCiVisibility && config.DD_AGENTLESS_LOG_SUBMISSION_ENABLED) {
273
296
  if (config.DD_API_KEY) {
274
297
  const LogSubmissionPlugin = require('./ci-visibility/log-submission/log-submission-plugin')
275
298
  const automaticLogPlugin = new LogSubmissionPlugin(this)
@@ -282,7 +305,7 @@ class Tracer extends NoopProxy {
282
305
  }
283
306
  }
284
307
 
285
- if (config.testOptimization.DD_TEST_FAILED_TEST_REPLAY_ENABLED) {
308
+ if (config.isCiVisibility && config.testOptimization.DD_TEST_FAILED_TEST_REPLAY_ENABLED) {
286
309
  const getDynamicInstrumentationClient = require('./ci-visibility/dynamic-instrumentation')
287
310
  // We instantiate the client but do not start the Worker here. The worker is started lazily
288
311
  getDynamicInstrumentationClient(config)
@@ -389,10 +412,12 @@ class Tracer extends NoopProxy {
389
412
  if (this._tracingInitialized) {
390
413
  this._tracer.configure(config)
391
414
  this._pluginManager.configure(config)
392
- DynamicInstrumentation.configure(config)
415
+ dynamicInstrumentation?.configure(config)
393
416
  setStartupLogPluginManager(this._pluginManager)
394
417
  startupLog()
395
418
  }
419
+
420
+ initializeServerlessTelemetry(this._serverlessTelemetry)
396
421
  }
397
422
 
398
423
  /**
@@ -404,6 +429,9 @@ class Tracer extends NoopProxy {
404
429
  */
405
430
  #updateDebugger (config, rc) {
406
431
  const shouldBeEnabled = config.dynamicInstrumentation.enabled
432
+ if (!shouldBeEnabled && dynamicInstrumentation === undefined) return
433
+
434
+ const DynamicInstrumentation = getDynamicInstrumentation()
407
435
  const isCurrentlyStarted = DynamicInstrumentation.isStarted()
408
436
 
409
437
  if (shouldBeEnabled) {
@@ -13,6 +13,7 @@ const noop = runtimeMetrics = {
13
13
  gauge () {},
14
14
  increment () {},
15
15
  decrement () {},
16
+ flush (done) { done?.() },
16
17
  }
17
18
 
18
19
  module.exports = {
@@ -42,6 +43,10 @@ module.exports = {
42
43
  runtimeMetrics = noop
43
44
  Object.setPrototypeOf(module.exports, noop)
44
45
  },
46
+
47
+ flush (done) {
48
+ runtimeMetrics.flush(done)
49
+ },
45
50
  }
46
51
 
47
52
  Object.setPrototypeOf(module.exports, noop)
@@ -245,6 +245,11 @@ module.exports = {
245
245
  decrement (name, tag) {
246
246
  this.count(name, -1, tag)
247
247
  },
248
+
249
+ flush (done) {
250
+ if (client) return client.flush(done)
251
+ done?.()
252
+ },
248
253
  }
249
254
 
250
255
  /**
@@ -27,6 +27,7 @@ let client = null
27
27
  let lastTime = 0
28
28
  let lastCpuUsage = null
29
29
  let eventLoopDelayObserver = null
30
+ let capture = null
30
31
 
31
32
  // !!!!!!!!!!!
32
33
  // IMPORTANT
@@ -76,11 +77,10 @@ module.exports = {
76
77
  lastTime = performance.now()
77
78
 
78
79
  if (nativeMetrics) {
79
- interval = setInterval(() => {
80
+ capture = () => {
80
81
  captureNativeMetrics(trackEventLoop, trackGc)
81
82
  captureCommonMetrics(trackEventLoop)
82
- client.flush()
83
- }, flushIntervalMs)
83
+ }
84
84
  } else {
85
85
  lastCpuUsage = process.cpuUsage()
86
86
 
@@ -92,17 +92,21 @@ module.exports = {
92
92
  eventLoopDelayObserver.enable()
93
93
  }
94
94
 
95
- interval = setInterval(() => {
95
+ capture = () => {
96
96
  captureCpuUsage()
97
97
  captureCommonMetrics(trackEventLoop)
98
98
  captureHeapSpace()
99
99
  if (trackEventLoop) {
100
100
  captureEventLoopDelay()
101
101
  }
102
- client.flush()
103
- }, flushIntervalMs)
102
+ }
104
103
  }
105
104
 
105
+ interval = setInterval(() => {
106
+ capture()
107
+ client.flush()
108
+ }, flushIntervalMs)
109
+
106
110
  interval.unref?.()
107
111
  },
108
112
 
@@ -114,6 +118,7 @@ module.exports = {
114
118
  interval = null
115
119
 
116
120
  client = null
121
+ capture = null
117
122
  lastCpuUsage = null
118
123
 
119
124
  gcObserver?.disconnect()
@@ -158,6 +163,12 @@ module.exports = {
158
163
  decrement (name, tag) {
159
164
  this.count(name, -1, tag)
160
165
  },
166
+
167
+ flush (done) {
168
+ if (!client) return done?.()
169
+ capture?.()
170
+ client.flush(done)
171
+ },
161
172
  }
162
173
 
163
174
  function captureCpuUsage () {
@@ -0,0 +1,55 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Tracks transport deliveries that must outlive a serverless request.
5
+ *
6
+ * The tracker is created only for platforms with an invocation-retention
7
+ * boundary. Exporters keep their normal callback path when it is absent.
8
+ */
9
+ class TelemetryDeliveryTracker {
10
+ #deliveries = new Set()
11
+
12
+ /**
13
+ * Tracks one asynchronous transport delivery until its callback runs.
14
+ * @param {(done: () => void) => void} deliver
15
+ * @param {(() => void)|undefined} done
16
+ */
17
+ track (deliver, done) {
18
+ const delivery = { callbacks: done ? [done] : [] }
19
+ this.#deliveries.add(delivery)
20
+
21
+ let completed = false
22
+ const complete = () => {
23
+ if (completed) return
24
+ completed = true
25
+ this.#deliveries.delete(delivery)
26
+ for (const callback of delivery.callbacks) callback()
27
+ }
28
+
29
+ try {
30
+ deliver(complete)
31
+ } catch (error) {
32
+ complete()
33
+ throw error
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Calls back after every delivery active at this boundary has completed.
39
+ * @param {(() => void)|undefined} done
40
+ */
41
+ waitForIdle (done) {
42
+ if (!done) return
43
+
44
+ const deliveries = [...this.#deliveries]
45
+ if (deliveries.length === 0) return done()
46
+
47
+ let pending = deliveries.length
48
+ const complete = () => {
49
+ if (--pending === 0) done()
50
+ }
51
+ for (const delivery of deliveries) delivery.callbacks.push(complete)
52
+ }
53
+ }
54
+
55
+ module.exports = TelemetryDeliveryTracker
@@ -0,0 +1,122 @@
1
+ 'use strict'
2
+
3
+ const { channel } = require('dc-polyfill')
4
+
5
+ const { getEnvironmentVariable } = require('../config/helper')
6
+ const log = require('../log')
7
+
8
+ const httpRequestFinishChannel = channel('apm:http:server:request:finish')
9
+ const http2RequestStartChannel = channel('apm:http2:server:request:start')
10
+ const http2ResponseEmitChannel = channel('apm:http2:server:response:emit')
11
+ const VERCEL_REQUEST_CONTEXT = Symbol.for('@vercel/request-context')
12
+ const VERCEL_FLUSH_TIMEOUT = 2000
13
+ const vercelRetentionHandlers = new WeakMap()
14
+
15
+ /**
16
+ * @typedef {{ flushAll?: (done: () => void, options?: { timeout?: number }) => void }} TelemetryFlusher
17
+ */
18
+
19
+ /**
20
+ * @param {TelemetryFlusher} tracer
21
+ * @param {() => void} done
22
+ * @returns {void}
23
+ */
24
+ function flushVercelTelemetry (tracer, done) {
25
+ setImmediate(() => {
26
+ try {
27
+ tracer.flushAll(done, { timeout: VERCEL_FLUSH_TIMEOUT })
28
+ } catch (error) {
29
+ log.warn('Unable to flush Vercel telemetry:', error)
30
+ done()
31
+ }
32
+ })
33
+ }
34
+
35
+ function registerVercelRequestFlush (tracer) {
36
+ const requestContext = getVercelRequestContext()
37
+ if (!requestContext) return
38
+
39
+ const { waitUntil } = requestContext
40
+ if (typeof waitUntil !== 'function') return
41
+
42
+ // Retain the invocation synchronously, then flush after the response completes.
43
+ let done
44
+ const pending = new Promise(resolve => { done = resolve })
45
+ try {
46
+ waitUntil(pending)
47
+ flushVercelTelemetry(tracer, done)
48
+ } catch (error) {
49
+ log.warn('Unable to retain Vercel telemetry:', error)
50
+ done()
51
+ }
52
+ }
53
+
54
+ function getVercelRequestContext () {
55
+ return globalThis[VERCEL_REQUEST_CONTEXT]?.get?.()
56
+ }
57
+
58
+ // HTTP/2 binds response lifecycle events while handling its request-start channel.
59
+ function activateHttp2Lifecycle () {}
60
+
61
+ /**
62
+ * Retains a Vercel Node Function until configured telemetry exporters complete.
63
+ *
64
+ * @param {TelemetryFlusher} tracer
65
+ * @returns {(() => void)|undefined}
66
+ */
67
+ function registerVercelTelemetryRetention (tracer) {
68
+ const existing = vercelRetentionHandlers.get(tracer)
69
+ if (existing) return existing
70
+
71
+ if (typeof tracer?.flushAll !== 'function') return
72
+ // The HTTP finish channel activates its response wrapper directly. HTTP/2 needs
73
+ // a passive request-start subscriber before it can bind response emit events.
74
+ const flushRequest = () => registerVercelRequestFlush(tracer)
75
+ const flushHttp2Response = ({ eventName }) => {
76
+ if (eventName === 'close') flushRequest()
77
+ }
78
+ httpRequestFinishChannel.subscribe(flushRequest)
79
+ http2RequestStartChannel.subscribe(activateHttp2Lifecycle)
80
+ http2ResponseEmitChannel.subscribe(flushHttp2Response)
81
+
82
+ const unregister = () => {
83
+ httpRequestFinishChannel.unsubscribe(flushRequest)
84
+ http2RequestStartChannel.unsubscribe(activateHttp2Lifecycle)
85
+ http2ResponseEmitChannel.unsubscribe(flushHttp2Response)
86
+ vercelRetentionHandlers.delete(tracer)
87
+ }
88
+ vercelRetentionHandlers.set(tracer, unregister)
89
+ return unregister
90
+ }
91
+
92
+ /**
93
+ * Gets Vercel deployment tags to attach to spans.
94
+ *
95
+ * @returns {string[]|undefined}
96
+ */
97
+ function getVercelPlatformTags () {
98
+ let tags
99
+ const projectId = getEnvironmentVariable('VERCEL_PROJECT_ID')
100
+ if (projectId) {
101
+ tags = ['vercel.project_id', projectId]
102
+ }
103
+
104
+ const environment = getEnvironmentVariable('VERCEL_ENV')
105
+ if (environment) {
106
+ tags ??= []
107
+ tags.push('vercel.environment', environment)
108
+ }
109
+
110
+ const region = getEnvironmentVariable('VERCEL_REGION')
111
+ if (region) {
112
+ tags ??= []
113
+ tags.push('vercel.region', region)
114
+ }
115
+
116
+ return tags
117
+ }
118
+
119
+ module.exports = {
120
+ getVercelPlatformTags,
121
+ registerVercelTelemetryRetention,
122
+ }
@@ -45,44 +45,61 @@ function isInServerlessEnvironment () {
45
45
  /**
46
46
  * Gets tags describing the serverless platform where the tracer is running.
47
47
  *
48
+ * @param {{ isVercel: boolean }} [platform] Detected serverless platform.
48
49
  * @returns {string[]|undefined}
49
50
  */
50
- function getServerlessPlatformTags () {
51
- if (getEnvironmentVariable('VERCEL') === '1') {
52
- return getVercelPlatformTags()
51
+ function getServerlessPlatformTags (platform = getServerlessPlatform()) {
52
+ if (platform.isVercel) {
53
+ return require('./serverless/vercel').getVercelPlatformTags()
53
54
  }
54
55
  }
55
56
 
56
57
  /**
57
- * @returns {string[]|undefined}
58
+ * Detects the serverless platform once while configuration is built.
59
+ * @returns {{ isVercel: boolean }}
58
60
  */
59
- function getVercelPlatformTags () {
60
- let tags
61
- const projectId = getEnvironmentVariable('VERCEL_PROJECT_ID')
62
- if (projectId) {
63
- tags = ['vercel.project_id', projectId]
64
- }
61
+ function getServerlessPlatform () {
62
+ return { isVercel: getEnvironmentVariable('VERCEL') === '1' }
63
+ }
65
64
 
66
- const environment = getEnvironmentVariable('VERCEL_ENV')
67
- if (environment) {
68
- tags ??= []
69
- tags.push('vercel.environment', environment)
70
- }
65
+ /**
66
+ * Whether the current platform can retain an invocation for telemetry delivery.
67
+ *
68
+ * Add future serverless platforms here as they gain an equivalent retention hook.
69
+ * @returns {boolean}
70
+ */
71
+ function supportsServerlessTelemetryRetention () {
72
+ return getServerlessPlatform().isVercel
73
+ }
71
74
 
72
- const region = getEnvironmentVariable('VERCEL_REGION')
73
- if (region) {
74
- tags ??= []
75
- tags.push('vercel.region', region)
75
+ /**
76
+ * Creates delivery tracking for platforms with an invocation retention hook.
77
+ */
78
+ function createServerlessDeliveryTracker () {
79
+ if (supportsServerlessTelemetryRetention()) {
80
+ return new (require('./serverless/telemetry-delivery-tracker'))()
76
81
  }
82
+ }
77
83
 
78
- return tags
84
+ /**
85
+ * Registers the lifecycle adapter selected by the detected serverless platform.
86
+ * @param {{ flushAll?: (done: () => void) => void }} tracer
87
+ */
88
+ function initializeServerlessTelemetry (tracer) {
89
+ if (supportsServerlessTelemetryRetention()) {
90
+ return require('./serverless/vercel').registerVercelTelemetryRetention(tracer)
91
+ }
79
92
  }
80
93
 
81
94
  module.exports = {
82
95
  getServerlessPlatformTags,
96
+ getServerlessPlatform,
97
+ supportsServerlessTelemetryRetention,
98
+ createServerlessDeliveryTracker,
83
99
  getIsGCPFunction,
84
100
  getIsAzureFunction,
85
101
  enableGCPPubSubPushSubscription,
86
102
  getIsFlexConsumptionAzureFunction,
103
+ initializeServerlessTelemetry,
87
104
  IS_SERVERLESS: isInServerlessEnvironment(),
88
105
  }
@@ -15,6 +15,7 @@ const {
15
15
  } = require('../../../ext/tags')
16
16
  const { ORIGIN_KEY, TOP_LEVEL_KEY, SVC_SRC_KEY, GRPC_STATUS_NAMES } = require('./constants')
17
17
  const id = require('./id')
18
+ const log = require('./log')
18
19
 
19
20
  const GRPC_STATUS_CODE_MAP = Object.fromEntries(GRPC_STATUS_NAMES.map((name, i) => [name, String(i)]))
20
21
  const ZERO_ID = id('0')
@@ -235,6 +236,18 @@ class SpanStatsProcessor {
235
236
  }
236
237
 
237
238
  onInterval () {
239
+ this.#flush()
240
+ }
241
+
242
+ /**
243
+ * Drains pending span statistics and waits for their export.
244
+ * @param {Function} [done]
245
+ */
246
+ forceFlush (done) {
247
+ this.#flush(done)
248
+ }
249
+
250
+ #flush (done) {
238
251
  const drained = this.#drainBuckets()
239
252
 
240
253
  if (this.enabled && !this.otlpExporter) {
@@ -248,10 +261,34 @@ class SpanStatsProcessor {
248
261
  RuntimeID: this.tags['runtime-id'],
249
262
  Sequence: ++this.sequence,
250
263
  ProcessTags: processTags.serialized,
251
- })
264
+ }, done)
252
265
  } else if (this.otlpExporter && drained.length > 0) {
253
- this.otlpExporter.export(drained, this.bucketSizeNs)
254
- }
266
+ if (typeof this.otlpExporter.flush === 'function' && done) {
267
+ // Snapshot requests already in flight before starting this boundary
268
+ // export, so a later invocation cannot extend this lifecycle barrier.
269
+ let pending = 2
270
+ const complete = () => {
271
+ if (--pending === 0) done()
272
+ }
273
+ try {
274
+ this.otlpExporter.flush(complete)
275
+ } catch (error) {
276
+ log.error('Failed to flush OTLP span stats:', error)
277
+ complete()
278
+ }
279
+ try {
280
+ this.otlpExporter.export(drained, this.bucketSizeNs, complete)
281
+ } catch (error) {
282
+ log.error('Failed to export OTLP span stats:', error)
283
+ complete()
284
+ }
285
+ } else {
286
+ this.otlpExporter.export(drained, this.bucketSizeNs, done)
287
+ }
288
+ } else if (this.otlpExporter) {
289
+ if (typeof this.otlpExporter.flush === 'function') this.otlpExporter.flush(done)
290
+ else done?.()
291
+ } else done?.()
255
292
  }
256
293
 
257
294
  onSpanFinished (span) {
@@ -13,6 +13,7 @@ const { isError } = require('./util')
13
13
  const { setStartupLogConfig } = require('./startup-log')
14
14
  const { DataStreamsCheckpointer, DataStreamsManager, DataStreamsProcessor } = require('./datastreams')
15
15
  const { IS_SERVERLESS } = require('./serverless')
16
+ const { flushServerlessTelemetry } = require('./flush')
16
17
  const log = require('./log')
17
18
  // Always-on writer (console.warn), not the channel-gated `log`: these surface regardless of
18
19
  // DD_TRACE_DEBUG.
@@ -150,6 +151,27 @@ class DatadogTracer extends Tracer {
150
151
  this._dataStreamsProcessor.setUrl(url)
151
152
  }
152
153
 
154
+ /**
155
+ * Flushes every configured telemetry pipeline for a serverless lifecycle.
156
+ * @param {Function} [done] Called after every configured export completes
157
+ * @param {{ timeout?: number }} [options] Bounds this flush operation.
158
+ */
159
+ flushAll (done, options) {
160
+ const traceExporter = this._exporter
161
+ const spanStats = this._processor?._stats
162
+ const traceFlusher = typeof traceExporter?.flush === 'function'
163
+ ? callback => traceExporter.flush(callback)
164
+ : undefined
165
+ const spanStatsFlusher = typeof spanStats?.forceFlush === 'function'
166
+ ? callback => spanStats.forceFlush(callback)
167
+ : undefined
168
+
169
+ flushServerlessTelemetry(done, options, {
170
+ trace: traceFlusher,
171
+ spanStats: spanStatsFlusher,
172
+ })
173
+ }
174
+
153
175
  scope () {
154
176
  return this._scope
155
177
  }