dd-trace 6.9.0 → 6.11.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 (175) hide show
  1. package/README.md +0 -17
  2. package/ci/vitest-no-worker-init-setup.mjs +112 -10
  3. package/index.d.ts +150 -3
  4. package/initialize.mjs +28 -17
  5. package/openfeature.js +2 -2
  6. package/package.json +14 -10
  7. package/packages/datadog-esbuild/index.js +0 -34
  8. package/packages/datadog-instrumentations/src/anthropic.js +210 -15
  9. package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
  10. package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
  11. package/packages/datadog-instrumentations/src/bunyan.js +3 -16
  12. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +111 -6
  13. package/packages/datadog-instrumentations/src/cucumber.js +26 -11
  14. package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
  15. package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
  16. package/packages/datadog-instrumentations/src/cypress.js +2 -0
  17. package/packages/datadog-instrumentations/src/fastify.js +2 -0
  18. package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
  19. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  20. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +634 -0
  21. package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +2 -1
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +81 -0
  24. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
  25. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +83 -1
  26. package/packages/datadog-instrumentations/src/jest.js +26 -18
  27. package/packages/datadog-instrumentations/src/mariadb.js +278 -21
  28. package/packages/datadog-instrumentations/src/mocha/main.js +540 -56
  29. package/packages/datadog-instrumentations/src/mocha/utils.js +132 -25
  30. package/packages/datadog-instrumentations/src/mysql.js +45 -15
  31. package/packages/datadog-instrumentations/src/mysql2.js +119 -18
  32. package/packages/datadog-instrumentations/src/path-to-regexp.js +67 -1
  33. package/packages/datadog-instrumentations/src/pg.js +116 -92
  34. package/packages/datadog-instrumentations/src/playwright-reporter.js +99 -0
  35. package/packages/datadog-instrumentations/src/playwright.js +149 -2
  36. package/packages/datadog-instrumentations/src/router.js +2 -0
  37. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -4
  38. package/packages/datadog-instrumentations/src/vitest-main.js +180 -20
  39. package/packages/datadog-instrumentations/src/vitest-util.js +64 -5
  40. package/packages/datadog-instrumentations/src/vitest-worker.js +150 -25
  41. package/packages/datadog-instrumentations/src/webdriverio.js +31 -17
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +12 -6
  43. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
  44. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
  45. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +4 -13
  46. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -5
  47. package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
  48. package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
  49. package/packages/datadog-plugin-bunyan/src/index.js +5 -8
  50. package/packages/datadog-plugin-cucumber/src/index.js +11 -0
  51. package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
  52. package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
  53. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +82 -30
  54. package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
  55. package/packages/datadog-plugin-cypress/src/index.js +12 -6
  56. package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
  57. package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
  58. package/packages/datadog-plugin-electron/src/index.js +2 -0
  59. package/packages/datadog-plugin-fs/src/index.js +1 -1
  60. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +31 -17
  61. package/packages/datadog-plugin-jest/src/index.js +11 -4
  62. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +2 -3
  63. package/packages/datadog-plugin-kafkajs/src/consumer.js +2 -3
  64. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -2
  65. package/packages/datadog-plugin-kafkajs/src/utils.js +65 -7
  66. package/packages/datadog-plugin-mocha/src/index.js +606 -40
  67. package/packages/datadog-plugin-mysql/src/index.js +37 -0
  68. package/packages/datadog-plugin-nats/src/index.js +1 -1
  69. package/packages/datadog-plugin-next/src/index.js +1 -19
  70. package/packages/datadog-plugin-openai/src/tracing.js +41 -30
  71. package/packages/datadog-plugin-openai-agents/src/integration.js +279 -69
  72. package/packages/datadog-plugin-openai-agents/src/processor.js +16 -4
  73. package/packages/datadog-plugin-pg/src/index.js +1 -0
  74. package/packages/datadog-plugin-playwright/src/index.js +42 -13
  75. package/packages/datadog-plugin-vitest/src/index.js +14 -20
  76. package/packages/datadog-plugin-ws/src/util.js +2 -1
  77. package/packages/datadog-webpack/index.js +0 -11
  78. package/packages/dd-trace/index.js +0 -1
  79. package/packages/dd-trace/src/agent/info.js +6 -5
  80. package/packages/dd-trace/src/aiguard/client.js +100 -1
  81. package/packages/dd-trace/src/aiguard/errors.js +41 -0
  82. package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
  83. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
  84. package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
  85. package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
  86. package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
  87. package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
  88. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
  89. package/packages/dd-trace/src/aiguard/sdk.js +22 -278
  90. package/packages/dd-trace/src/appsec/api_security/normalized-route.js +453 -0
  91. package/packages/dd-trace/src/appsec/handlers/http-request.js +13 -0
  92. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
  93. package/packages/dd-trace/src/carrier.js +356 -0
  94. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
  95. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +27 -3
  96. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +22 -6
  97. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
  98. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
  99. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +22 -6
  100. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
  101. package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
  102. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
  103. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
  104. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -1
  105. package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
  106. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
  107. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +6 -2
  108. package/packages/dd-trace/src/config/generated-config-types.d.ts +7 -0
  109. package/packages/dd-trace/src/config/index.js +13 -2
  110. package/packages/dd-trace/src/config/major-overrides.js +7 -0
  111. package/packages/dd-trace/src/config/supported-configurations.json +24 -0
  112. package/packages/dd-trace/src/datastreams/context.js +6 -1
  113. package/packages/dd-trace/src/datastreams/index.js +2 -0
  114. package/packages/dd-trace/src/datastreams/manager.js +14 -3
  115. package/packages/dd-trace/src/datastreams/pathway.js +23 -22
  116. package/packages/dd-trace/src/datastreams/processor.js +16 -13
  117. package/packages/dd-trace/src/datastreams/size.js +5 -0
  118. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -2
  119. package/packages/dd-trace/src/exporters/agent/writer.js +49 -9
  120. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
  121. package/packages/dd-trace/src/exporters/common/writer.js +11 -6
  122. package/packages/dd-trace/src/llmobs/constants/tags.js +5 -0
  123. package/packages/dd-trace/src/llmobs/experiments/client.js +163 -5
  124. package/packages/dd-trace/src/llmobs/experiments/dataset.js +241 -87
  125. package/packages/dd-trace/src/llmobs/experiments/experiment.js +280 -20
  126. package/packages/dd-trace/src/llmobs/experiments/index.js +58 -59
  127. package/packages/dd-trace/src/llmobs/experiments/noop.js +71 -3
  128. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  129. package/packages/dd-trace/src/llmobs/experiments/util.js +41 -5
  130. package/packages/dd-trace/src/llmobs/index.js +34 -2
  131. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
  132. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
  133. package/packages/dd-trace/src/llmobs/span_processor.js +16 -0
  134. package/packages/dd-trace/src/llmobs/tagger.js +96 -0
  135. package/packages/dd-trace/src/llmobs/util.js +78 -0
  136. package/packages/dd-trace/src/noop/proxy.js +3 -4
  137. package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
  138. package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
  139. package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
  140. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
  141. package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
  142. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
  143. package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
  144. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
  145. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
  146. package/packages/dd-trace/src/opentracing/propagation/text_map.js +126 -120
  147. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +4 -8
  148. package/packages/dd-trace/src/opentracing/tracer.js +5 -2
  149. package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
  150. package/packages/dd-trace/src/plugin_manager.js +5 -3
  151. package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
  152. package/packages/dd-trace/src/plugins/index.js +1 -0
  153. package/packages/{datadog-plugin-jest/src/util.js → dd-trace/src/plugins/util/jest.js} +5 -65
  154. package/packages/dd-trace/src/plugins/util/test.js +93 -14
  155. package/packages/dd-trace/src/plugins/util/web.js +56 -2
  156. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  157. package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
  158. package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
  159. package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
  160. package/packages/dd-trace/src/proxy.js +38 -29
  161. package/packages/dd-trace/src/ritm.js +9 -2
  162. package/packages/dd-trace/src/serverless.js +37 -0
  163. package/packages/dd-trace/src/span_format.js +1 -0
  164. package/packages/dd-trace/src/span_stats.js +30 -3
  165. package/packages/dd-trace/src/storage-channels.js +86 -0
  166. package/packages/dd-trace/src/tracer.js +9 -2
  167. package/packages/dd-trace/src/tracer_metadata.js +14 -1
  168. package/packages/dd-trace/src/web-tags-cache.js +132 -0
  169. package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
  170. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
  171. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
  172. package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
  173. package/packages/dd-trace/src/feature-registry.js +0 -29
  174. package/packages/dd-trace/src/openfeature/register.js +0 -35
  175. package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
