dd-trace 6.18.0 → 6.19.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/LICENSE-3rdparty.csv +1 -0
- package/ci/vitest-no-worker-init-setup.mjs +22 -12
- package/index.d.ts +3 -2
- package/package.json +6 -6
- package/packages/datadog-instrumentations/src/anthropic.js +111 -9
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +5 -1
- package/packages/datadog-instrumentations/src/cucumber.js +40 -4
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +8 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +2 -0
- package/packages/datadog-instrumentations/src/jest/session-error.js +102 -0
- package/packages/datadog-instrumentations/src/jest.js +3 -10
- package/packages/datadog-instrumentations/src/playwright.js +26 -0
- package/packages/datadog-instrumentations/src/vitest-main.js +4 -1
- package/packages/datadog-instrumentations/src/vitest-worker.js +34 -3
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +83 -29
- package/packages/datadog-plugin-cucumber/src/index.js +19 -4
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +7 -0
- package/packages/datadog-plugin-fetch/src/index.js +3 -0
- package/packages/datadog-plugin-oracledb/src/connection-parser.js +3 -1
- package/packages/datadog-plugin-undici/src/index.js +5 -0
- package/packages/dd-trace/src/aiguard/client.js +38 -19
- package/packages/dd-trace/src/aiguard/integrations/anthropic.js +29 -4
- package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
- package/packages/dd-trace/src/aiguard/integrations/openai.js +20 -56
- package/packages/dd-trace/src/aiguard/integrations/stream.js +60 -0
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +64 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -4
- package/packages/dd-trace/src/config/supported-configurations.json +5 -5
- package/packages/dd-trace/src/constants.js +2 -0
- package/packages/dd-trace/src/llmobs/constants/tags.js +31 -8
- package/packages/dd-trace/src/llmobs/gen-ai-tags.js +127 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +18 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +33 -8
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +77 -41
- package/packages/dd-trace/src/llmobs/plugins/base.js +154 -15
- package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +162 -10
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +65 -16
- package/packages/dd-trace/src/llmobs/plugins/genai/index.js +14 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +34 -35
- package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +10 -0
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +25 -2
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +8 -7
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +13 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +5 -0
- package/packages/dd-trace/src/llmobs/plugins/vertexai.js +7 -0
- package/packages/dd-trace/src/llmobs/prompts/prompt.js +2 -1
- package/packages/dd-trace/src/llmobs/span_processor.js +10 -65
- package/packages/dd-trace/src/llmobs/tagger.js +2 -36
- package/packages/dd-trace/src/opentelemetry/trace/index.js +5 -0
- package/packages/dd-trace/src/plugins/util/test.js +4 -0
- package/packages/dd-trace/src/profiler.js +51 -5
- package/packages/dd-trace/src/profiling/profiler.js +24 -20
- package/packages/dd-trace/src/ritm.js +12 -9
- package/packages/dd-trace/src/span_processor.js +6 -1
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
package/LICENSE-3rdparty.csv
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"component","origin","license","copyright"
|
|
2
2
|
"@apm-js-collab/code-transformer","https://github.com/nodejs/orchestrion-js","['Apache-2.0']","['nodejs']"
|
|
3
|
+
"@bufbuild/protobuf","https://github.com/bufbuild/protobuf-es","['(Apache-2.0 AND BSD-3-Clause)']","['bufbuild']"
|
|
3
4
|
"@datadog/flagging-core","https://github.com/DataDog/openfeature-js-client","['Apache-2.0']","['DataDog']"
|
|
4
5
|
"@datadog/libdatadog","https://github.com/DataDog/libdatadog-nodejs","['Apache-2.0']","['DataDog']"
|
|
5
6
|
"@datadog/libdatadog-extras","https://github.com/DataDog/libdatadog-nodejs","['Apache-2.0']","['Datadog Inc.']"
|
|
@@ -114,14 +114,12 @@ if (isNoWorkerInitActive) {
|
|
|
114
114
|
onTestFinished(() => {
|
|
115
115
|
recordTestAttemptTiming(task, attemptIndex, attemptStart)
|
|
116
116
|
recordRetryErrorCount(task)
|
|
117
|
-
if (
|
|
118
|
-
(isAttemptToFixTest
|
|
119
|
-
attemptIndex === getFinalAttemptIndex(task)
|
|
120
|
-
) {
|
|
117
|
+
if (isEarlyFlakeDetectionTestAttempt || isQuarantinedTest ||
|
|
118
|
+
(isAttemptToFixTest && attemptIndex === getFinalAttemptIndex(task))) {
|
|
121
119
|
if (isAttemptToFixTest || isEarlyFlakeDetectionTestAttempt) {
|
|
122
|
-
recordTestOptimizationStatus(task, attemptIndex,
|
|
120
|
+
recordTestOptimizationStatus(task, attemptIndex, attemptIndex === getFinalAttemptIndex(task))
|
|
123
121
|
}
|
|
124
|
-
switchQuarantinedFinalFailure(task
|
|
122
|
+
switchQuarantinedFinalFailure(task)
|
|
125
123
|
}
|
|
126
124
|
finishRumCorrelation(task, attemptIndex)
|
|
127
125
|
})
|
|
@@ -134,7 +132,7 @@ if (isNoWorkerInitActive) {
|
|
|
134
132
|
recordTestOptimizationStatus(task, attemptIndex)
|
|
135
133
|
}
|
|
136
134
|
if (!restoredEarlyFlakeDetectionResult) {
|
|
137
|
-
switchQuarantinedFinalFailure(task
|
|
135
|
+
switchQuarantinedFinalFailure(task)
|
|
138
136
|
}
|
|
139
137
|
})
|
|
140
138
|
}
|
|
@@ -433,7 +431,10 @@ function recordEarlyFlakeDetectionStatus (task, attemptIndex, onlyIfNewErrors) {
|
|
|
433
431
|
return
|
|
434
432
|
}
|
|
435
433
|
|
|
436
|
-
|
|
434
|
+
// onTestFinished runs before aroundEach fixture teardown. Select the budget
|
|
435
|
+
// when the next attempt records the completed attempt instead.
|
|
436
|
+
const isCompletedAttempt = attemptIndex < task.meta.__ddTestOptCurrentAttemptIndex
|
|
437
|
+
if (isCompletedAttempt && !earlyFlakeDetectionRetriesByTask.has(task)) {
|
|
437
438
|
const retryCount = getEarlyFlakeDetectionRetryCount(task)
|
|
438
439
|
earlyFlakeDetectionRetriesByTask.set(task, retryCount)
|
|
439
440
|
task.repeats = retryCount
|
|
@@ -452,9 +453,15 @@ function recordEarlyFlakeDetectionStatus (task, attemptIndex, onlyIfNewErrors) {
|
|
|
452
453
|
)
|
|
453
454
|
task.meta.__ddTestOptEfdErrorCounts[attemptIndex] = task.result?.errors?.length || 0
|
|
454
455
|
|
|
455
|
-
|
|
456
|
+
// Vitest 5.0.3 retains failures across repeats. Defer EFD's overall failure until
|
|
457
|
+
// the last attempt, while keeping each attempt's actual outcome in the metadata.
|
|
458
|
+
if (attemptIndex < getEarlyFlakeDetectionRetryCountForTask(task) ||
|
|
456
459
|
task.meta.__ddTestOptEfdStatuses.includes('pass')) {
|
|
457
460
|
task.result.state = 'pass'
|
|
461
|
+
} else if (isCompletedAttempt) {
|
|
462
|
+
// A zero retry budget cancels repeats after we suppressed the initial failure.
|
|
463
|
+
task.result.state = 'fail'
|
|
464
|
+
markQuarantinedFailure(task)
|
|
458
465
|
}
|
|
459
466
|
}
|
|
460
467
|
|
|
@@ -525,7 +532,7 @@ function wrapRetryCondition (task) {
|
|
|
525
532
|
shouldRetry = condition(error)
|
|
526
533
|
}
|
|
527
534
|
|
|
528
|
-
if (!shouldRetry
|
|
535
|
+
if (!shouldRetry) {
|
|
529
536
|
const attemptIndex = task.meta.__ddTestOptCurrentAttemptIndex
|
|
530
537
|
recordTestOptimizationStatus(task, attemptIndex)
|
|
531
538
|
markQuarantinedFailure(task)
|
|
@@ -579,7 +586,7 @@ function getFinalAttemptIndex (task) {
|
|
|
579
586
|
return attemptIndex + retriesRemaining + (repeatsRemaining * (retryLimit + 1))
|
|
580
587
|
}
|
|
581
588
|
|
|
582
|
-
function switchQuarantinedFinalFailure (task
|
|
589
|
+
function switchQuarantinedFinalFailure (task) {
|
|
583
590
|
const testSuite = getTestSuite(task)
|
|
584
591
|
const testName = getTestName(task)
|
|
585
592
|
if (
|
|
@@ -590,7 +597,10 @@ function switchQuarantinedFinalFailure (task, attemptIndex) {
|
|
|
590
597
|
return
|
|
591
598
|
}
|
|
592
599
|
|
|
593
|
-
|
|
600
|
+
// Finish the retries in this repetition before suppressing its failure. A later
|
|
601
|
+
// repetition cannot undo a failure that Vitest has already aggregated.
|
|
602
|
+
const retryAttemptIndex = retryAttemptIndexByTask.get(task)?.index || 0
|
|
603
|
+
if (retryAttemptIndex < getRetryLimit(task)) {
|
|
594
604
|
return
|
|
595
605
|
}
|
|
596
606
|
|
package/index.d.ts
CHANGED
|
@@ -2025,8 +2025,9 @@ declare namespace tracer {
|
|
|
2025
2025
|
/**
|
|
2026
2026
|
* Whether to capture LLM Observability spans for this integration. When set to `false`,
|
|
2027
2027
|
* the integration keeps emitting APM spans and propagating trace context, but no LLM
|
|
2028
|
-
* Observability spans are produced. Useful when another
|
|
2029
|
-
* same operation and the payloads would otherwise be
|
|
2028
|
+
* Observability spans and no basic `gen_ai.*` APM tags are produced. Useful when another
|
|
2029
|
+
* integration already captures the same operation and the payloads would otherwise be
|
|
2030
|
+
* stored twice.
|
|
2030
2031
|
* @default true
|
|
2031
2032
|
*/
|
|
2032
2033
|
llmobs?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dd-trace",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.19.0",
|
|
4
4
|
"description": "Datadog APM tracing client for JavaScript",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"typings": "index.d.ts",
|
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
"@eslint/eslintrc": "^3.3.7",
|
|
207
207
|
"@eslint/js": "^10.0.1",
|
|
208
208
|
"@eslint/plugin-kit": "^0.7.3",
|
|
209
|
-
"@datadog/openfeature-node-server": "2.
|
|
209
|
+
"@datadog/openfeature-node-server": "2.4.0",
|
|
210
210
|
"@msgpack/msgpack": "^3.1.3",
|
|
211
211
|
"@openfeature/core": "^1.12.0",
|
|
212
212
|
"@openfeature/server-sdk": "~1.23.0",
|
|
@@ -222,14 +222,14 @@
|
|
|
222
222
|
"bun": "1.4.2",
|
|
223
223
|
"c8": "^12.0.0",
|
|
224
224
|
"codeowners-audit": "^2.9.0",
|
|
225
|
-
"eslint": "^10.
|
|
225
|
+
"eslint": "^10.11.0",
|
|
226
226
|
"eslint-plugin-cypress": "^7.0.2",
|
|
227
227
|
"eslint-plugin-import-x": "^4.16.2",
|
|
228
|
-
"eslint-plugin-jsdoc": "^64.5.
|
|
228
|
+
"eslint-plugin-jsdoc": "^64.5.4",
|
|
229
229
|
"eslint-plugin-mocha": "^12.0.2",
|
|
230
230
|
"eslint-plugin-n": "^18.3.0",
|
|
231
231
|
"eslint-plugin-promise": "^7.3.0",
|
|
232
|
-
"eslint-plugin-regexp": "^3.3.
|
|
232
|
+
"eslint-plugin-regexp": "^3.3.1",
|
|
233
233
|
"eslint-plugin-sonarjs": "^4.2.1",
|
|
234
234
|
"eslint-plugin-unicorn": "^74.0.0",
|
|
235
235
|
"express": "^5.1.0",
|
|
@@ -251,7 +251,7 @@
|
|
|
251
251
|
"node-preload": "^0.2.1",
|
|
252
252
|
"nyc": "^18.0.0",
|
|
253
253
|
"octokit": "^5.0.3",
|
|
254
|
-
"p-limit": "^7.3.
|
|
254
|
+
"p-limit": "^7.3.3",
|
|
255
255
|
"proxyquire": "^2.1.3",
|
|
256
256
|
"retry": "^0.13.1",
|
|
257
257
|
"semifies": "^1.0.0",
|
|
@@ -124,13 +124,48 @@ function wrapStreamIterator (iterator, ctx) {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Gives the SDK a detached copy of the response to decode, leaving the body the caller can still
|
|
129
|
+
* read through `asResponse()` untouched. The bytes are buffered instead of streamed from a live
|
|
130
|
+
* clone, and the controller is replaced, because the SDK cancels its reader and aborts its
|
|
131
|
+
* controller whenever a stream ends early: that abort would kill the caller's request, and on a
|
|
132
|
+
* cloned body that cancel blocks until the other branch is read, which the caller cannot do while
|
|
133
|
+
* it is still waiting for this parse.
|
|
134
|
+
*
|
|
135
|
+
* Any failure falls back to the original props and tells the wrapper to skip inspection, leaving
|
|
136
|
+
* the SDK to behave as it does untraced.
|
|
137
|
+
*
|
|
138
|
+
* @param {{response: Response}} props
|
|
139
|
+
* @param {() => void} onFailure
|
|
140
|
+
* @returns {object|Promise<object>}
|
|
141
|
+
*/
|
|
142
|
+
function detachResponse (props, onFailure) {
|
|
143
|
+
const { status, statusText, headers } = props.response
|
|
144
|
+
try {
|
|
145
|
+
return props.response.clone().arrayBuffer()
|
|
146
|
+
.then(body => ({
|
|
147
|
+
...props,
|
|
148
|
+
response: new Response(body, { status, statusText, headers }),
|
|
149
|
+
controller: new AbortController(),
|
|
150
|
+
}))
|
|
151
|
+
.catch(() => {
|
|
152
|
+
onFailure()
|
|
153
|
+
return props
|
|
154
|
+
})
|
|
155
|
+
} catch {
|
|
156
|
+
onFailure()
|
|
157
|
+
return props
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
127
161
|
function wrapCreate (create) {
|
|
128
162
|
return function (...args) {
|
|
129
163
|
const stream = args[0]?.stream
|
|
130
|
-
const
|
|
131
|
-
const
|
|
164
|
+
const tracing = anthropicTracingChannel.start.hasSubscribers
|
|
165
|
+
const preparing = messagesPrepareChannel.hasSubscribers
|
|
166
|
+
const intercepting = messagesInterceptChannel.hasSubscribers
|
|
132
167
|
|
|
133
|
-
if (!
|
|
168
|
+
if (!tracing && !preparing && !intercepting) {
|
|
134
169
|
return create.apply(this, args)
|
|
135
170
|
}
|
|
136
171
|
|
|
@@ -160,23 +195,60 @@ function wrapCreate (create) {
|
|
|
160
195
|
|
|
161
196
|
let parseResult
|
|
162
197
|
let wrappedResponse
|
|
198
|
+
// Set around a `parse()` whose body a raw-response reader also holds, so the SDK decodes a
|
|
199
|
+
// copy and leaves that body to the caller. Plain streaming has no second reader and copies
|
|
200
|
+
// nothing.
|
|
201
|
+
let detach = false
|
|
163
202
|
|
|
203
|
+
// These helpers live on each returned APIPromise, so there is no module export for
|
|
204
|
+
// Orchestrion to replace; wrap this instance instead.
|
|
164
205
|
shimmer.wrap(apiPromise, 'parse', parse => function (...parseArgs) {
|
|
165
206
|
if (parseResult) return parseResult
|
|
166
207
|
|
|
167
|
-
|
|
208
|
+
let detachmentFailed = false
|
|
209
|
+
const responsePromise = detach ? this.responsePromise : undefined
|
|
210
|
+
if (responsePromise) {
|
|
211
|
+
this.responsePromise = responsePromise.then(props => detachResponse(props, () => {
|
|
212
|
+
detachmentFailed = true
|
|
213
|
+
}))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let result
|
|
217
|
+
try {
|
|
218
|
+
result = parse.apply(this, parseArgs)
|
|
219
|
+
} finally {
|
|
220
|
+
if (responsePromise) this.responsePromise = responsePromise
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const onResult = interceptCtx?.onResult
|
|
224
|
+
parseResult = heldUntil(result, interceptCtx?.beforeResult?.())
|
|
168
225
|
.then(response => {
|
|
169
226
|
if (stream) {
|
|
170
|
-
|
|
171
|
-
|
|
227
|
+
if (!onResult || detachmentFailed) {
|
|
228
|
+
if (tracing) {
|
|
229
|
+
shimmer.wrap(response, Symbol.asyncIterator, iterator => wrapStreamIterator(iterator, ctx))
|
|
230
|
+
}
|
|
231
|
+
return response
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return Promise.resolve(onResult(response)).then(deliveredResponse => {
|
|
235
|
+
if (tracing) {
|
|
236
|
+
shimmer.wrap(
|
|
237
|
+
deliveredResponse,
|
|
238
|
+
Symbol.asyncIterator,
|
|
239
|
+
iterator => wrapStreamIterator(iterator, ctx)
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
return deliveredResponse
|
|
243
|
+
})
|
|
172
244
|
}
|
|
173
245
|
|
|
174
|
-
if (!
|
|
246
|
+
if (!onResult) {
|
|
175
247
|
finish(ctx, response)
|
|
176
248
|
return response
|
|
177
249
|
}
|
|
178
250
|
|
|
179
|
-
return Promise.resolve(
|
|
251
|
+
return Promise.resolve(onResult(response)).then(deliveredResponse => {
|
|
180
252
|
finish(ctx, response)
|
|
181
253
|
return deliveredResponse
|
|
182
254
|
})
|
|
@@ -190,9 +262,26 @@ function wrapCreate (create) {
|
|
|
190
262
|
shimmer.wrap(apiPromise, 'asResponse', origAsResponse => function (...asResponseArgs) {
|
|
191
263
|
return heldUntil(origAsResponse.apply(this, asResponseArgs), interceptCtx?.beforeResult?.())
|
|
192
264
|
.then(response => {
|
|
265
|
+
if (stream) {
|
|
266
|
+
// A caller holding the parsed stream closes the span as it drains it. When only
|
|
267
|
+
// the raw body is read, decode the stream here so it is still evaluated.
|
|
268
|
+
if (!interceptCtx?.onResult) return response
|
|
269
|
+
if (parseResult) return parseResult.then(() => response)
|
|
270
|
+
|
|
271
|
+
detach = true
|
|
272
|
+
try {
|
|
273
|
+
return apiPromise.parse().then(() => {
|
|
274
|
+
finish(ctx)
|
|
275
|
+
return response
|
|
276
|
+
})
|
|
277
|
+
} finally {
|
|
278
|
+
detach = false
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
193
282
|
// Wrap json()/text()/clone() so the span still closes on the raw-response path,
|
|
194
283
|
// and not twice for the same response.
|
|
195
|
-
if (
|
|
284
|
+
if (wrappedResponse !== response) {
|
|
196
285
|
wrappedResponse = response
|
|
197
286
|
wrapRawResponse(response, ctx, interceptCtx)
|
|
198
287
|
}
|
|
@@ -202,6 +291,19 @@ function wrapCreate (create) {
|
|
|
202
291
|
})
|
|
203
292
|
}
|
|
204
293
|
|
|
294
|
+
// `withResponse()` is the one SDK call that hands out the raw response *and* the parsed
|
|
295
|
+
// stream; it invokes `parse()` synchronously, so the flag only has to outlive that call.
|
|
296
|
+
if (stream && interceptCtx?.onResult && typeof apiPromise.withResponse === 'function') {
|
|
297
|
+
shimmer.wrap(apiPromise, 'withResponse', withResponse => function (...withResponseArgs) {
|
|
298
|
+
detach = true
|
|
299
|
+
try {
|
|
300
|
+
return withResponse.apply(this, withResponseArgs)
|
|
301
|
+
} finally {
|
|
302
|
+
detach = false
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
}
|
|
306
|
+
|
|
205
307
|
anthropicTracingChannel.end.publish(ctx)
|
|
206
308
|
|
|
207
309
|
return apiPromise
|
|
@@ -44,6 +44,7 @@ const chunkEmitTimes = new WeakMap()
|
|
|
44
44
|
* tools: Map<string, ToolState>,
|
|
45
45
|
* subagents: Map<string, Record<string, unknown>>,
|
|
46
46
|
* prompt?: string,
|
|
47
|
+
* systemPrompt?: string | string[] | { type: string, prompt?: string | string[], append?: string },
|
|
47
48
|
* model?: string,
|
|
48
49
|
* resume?: string,
|
|
49
50
|
* maxTurns?: number,
|
|
@@ -235,6 +236,7 @@ function onQueryStart (ctx) {
|
|
|
235
236
|
const prompt = queryArg.prompt
|
|
236
237
|
const sessionCtx = {
|
|
237
238
|
prompt: typeof prompt === 'string' ? prompt : undefined,
|
|
239
|
+
systemPrompt: options.systemPrompt,
|
|
238
240
|
model: options.model,
|
|
239
241
|
resume: options.resume,
|
|
240
242
|
maxTurns: options.maxTurns,
|
|
@@ -451,6 +453,7 @@ function processStep (
|
|
|
451
453
|
llmCh.traceSync(() => {}, {
|
|
452
454
|
model,
|
|
453
455
|
usage,
|
|
456
|
+
systemPrompt: parentToolUseId ? undefined : sessionCtx?.systemPrompt,
|
|
454
457
|
startTime: stepStartTime,
|
|
455
458
|
finishTime: chunkEmitTimes.get(chunks[messageEndIdx - 1]),
|
|
456
459
|
chunks,
|
|
@@ -529,7 +532,8 @@ function processChunks (chunks, agentCtx) {
|
|
|
529
532
|
const sessionCtx = agentCtx.sessionCtx
|
|
530
533
|
const getLifecycle = createStreamLookup(chunks)
|
|
531
534
|
|
|
532
|
-
const
|
|
535
|
+
const initChunk = chunks.find(chunk => chunk.type === 'system' && chunk.subtype === 'init') || chunks[0]
|
|
536
|
+
const { type, subtype, ...rest } = initChunk
|
|
533
537
|
Object.assign(agentCtx, rest)
|
|
534
538
|
if (sessionCtx) {
|
|
535
539
|
if (sessionCtx.sessionId) agentCtx.session_id = sessionCtx.sessionId
|
|
@@ -1418,13 +1418,49 @@ function getWrappedRunTestCase (runTestCaseFunction, isNewerCucumberVersion = fa
|
|
|
1418
1418
|
|
|
1419
1419
|
if (isImpactedTestsEnabled) {
|
|
1420
1420
|
const setIsModified = (receivedIsModified) => { isModified = receivedIsModified }
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1423
|
-
|
|
1421
|
+
const gherkinNodes = []
|
|
1422
|
+
// Prefix ranges catch removed Backgrounds, which have no AST node in the target document.
|
|
1423
|
+
const gherkinScopeRanges = []
|
|
1424
|
+
const feature = gherkinDocument.feature
|
|
1425
|
+
const featureChildren = feature?.children
|
|
1426
|
+
if (featureChildren?.length) {
|
|
1427
|
+
const firstFeatureChild = featureChildren[0]
|
|
1428
|
+
const firstFeatureNode = firstFeatureChild.background ?? firstFeatureChild.rule ?? firstFeatureChild.scenario
|
|
1429
|
+
const firstFeatureLine = firstFeatureNode.tags?.[0]?.location.line ?? firstFeatureNode.location.line
|
|
1430
|
+
const featurePrefix = [feature.location.line, firstFeatureLine - 1]
|
|
1431
|
+
let featureBackground
|
|
1432
|
+
for (const child of featureChildren) {
|
|
1433
|
+
if (child.background) {
|
|
1434
|
+
featureBackground = child.background
|
|
1435
|
+
continue
|
|
1436
|
+
}
|
|
1437
|
+
const rule = child.rule
|
|
1438
|
+
const children = rule?.children ?? [child]
|
|
1439
|
+
let ruleBackground
|
|
1440
|
+
for (const { background } of children) {
|
|
1441
|
+
if (background) ruleBackground = background
|
|
1442
|
+
}
|
|
1443
|
+
for (const { scenario } of children) {
|
|
1444
|
+
if (scenario && pickle.astNodeIds.includes(scenario.id)) {
|
|
1445
|
+
if (!featureBackground) gherkinScopeRanges.push(featurePrefix)
|
|
1446
|
+
if (rule?.children?.length && !ruleBackground) {
|
|
1447
|
+
const firstRuleChild = rule.children.at(0)
|
|
1448
|
+
const firstRuleNode = firstRuleChild.background ?? firstRuleChild.scenario
|
|
1449
|
+
const firstRuleLine = firstRuleNode.tags?.[0]?.location.line ?? firstRuleNode.location.line
|
|
1450
|
+
gherkinScopeRanges.push([rule.location.line, firstRuleLine - 1])
|
|
1451
|
+
}
|
|
1452
|
+
if (featureBackground) gherkinNodes.push(featureBackground)
|
|
1453
|
+
if (ruleBackground) gherkinNodes.push(ruleBackground)
|
|
1454
|
+
gherkinNodes.push(scenario)
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1424
1459
|
const stepIds = testCase?.testSteps?.flatMap(testStep => testStep.stepDefinitionIds)
|
|
1425
1460
|
|
|
1426
1461
|
isModifiedCh.publish({
|
|
1427
|
-
|
|
1462
|
+
gherkinNodes,
|
|
1463
|
+
gherkinScopeRanges,
|
|
1428
1464
|
testFileAbsolutePath: gherkinDocument.uri,
|
|
1429
1465
|
modifiedFiles,
|
|
1430
1466
|
stepIds,
|
package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
// Playwright keeps several hook targets in private local classes/functions.
|
|
4
4
|
// Keep these rewrites limited to bundled internals that addHook cannot wrap.
|
|
5
5
|
module.exports = [
|
|
6
|
+
...[
|
|
7
|
+
['>=1.38.0 <1.60.0', 'lib/common/config.js'],
|
|
8
|
+
['>=1.60.0', 'lib/common/index.js'],
|
|
9
|
+
].map(([versionRange, filePath]) => ({
|
|
10
|
+
module: { name: 'playwright', versionRange, filePath },
|
|
11
|
+
functionQuery: { className: 'FullProjectInternal', methodName: 'constructor', kind: 'Sync' },
|
|
12
|
+
channelName: 'FullProjectInternal',
|
|
13
|
+
})),
|
|
6
14
|
{
|
|
7
15
|
module: {
|
|
8
16
|
name: '@playwright/test',
|
|
@@ -77,6 +77,8 @@
|
|
|
77
77
|
"jasmine-core/lib/jasmine-core/jasmine.js": "jasmine-core",
|
|
78
78
|
"mercurius/index.js": "mercurius",
|
|
79
79
|
"playwright-core/lib/coreBundle.js": "playwright-core",
|
|
80
|
+
"playwright/lib/common/config.js": "playwright",
|
|
81
|
+
"playwright/lib/common/index.js": "playwright",
|
|
80
82
|
"playwright/lib/index.js": "playwright",
|
|
81
83
|
"playwright/lib/runner/index.js": "playwright",
|
|
82
84
|
"playwright/lib/runner/lastRun.js": "playwright",
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { stripVTControlCharacters } = require('node:util')
|
|
4
|
+
|
|
5
|
+
const { MAX_META_VALUE_LENGTH_TEST_OPTIMIZATION: MAX_LENGTH } =
|
|
6
|
+
require('../../../dd-trace/src/encode/tags-processors')
|
|
7
|
+
const log = require('../../../dd-trace/src/log')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {{ name?: unknown, type?: unknown, message?: unknown, stack?: unknown }} SuiteError
|
|
11
|
+
* @typedef {object} JestResults
|
|
12
|
+
* @property {number} [numFailedTestSuites]
|
|
13
|
+
* @property {number} [numFailedTests]
|
|
14
|
+
* @property {{ testExecError?: SuiteError, failureMessage?: unknown, numFailingTests?: number }[]} [testResults]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Includes complete, deduplicated suite errors only when all details fit.
|
|
19
|
+
*
|
|
20
|
+
* @param {JestResults} results
|
|
21
|
+
* @returns {Error}
|
|
22
|
+
*/
|
|
23
|
+
function getSessionError (results = {}) {
|
|
24
|
+
const { numFailedTestSuites = 0, numFailedTests = 0 } = results
|
|
25
|
+
const summary = `Failed test suites: ${numFailedTestSuites}. Failed tests: ${numFailedTests}`
|
|
26
|
+
const error = new Error(summary)
|
|
27
|
+
const groups = new Map()
|
|
28
|
+
let size = 0
|
|
29
|
+
try {
|
|
30
|
+
const { testResults } = results
|
|
31
|
+
if (!testResults) return error
|
|
32
|
+
for (const result of testResults) {
|
|
33
|
+
let name, message, stack
|
|
34
|
+
try {
|
|
35
|
+
const { testExecError } = result
|
|
36
|
+
if (testExecError && !result.numFailingTests) {
|
|
37
|
+
name = readField(testExecError.name)
|
|
38
|
+
if (name === '') name = readField(testExecError.type)
|
|
39
|
+
message = readField(testExecError.message)
|
|
40
|
+
stack = readField(testExecError.stack)
|
|
41
|
+
} else {
|
|
42
|
+
// Jest's report includes assertion failures, hook errors and any suite execution error.
|
|
43
|
+
name = 'Error'
|
|
44
|
+
message = readField(result.failureMessage)
|
|
45
|
+
stack = message
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
log.debug('Skipping unreadable Jest suite error in the test session summary')
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
if (name === undefined || message === undefined || stack === undefined) return omitDetails(error)
|
|
52
|
+
if (!message && !stack) continue
|
|
53
|
+
name ||= 'Error'
|
|
54
|
+
const key = JSON.stringify([name, message, stack])
|
|
55
|
+
const group = groups.get(key)
|
|
56
|
+
if (group) {
|
|
57
|
+
group.count++
|
|
58
|
+
} else {
|
|
59
|
+
// Retained text must fit across error.message and error.stack; also bound grouping memory.
|
|
60
|
+
size += name.length + message.length + stack.length
|
|
61
|
+
if (size > 2 * MAX_LENGTH) return omitDetails(error)
|
|
62
|
+
groups.set(key, { name, message, stack, count: 1 })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
log.debug('Unable to read Jest results for the test session summary')
|
|
67
|
+
return error
|
|
68
|
+
}
|
|
69
|
+
if (!groups.size) return error
|
|
70
|
+
|
|
71
|
+
const errors = [...groups.keys()].sort().map(key => groups.get(key))
|
|
72
|
+
let message = summary
|
|
73
|
+
let stack = ''
|
|
74
|
+
for (const { name, message: detail, stack: originalStack, count } of errors) {
|
|
75
|
+
message += `\n\n${name}: ${detail} (${count} suite${count === 1 ? '' : 's'})`
|
|
76
|
+
const prefix = errors.length === 1 ? '' : `Affected suites: ${count}\n`
|
|
77
|
+
stack += (stack ? '\n\n' : '') + prefix + (originalStack || `${name}: ${detail}`)
|
|
78
|
+
if (message.length > MAX_LENGTH || stack.length > MAX_LENGTH) return omitDetails(error)
|
|
79
|
+
}
|
|
80
|
+
error.name = errors.length === 1 ? errors[0].name : 'Error'
|
|
81
|
+
error.message = message
|
|
82
|
+
error.stack = stack
|
|
83
|
+
return error
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Returns undefined for oversized input before scanning or copying it.
|
|
88
|
+
* @param {unknown} value
|
|
89
|
+
*/
|
|
90
|
+
function readField (value) {
|
|
91
|
+
if (typeof value !== 'string') return ''
|
|
92
|
+
if (value.length > MAX_LENGTH) return
|
|
93
|
+
return stripVTControlCharacters(value)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** @param {Error} error */
|
|
97
|
+
function omitDetails (error) {
|
|
98
|
+
error.message += '\n\nError details exceed the size limit. See test and suite events for full details.'
|
|
99
|
+
return error
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { getSessionError }
|
|
@@ -58,6 +58,7 @@ const {
|
|
|
58
58
|
addCoverageBackfillUntestedFiles,
|
|
59
59
|
getCoverageBackfillFiles,
|
|
60
60
|
} = require('./jest/coverage-backfill')
|
|
61
|
+
const { getSessionError } = require('./jest/session-error')
|
|
61
62
|
const {
|
|
62
63
|
getChannelPromise,
|
|
63
64
|
publishWithCompletion,
|
|
@@ -2739,13 +2740,6 @@ function applySkippedCoverageToJestCoverageMap (coverageMap, rootDir) {
|
|
|
2739
2740
|
)
|
|
2740
2741
|
}
|
|
2741
2742
|
|
|
2742
|
-
function getSessionFinishError (results) {
|
|
2743
|
-
const numFailedTestSuites = results?.numFailedTestSuites || 0
|
|
2744
|
-
const numFailedTests = results?.numFailedTests || 0
|
|
2745
|
-
|
|
2746
|
-
return new Error(`Failed test suites: ${numFailedTestSuites}. Failed tests: ${numFailedTests}`)
|
|
2747
|
-
}
|
|
2748
|
-
|
|
2749
2743
|
function getTestSessionCoveragePayload (results, fallbackRootDir) {
|
|
2750
2744
|
const payload = {}
|
|
2751
2745
|
if (!shouldReportCodeCoverageLinesPct()) return payload
|
|
@@ -2847,7 +2841,7 @@ function getTestSessionFinishPayload (status, error, extra = {}) {
|
|
|
2847
2841
|
async function finishBailTestSession (results, fallbackRootDir) {
|
|
2848
2842
|
await waitForTestSessionFinish(getTestSessionFinishPayload(
|
|
2849
2843
|
'fail',
|
|
2850
|
-
|
|
2844
|
+
getSessionError(results),
|
|
2851
2845
|
getTestSessionCoveragePayload(results, fallbackRootDir)
|
|
2852
2846
|
))
|
|
2853
2847
|
}
|
|
@@ -3170,7 +3164,6 @@ function getCliWrapper (isNewJestVersion) {
|
|
|
3170
3164
|
|
|
3171
3165
|
const {
|
|
3172
3166
|
results: {
|
|
3173
|
-
numFailedTestSuites,
|
|
3174
3167
|
numFailedTests,
|
|
3175
3168
|
numPassedTests,
|
|
3176
3169
|
numRuntimeErrorTestSuites = 0,
|
|
@@ -3351,7 +3344,7 @@ function getCliWrapper (isNewJestVersion) {
|
|
|
3351
3344
|
status = testSessionEmptyReason ? 'skip' : 'pass'
|
|
3352
3345
|
} else {
|
|
3353
3346
|
status = 'fail'
|
|
3354
|
-
error =
|
|
3347
|
+
error = getSessionError(result.results)
|
|
3355
3348
|
}
|
|
3356
3349
|
|
|
3357
3350
|
await waitForTestSessionFinish(getTestSessionFinishPayload(status, error, {
|
|
@@ -75,6 +75,7 @@ const artifactsRecorderScreenshotPathCh =
|
|
|
75
75
|
const snapshotRecorderScreenshotPathCh = tracingChannel('orchestrion:playwright:SnapshotRecorder_createAttachmentPath')
|
|
76
76
|
const saveAutomaticVideoCh = tracingChannel('orchestrion:playwright:saveAutomaticVideo')
|
|
77
77
|
const pageGotoCh = tracingChannel('orchestrion:playwright-core:Page_goto')
|
|
78
|
+
const fullProjectInternalCh = tracingChannel('orchestrion:playwright:FullProjectInternal')
|
|
78
79
|
|
|
79
80
|
const testToCtx = new WeakMap()
|
|
80
81
|
const originalTestIdsByRetry = new WeakMap()
|
|
@@ -91,6 +92,8 @@ const isFailureScreenshotUploadEnabled =
|
|
|
91
92
|
getValueFromEnvSources('DD_TEST_FAILURE_SCREENSHOTS_ENABLED') === true
|
|
92
93
|
const isFailureVideoUploadEnabled =
|
|
93
94
|
getValueFromEnvSources('DD_TEST_FAILURE_VIDEOS_ENABLED') === true
|
|
95
|
+
const shouldEnableFailureScreenshots = getValueFromEnvSources('DD_TEST_FAILURE_SCREENSHOTS_ENABLED', true) === true
|
|
96
|
+
const shouldEnableFailureVideos = getValueFromEnvSources('DD_TEST_FAILURE_VIDEOS_ENABLED', true) === true
|
|
94
97
|
|
|
95
98
|
let applyRepeatEachIndex = null
|
|
96
99
|
let reporterError
|
|
@@ -1057,6 +1060,8 @@ function testEndHandler ({
|
|
|
1057
1060
|
if (isFlakyTestRetriesEnabled && !testProperties.attemptToFix && !test._ddIsEfdRetry &&
|
|
1058
1061
|
!(test._ddIsNew || test._ddIsModified) &&
|
|
1059
1062
|
atrRetryCount != null && atrRetryCount > 0 &&
|
|
1063
|
+
// Serial suites can add skipped results before a test's first execution.
|
|
1064
|
+
results.some((result, index) => index < results.length - 1 && result.status !== 'skipped') &&
|
|
1060
1065
|
!willRetry && testResultStatus !== expectedStatus &&
|
|
1061
1066
|
testStatuses.every(status => status === 'fail')) {
|
|
1062
1067
|
test._ddHasFailedAllRetries = true
|
|
@@ -1882,6 +1887,27 @@ dispatcherRunCh.subscribe({
|
|
|
1882
1887
|
},
|
|
1883
1888
|
})
|
|
1884
1889
|
|
|
1890
|
+
fullProjectInternalCh.subscribe({
|
|
1891
|
+
end ({ self, error }) {
|
|
1892
|
+
if (error || !libraryConfigurationCh.hasSubscribers) return
|
|
1893
|
+
|
|
1894
|
+
// Workers reload the config, so apply capture settings as each project is resolved in either process.
|
|
1895
|
+
const { project } = self
|
|
1896
|
+
if (shouldEnableFailureScreenshots && !isFailureScreenshotCaptureEnabled([project])) {
|
|
1897
|
+
const screenshot = project.use.screenshot
|
|
1898
|
+
project.use.screenshot = typeof screenshot === 'object' && screenshot !== null
|
|
1899
|
+
? { ...screenshot, mode: 'only-on-failure' }
|
|
1900
|
+
: 'only-on-failure'
|
|
1901
|
+
}
|
|
1902
|
+
if (shouldEnableFailureVideos && !isFailureVideoCaptureEnabled([project])) {
|
|
1903
|
+
const video = project.use.video
|
|
1904
|
+
project.use.video = typeof video === 'object' && video !== null
|
|
1905
|
+
? { ...video, mode: 'retain-on-failure' }
|
|
1906
|
+
: 'retain-on-failure'
|
|
1907
|
+
}
|
|
1908
|
+
},
|
|
1909
|
+
})
|
|
1910
|
+
|
|
1885
1911
|
dispatcherCreateWorkerCh.subscribe({
|
|
1886
1912
|
end (ctx) {
|
|
1887
1913
|
onDispatcherCreateWorker(ctx.self, ctx.result)
|
|
@@ -1981,6 +1981,9 @@ function getWrappedOn (on) {
|
|
|
1981
1981
|
if (event !== 'message') {
|
|
1982
1982
|
return on.apply(this, arguments)
|
|
1983
1983
|
}
|
|
1984
|
+
// Reply through Vitest's pool worker, which owns its version-specific serialization.
|
|
1985
|
+
// The listener's `this` is the underlying ChildProcess or Worker instead.
|
|
1986
|
+
const worker = this
|
|
1984
1987
|
// `arguments[1]` is the callback function, which
|
|
1985
1988
|
// we modify to intercept our messages to not interfere
|
|
1986
1989
|
// with vitest's own messages
|
|
@@ -1988,7 +1991,7 @@ function getWrappedOn (on) {
|
|
|
1988
1991
|
if (message.type !== 'Buffer' && Array.isArray(message)) {
|
|
1989
1992
|
const [interprocessCode, data] = message
|
|
1990
1993
|
if (
|
|
1991
|
-
handleEfdAdmissionMessage(
|
|
1994
|
+
handleEfdAdmissionMessage(worker, interprocessCode, data) ||
|
|
1992
1995
|
handleWorkerReport(interprocessCode, data)
|
|
1993
1996
|
) {
|
|
1994
1997
|
// If we execute the callback vitest crashes, as the message is not supported
|