@@ -0,0 +1,387 @@
1
+ 'use strict'
2
+
3
+ // OTEP-4947 Thread Local Context Record writer integration.
4
+ //
5
+ // Hooks into the active-span lifecycle (storage:enter, span:finish,
6
+ // span:tags:update channels) and mirrors the active trace ID, span ID
7
+ // and current endpoint into a thread-local record that an
8
+ // out-of-process eBPF reader can discover via the
9
+ // otel_thread_ctx_nodejs_v1 TLS symbol exported by the @datadog/pprof
10
+ // addon.
11
+ //
12
+ // Linux + AsyncContextFrame only. Degrades to a no-op start() on
13
+ // platforms or Node versions where the writer can't operate; the
14
+ // caller is expected to gate activation via the DD_TRACE_OTEL_CTX_ENABLED
15
+ // env var (or future config flag).
16
+ //
17
+ // Covers both the dd-trace API and the OpenTelemetry API when the
18
+ // latter is used through dd-trace-js's TracerProvider: an OTel Span
19
+ // wraps a DatadogSpan (packages/dd-trace/src/opentelemetry/span.js)
20
+ // and the OTel ContextManager activates spans via
21
+ // storage('legacy').run({ span: ddSpan }, ...), so the active span
22
+ // our writer reads from legacy storage is the same DatadogSpan in
23
+ // both cases. The OTel-visible trace/span IDs come from the same
24
+ // _ddContext.toTraceId/toSpanId calls we use here.
25
+
26
+ const { isMainThread, threadId } = require('worker_threads')
27
+
28
+ const { isACFActive } = require('../../datadog-core/src/storage')
29
+ const log = require('./log')
30
+ const {
31
+ enterCh,
32
+ spanFinishCh,
33
+ getActiveSpan,
34
+ ensureChannelsActivated,
35
+ } = require('./storage-channels')
36
+ const {
37
+ finalEndpoint,
38
+ getStartedSpans,
39
+ } = require('./profiling/webspan-utils')
40
+ const webTagsCache = require('./web-tags-cache')
41
+
42
+ // OTEP-4947 duplicates are last-wins, so appending a later endpoint value would
43
+ // correctly overwrite an earlier one — but an out-of-process reader sampling the
44
+ // record mid-request would still see the interim value (e.g. a bare `GET`) as
45
+ // the endpoint for whatever it was sampling. So a record is built without an
46
+ // endpoint until finalEndpoint (profiling/webspan-utils.js) reports the value
47
+ // settled, and gets one appended when webTagsCache announces that moment.
48
+ //
49
+ // Records still waiting on that announcement, grouped by the web-server tag bag
50
+ // they are waiting on. Every span under one request resolves to that same bag
51
+ // (webTagsCache maps a span to its nearest web-server ancestor's tags) and the
52
+ // endpoint belongs to the request rather than to any one span, so a single
53
+ // resolution fills in every record built before it — the request span's own and
54
+ // each descendant's. Descendants are the reason this can't just live on the
55
+ // per-span cache entry: the tags:update that carries the route is published for
56
+ // the ancestor, which has no way to enumerate them.
57
+ //
58
+ // Keyed by the tag bag itself because that is all a descendant has at build
59
+ // time, and the announcement carries the span whose getTags() is that same
60
+ // object. Tag-bag identity is stable for live spans — only spanleak.js replaces
61
+ // a bag, on already-expired spans under manual-GC debug mode — and webTagsCache
62
+ // already depends on it, holding bags the wall profiler dereferences much later.
63
+ //
64
+ // Entries are dropped as soon as the endpoint resolves, or die with the tag bag
65
+ // if it never does.
66
+ const pendingEndpoints = new WeakMap()
67
+
68
+ // Positional attribute layout. The local root span ID stays at index 0 by
69
+ // convention (mirrors libdatadog's libdd-otel-thread-ctx, where
70
+ // `local_root_span_id` is always the first entry in
71
+ // `threadlocal.attribute_key_map`), encoded as a 16-character lowercase
72
+ // hex string. Endpoint, thread name, and thread id follow. Adding more
73
+ // means assigning the next index and updating ATTRIBUTE_KEYS
74
+ // accordingly.
75
+ const LOCAL_ROOT_SPAN_ID_IDX = 0
76
+ const ENDPOINT_IDX = 1
77
+ const THREAD_NAME_IDX = 2
78
+ const THREAD_ID_IDX = 3
79
+
80
+ // The dd-trace-js-supplied subset of the OTEP-4719 attribute_key_map
81
+ // (the implicit `datadog.local_root_span_id` at wire index 0 is
82
+ // prepended by libdatadog when it publishes the process context, so it
83
+ // is NOT listed here). Index N here corresponds to wire key index N+1.
84
+ // Kept in sync with the positional indices above.
85
+ // Also see https://docs.google.com/document/d/1IwjjVJzEChcFPcnVV2N5Kkjg-4_Q4v4Q3ojpxntbdvY/edit?pli=1&tab=t.efaosgjya44c#bookmark=id.700gvw31vb7h
86
+ const ATTRIBUTE_KEYS = [
87
+ 'datadog.trace_endpoint',
88
+ 'datadog.thread_name',
89
+ 'datadog.thread_id',
90
+ ]
91
+
92
+ // Stable per-thread values baked into every record. Same shape as the
93
+ // profiler's `eventLoopThreadName` in profiling/profilers/shared.js.
94
+ const THREAD_NAME = (isMainThread ? 'Main' : `Worker #${threadId}`) + ' Event Loop'
95
+ const THREAD_ID = String(threadId)
96
+
97
+ // Cache slot on span objects. One ThreadContext is built per span on first
98
+ // activation and re-installed across every async-context frame that
99
+ // re-enters the span — V8's AsyncContextFrame inherits the JS
100
+ // reference verbatim, and the context's record buffer is mutated in
101
+ // place by appendAttributes, so all frames observe the same record.
102
+ //
103
+ // Fields:
104
+ // context: ThreadContext from @datadog/pprof.otelThreadCtx — built the first
105
+ // time onEnter activates the span, and cleared on span finish, which
106
+ // is how a pendingEndpoints waiter recognizes a record it must no
107
+ // longer append to.
108
+ const CachedSym = Symbol('OtelThreadCtx.cached')
109
+
110
+ let started = false
111
+ let ThreadContext
112
+ let getContext
113
+ let clearContext
114
+
115
+ function getOrBuildContext (span) {
116
+ let cached = span[CachedSym]
117
+ if (cached !== undefined && cached.context !== undefined) return cached.context
118
+ const spanContext = span.context()
119
+ const traceId = Uint8Array.from(Buffer.from(spanContext.toTraceId(true), 'hex'))
120
+ const spanId = Uint8Array.from(Buffer.from(spanContext.toSpanId(true), 'hex'))
121
+ // Local root span: the first entry in the trace's started-spans list, or
122
+ // this span itself when it IS the root. Encoded as 16-char lowercase hex
123
+ // per the libdatadog convention.
124
+ const startedSpans = getStartedSpans(spanContext)
125
+ const rootContext = startedSpans.length ? startedSpans[0].context() : spanContext
126
+ // Only write the endpoint when its value has settled; otherwise leave a hole
127
+ // and wait for webTagsCache to announce that it has.
128
+ const webTags = webTagsCache.getCachedWebTags(span)
129
+ const endpoint = finalEndpoint(webTags)
130
+ const attrs = []
131
+ attrs[LOCAL_ROOT_SPAN_ID_IDX] = rootContext.toSpanId(true)
132
+ if (endpoint !== undefined) attrs[ENDPOINT_IDX] = endpoint
133
+ attrs[THREAD_NAME_IDX] = THREAD_NAME
134
+ attrs[THREAD_ID_IDX] = THREAD_ID
135
+ if (cached === undefined) {
136
+ cached = {}
137
+ span[CachedSym] = cached
138
+ }
139
+ cached.context = new ThreadContext(traceId, spanId, attrs)
140
+ if (endpoint === undefined) awaitEndpoint(webTags, cached)
141
+ return cached.context
142
+ }
143
+
144
+ // Enlist a record built without an endpoint, so the resolution announcement for
145
+ // its request fills it in.
146
+ function awaitEndpoint (webTags, cached) {
147
+ // No web-server span in this span's ancestry, so no endpoint is coming. Should
148
+ // one appear later, webTagsCache announces that separately — see
149
+ // onWebTagsResolved.
150
+ if (webTags === undefined) return
151
+ const waiting = pendingEndpoints.get(webTags)
152
+ if (waiting === undefined) {
153
+ pendingEndpoints.set(webTags, [cached])
154
+ } else {
155
+ waiting.push(cached)
156
+ }
157
+ }
158
+
159
+ function appendEndpoint (context, endpoint) {
160
+ // Append in place. The record buffer is shared across every async-context
161
+ // frame holding this context, so the endpoint becomes visible everywhere at
162
+ // once.
163
+ const append = []
164
+ append[ENDPOINT_IDX] = endpoint
165
+ context.appendAttributes(append)
166
+ }
167
+
168
+ function onEnter () {
169
+ if (!started) return
170
+ const span = getActiveSpan()
171
+ if (!span) {
172
+ clearContext()
173
+ return
174
+ }
175
+ const context = getOrBuildContext(span)
176
+ // Skip if this CPED already holds the same context. Same allocation-churn
177
+ // fix as the wall profiler in dd-trace-js#8638.
178
+ if (getContext() === context) return
179
+ context.enter()
180
+ }
181
+
182
+ function onSpanFinished (span) {
183
+ if (!started) return
184
+ const cached = span[CachedSym]
185
+ if (cached === undefined) return
186
+ span[CachedSym] = undefined
187
+ const context = cached.context
188
+ if (context === undefined) return
189
+ // The span is over, so its record must stop presenting itself as an active
190
+ // thread context to an out-of-process reader. Invalidating the record covers
191
+ // every async-context frame still holding this same context reference —
192
+ // sibling frames, and continuations the span scheduled before it finished.
193
+ // Detaching only the current frame wouldn't: with enterWith-style activation
194
+ // (sticky storage) no storage:enter fires in those inherited frames to
195
+ // overwrite the record, and in ACF mode there is no `before` hook either, so
196
+ // they would keep exposing the finished span's IDs.
197
+ context.invalidate()
198
+ // Marks the record dead for any pendingEndpoints waiter still holding this
199
+ // entry, which must not append to an invalidated record.
200
+ cached.context = undefined
201
+ // Also detach from the current frame when it is the holder, so the record
202
+ // becomes unreachable and collectable instead of lingering as an invalid one
203
+ // for the rest of the frame's life.
204
+ if (getContext() === context) clearContext()
205
+ // Dropping the cache entry means a later storage:enter naming this same
206
+ // finished span builds a fresh, valid record for it — which is deliberate, and
207
+ // not the staleness invalidate() above exists to prevent. The two cases differ
208
+ // in what the tracer told us: an inherited frame is a frame we never heard
209
+ // about again, while an enter is the tracer affirmatively naming the active
210
+ // span, finished or not. Work running under a finished request span still
211
+ // belongs to that request, so a reader should attribute it there; the wall
212
+ // profiler decides the same way, dropping its per-span context on finish and
213
+ // rebuilding it on re-entry (profiling/profilers/wall.js). Clearing instead
214
+ // would strip trace and endpoint attribution off that work entirely.
215
+ }
216
+
217
+ // The endpoint of a request has settled: fill in every record built while it was
218
+ // still unresolved. `span` is the web-server span, so its tag bag is the key its
219
+ // descendants enlisted under.
220
+ function onEndpointResolved (span) {
221
+ if (!started) return
222
+ const webTags = span.context().getTags()
223
+ const waiting = pendingEndpoints.get(webTags)
224
+ if (waiting === undefined) return
225
+ const endpoint = finalEndpoint(webTags)
226
+ // webTagsCache only announces settled endpoints, so this is belt and braces.
227
+ if (endpoint === undefined) return
228
+ pendingEndpoints.delete(webTags)
229
+ for (const cached of waiting) {
230
+ if (cached.context !== undefined) appendEndpoint(cached.context, endpoint)
231
+ }
232
+ }
233
+
234
+ // A span whose record was built before it looked like a web-server span at all
235
+ // has just been recognized as one, so it has no endpoint and never enlisted for
236
+ // this request. Its ancestry can't have changed, only its own tags, so this is
237
+ // only ever about the announced span's own record.
238
+ function onWebTagsResolved (span) {
239
+ if (!started) return
240
+ const cached = span[CachedSym]
241
+ if (cached === undefined || cached.context === undefined) return
242
+ const webTags = webTagsCache.getCachedWebTags(span)
243
+ const endpoint = finalEndpoint(webTags)
244
+ if (endpoint === undefined) {
245
+ awaitEndpoint(webTags, cached)
246
+ } else {
247
+ appendEndpoint(cached.context, endpoint)
248
+ }
249
+ }
250
+
251
+ // Every otelThreadCtx member the writer calls, checked before it starts. Anything
252
+ // missing would otherwise surface from inside a diagnostic-channel subscriber on
253
+ // a hot path, where an exception lands in application code: a ThreadContext
254
+ // without invalidate() would throw out of onSpanFinished and up through
255
+ // DatadogSpan#finish() the first time an activated span finished.
256
+ //
257
+ // Returns the name of the first missing member, or undefined when the surface is
258
+ // complete.
259
+ function missingApiMember (ns) {
260
+ if (!ns) return 'otelThreadCtx'
261
+ for (const name of ['ThreadContext', 'getContext', 'clearContext', 'getProcessContextAttributes']) {
262
+ if (typeof ns[name] !== 'function') return name
263
+ }
264
+ for (const name of ['appendAttributes', 'enter', 'invalidate']) {
265
+ if (typeof ns.ThreadContext.prototype[name] !== 'function') return `ThreadContext.prototype.${name}`
266
+ }
267
+ }
268
+
269
+ // Install and detach one throwaway context, to establish that this process can
270
+ // actually do it before any span depends on it.
271
+ //
272
+ // @datadog/pprof decides whether AsyncContextFrame is available by inspecting
273
+ // `process.execArgv`, and throws from enter() when it concludes it isn't. That
274
+ // disagrees with the feature detection behind `isACFActive` whenever the flag
275
+ // reached Node by another route: `NODE_OPTIONS=--experimental-async-context-frame`
276
+ // is accepted on Node 22 and 23 and leaves `execArgv` empty, and a worker thread
277
+ // created with an explicit `execArgv` loses it too. Since our subscribers run
278
+ // inline with `storage.enterWith`, letting that throw would put the exception in
279
+ // application code on the first span activation, so find out here instead, where
280
+ // declining to start is still an option.
281
+ function canInstallContext (ns) {
282
+ try {
283
+ // Zero-filled ids: the record is only readable while it is installed, which
284
+ // is for the length of this function, and an all-zero trace id identifies
285
+ // nothing.
286
+ const probe = new ns.ThreadContext(new Uint8Array(16), new Uint8Array(8))
287
+ probe.enter()
288
+ ns.clearContext()
289
+ return true
290
+ } catch (e) {
291
+ log.warn('OTEP-4947 thread context writer: @datadog/pprof cannot install a thread context', e)
292
+ return false
293
+ }
294
+ }
295
+
296
+ function start () {
297
+ if (started) return true
298
+ if (process.platform !== 'linux') {
299
+ log.debug('OTEP-4947 thread context writer: not on Linux, skipping')
300
+ return false
301
+ }
302
+ if (!isACFActive) {
303
+ log.warn(
304
+ 'OTEP-4947 thread context writer requires AsyncContextFrame to be active; not enabling'
305
+ )
306
+ return false
307
+ }
308
+ let pprofMod
309
+ try {
310
+ pprofMod = require('@datadog/pprof')
311
+ } catch (e) {
312
+ log.warn('OTEP-4947 thread context writer: @datadog/pprof unavailable', e)
313
+ return false
314
+ }
315
+ const ns = pprofMod.otelThreadCtx
316
+ const missing = missingApiMember(ns)
317
+ if (missing !== undefined) {
318
+ log.warn(
319
+ 'OTEP-4947 thread context writer: installed @datadog/pprof does not expose the otelThreadCtx API (missing %s)',
320
+ missing
321
+ )
322
+ return false
323
+ }
324
+ if (!canInstallContext(ns)) return false
325
+ ThreadContext = ns.ThreadContext
326
+ getContext = ns.getContext
327
+ clearContext = ns.clearContext
328
+
329
+ ensureChannelsActivated(isACFActive)
330
+ enterCh.subscribe(onEnter)
331
+ spanFinishCh.subscribe(onSpanFinished)
332
+ // Endpoint updates come from the shared web-tags cache's transition channels
333
+ // rather than from `dd-trace:span:tags:update` directly: the cache already
334
+ // subscribes to that channel and derives both transitions we care about, so
335
+ // this keeps one subscriber on the hot path instead of two, and removes any
336
+ // ordering dependency on the cache having handled an update before we read it.
337
+ webTagsCache.endpointResolvedCh.subscribe(onEndpointResolved)
338
+ webTagsCache.resolvedCh.subscribe(onWebTagsResolved)
339
+ // Turn on the shared web-tags cache's own tagsUpdate subscription for as
340
+ // long as this writer runs — see web-tags-cache.js.
341
+ webTagsCache.activate()
342
+
343
+ started = true
344
+ log.info('OTEP-4947 thread context writer started')
345
+ return true
346
+ }
347
+
348
+ // Snapshot of the OTEP-4719 process-context attributes describing this
349
+ // writer's on-the-wire record schema — schema-version string, the caller-side
350
+ // attribute key map, and the V8 layout constants a reader needs to walk from
351
+ // our discovery TLS symbol into the record. Returned in the shape libdatadog's
352
+ // napi ThreadLocalMetadata expects:
353
+ //
354
+ // { attributeKeys, schemaVersion, extraAttributes: [{ key, intValue|stringValue }] }
355
+ //
356
+ // Returns undefined unless start() succeeded. Callers should treat that as
357
+ // "no threadlocal block" (equivalent to the flag being off) — otherwise we'd
358
+ // publish process-context metadata advertising a decodable OTEP-4947 stream
359
+ // while no writer is producing records.
360
+ function getThreadLocalMetadata () {
361
+ if (!started) return
362
+ // start() verified @datadog/pprof.otelThreadCtx exposes the full API
363
+ // surface (ThreadContext/getContext/clearContext/getProcessContextAttributes),
364
+ // so this require is a cached lookup and the method is guaranteed present.
365
+ const pca = require('@datadog/pprof').otelThreadCtx
366
+ .getProcessContextAttributes(ATTRIBUTE_KEYS)
367
+ const extraAttributes = []
368
+ for (const [key, value] of Object.entries(pca)) {
369
+ if (key === 'threadlocal.schema_version' || key === 'threadlocal.attribute_key_map') continue
370
+ if (typeof value === 'number' && Number.isInteger(value)) {
371
+ extraAttributes.push({ key, intValue: value })
372
+ } else if (typeof value === 'string') {
373
+ extraAttributes.push({ key, stringValue: value })
374
+ } else {
375
+ throw new TypeError(
376
+ `OTEP-4947 process-context attribute ${JSON.stringify(key)} has unsupported value type: ${typeof value}`
377
+ )
378
+ }
379
+ }
380
+ return {
381
+ attributeKeys: [...pca['threadlocal.attribute_key_map']],
382
+ schemaVersion: pca['threadlocal.schema_version'],
383
+ extraAttributes,
384
+ }
385
+ }
386
+
387
+ module.exports = { start, ATTRIBUTE_KEYS, getThreadLocalMetadata }
@@ -61,9 +61,9 @@ function maybeEnable (Plugin) {
61
61
  function getEnabled (Plugin) {
62
62
  const envName = `DD_TRACE_${Plugin.id.toUpperCase()}_ENABLED`
63
63
  // skipDefault: only an explicitly configured value should drive enablement here. A registered
64
- // default of `false` (e.g. an experimental plugin like `nats`) must not be read as an explicit
64
+ // default of `false` (e.g. an opt-in plugin like `nats`) must not be read as an explicit
65
65
  // "disabled via configuration option" — that path both logs a misleading line and nulls the
66
- // plugin class, bypassing the experimental opt-in handled by `loadPlugin`.
66
+ // plugin class, bypassing the opt-in handled by `loadPlugin`.
67
67
  return getValueFromEnvSources(normalizePluginEnvName(envName), true)
68
68
  }
69
69
 
@@ -102,7 +102,7 @@ module.exports = class PluginManager {
102
102
  }
103
103
  const pluginConfig = this._configsByName[name] || {
104
104
  enabled: this._tracerConfig.plugins !== false &&
105
- (!Plugin.experimental || isTrue(getEnabled(Plugin))),
105
+ (!Plugin.optIn || isTrue(getEnabled(Plugin))),
106
106
  }
107
107
 
108
108
  // extracts predetermined configuration from tracer and combines it with plugin-specific config
@@ -165,6 +165,7 @@ module.exports = class PluginManager {
165
165
  codeOriginForSpans,
166
166
  dbmPropagationMode,
167
167
  dsmEnabled,
168
+ DD_TRACE_HTTP_SERVER_ERROR_STATUSES,
168
169
  clientIpEnabled,
169
170
  clientIpHeader,
170
171
  DD_TRACE_MEMCACHED_COMMAND_ENABLED,
@@ -194,6 +195,7 @@ module.exports = class PluginManager {
194
195
  site,
195
196
  url,
196
197
  headers: headerTags || [],
198
+ DD_TRACE_HTTP_SERVER_ERROR_STATUSES,
197
199
  clientIpHeader,
198
200
  DD_TEST_SESSION_NAME,
199
201
  DD_AGENTLESS_LOG_SUBMISSION_ENABLED,
@@ -728,7 +728,12 @@ module.exports = class CiPlugin extends Plugin {
728
728
  this._bufferWorkerTrace(missingTestSuite, trace)
729
729
  return
730
730
  }
731
- this.tracer._exporter.export(trace)
731
+ const exporter = this.tracer._exporter
732
+ if (exporter.exportTraceWithDeferredTestSuite) {
733
+ exporter.exportTraceWithDeferredTestSuite(trace)
734
+ } else {
735
+ exporter.export(trace)
736
+ }
732
737
  }
733
738
 
734
739
  /**
@@ -46,6 +46,7 @@ const plugins = {
46
46
  get amqplib () { return require('../../../datadog-plugin-amqplib/src') },
47
47
  get avsc () { return require('../../../datadog-plugin-avsc/src') },
48
48
  get 'aws-sdk' () { return require('../../../datadog-plugin-aws-sdk/src') },
49
+ get 'browser-bunyan' () { return require('../../../datadog-plugin-browser-bunyan/src') },
49
50
  get bullmq () { return require('../../../datadog-plugin-bullmq/src') },
50
51
  get bunyan () { return require('../../../datadog-plugin-bunyan/src') },
51
52
  get 'cassandra-driver' () { return require('../../../datadog-plugin-cassandra-driver/src') },
@@ -1,10 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { readFileSync } = require('fs')
4
- const { parse } = require('../../../vendor/dist/jest-docblock')
5
-
6
- const { getTestSuitePath } = require('../../dd-trace/src/plugins/util/test')
7
- const log = require('../../dd-trace/src/log')
3
+ const { getTestSuitePath, isMarkedAsUnskippable } = require('./test')
8
4
 
9
5
  /**
10
6
  * There are two ways to call `test.each` in `jest`:
@@ -69,65 +65,11 @@ function getJestTestName (test) {
69
65
  return removeSeedSuffixFromTestName(getRawJestTestName(test))
70
66
  }
71
67
 
72
- const globalDocblockRegExp = /^\s*(\/\*\*?(.|\r?\n)*?\*\/)/
73
- const MAX_COMMENTS_CHECKED = 10
74
-
75
- function isMarkedAsUnskippable (test) {
76
- let testSource
77
-
78
- try {
79
- testSource = readFileSync(test.path, 'utf8')
80
- } catch {
81
- return false
82
- }
83
-
84
- const re = globalDocblockRegExp
85
- re.lastIndex = 0
86
- let commentsChecked = 0
87
-
88
- while (testSource.length) {
89
- const match = re.exec(testSource)
90
- if (!match) break
91
- const comment = match[1]
92
-
93
- let docblocks
94
- try {
95
- docblocks = parse(comment)
96
- } catch {
97
- // Skip unparsable comment and continue scanning
98
- if (commentsChecked++ >= MAX_COMMENTS_CHECKED) {
99
- return false
100
- }
101
- continue
102
- }
103
-
104
- if (docblocks?.datadog) {
105
- try {
106
- // @ts-expect-error The datadog type is defined by us and may only be a string.
107
- return JSON.parse(docblocks.datadog).unskippable
108
- } catch {
109
- // If the @datadog block comment is present but malformed, we'll run the suite
110
- log.warn('@datadog block comment is malformed.')
111
- return true
112
- }
113
- }
114
-
115
- if (commentsChecked++ >= MAX_COMMENTS_CHECKED) {
116
- return false
117
- }
118
-
119
- // To stop as soon as no doc blocks are found, slice the source. That way the
120
- // regexp works by using the `^` anchor. Without it, it would continue
121
- // scanning the rest of the file.
122
- testSource = testSource.slice(match[0].length)
123
- }
124
-
125
- return false
126
- }
127
-
128
68
  function getJestSuitesToRun (skippableSuites, originalTests, rootDir, fallbackRootDir) {
129
69
  const unskippableSuites = {}
130
70
  const forcedToRunSuites = {}
71
+ let hasUnskippableSuites = false
72
+ let hasForcedToRunSuites = false
131
73
 
132
74
  const skippedSuites = []
133
75
  const suitesToRun = []
@@ -147,11 +89,13 @@ function getJestSuitesToRun (skippableSuites, originalTests, rootDir, fallbackRo
147
89
  if (isMarkedAsUnskippable(test)) {
148
90
  suitesToRun.push(test)
149
91
  unskippableSuites[relativePath] = true
92
+ hasUnskippableSuites = true
150
93
  if (fallbackRelativePath !== undefined) {
151
94
  unskippableSuites[fallbackRelativePath] = true
152
95
  }
153
96
  if (skippedSuite !== undefined) {
154
97
  forcedToRunSuites[relativePath] = true
98
+ hasForcedToRunSuites = true
155
99
  if (fallbackRelativePath !== undefined) {
156
100
  forcedToRunSuites[fallbackRelativePath] = true
157
101
  }
@@ -165,9 +109,6 @@ function getJestSuitesToRun (skippableSuites, originalTests, rootDir, fallbackRo
165
109
  }
166
110
  }
167
111
 
168
- const hasUnskippableSuites = Object.keys(unskippableSuites).length > 0
169
- const hasForcedToRunSuites = Object.keys(forcedToRunSuites).length > 0
170
-
171
112
  if (originalTests.length) {
172
113
  // The config object is shared by all tests, so we can just take the first one
173
114
  const [test] = originalTests
@@ -195,6 +136,5 @@ module.exports = {
195
136
  getJestTestName,
196
137
  getRawJestTestName,
197
138
  getJestSuitesToRun,
198
- isMarkedAsUnskippable,
199
139
  removeSeedSuffixFromTestName,
200
140
  }