dd-trace 6.0.0 → 6.2.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 -2
- package/index.d.ts +28 -1
- package/initialize.mjs +4 -2
- package/package.json +34 -30
- package/packages/datadog-instrumentations/src/ai.js +45 -0
- package/packages/datadog-instrumentations/src/apollo-server.js +5 -13
- package/packages/datadog-instrumentations/src/child_process.js +3 -3
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +587 -0
- package/packages/datadog-instrumentations/src/cucumber.js +102 -43
- package/packages/datadog-instrumentations/src/cypress-config.js +11 -3
- package/packages/datadog-instrumentations/src/fastify.js +27 -8
- package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
- package/packages/datadog-instrumentations/src/helpers/instrument.js +7 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +5 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +6 -6
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/claude-agent-sdk.js +17 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +31 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +19 -5
- package/packages/datadog-instrumentations/src/jest.js +713 -63
- package/packages/datadog-instrumentations/src/mercurius.js +11 -0
- package/packages/datadog-instrumentations/src/mocha/main.js +24 -3
- package/packages/datadog-instrumentations/src/mocha/utils.js +128 -22
- package/packages/datadog-instrumentations/src/mocha/worker.js +13 -0
- package/packages/datadog-instrumentations/src/mongodb.js +74 -0
- package/packages/datadog-instrumentations/src/mongoose.js +4 -5
- package/packages/datadog-instrumentations/src/otel-sdk-trace.js +15 -0
- package/packages/datadog-instrumentations/src/playwright.js +13 -9
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +4 -0
- package/packages/datadog-instrumentations/src/vitest-main.js +26 -1
- package/packages/datadog-instrumentations/src/vitest-util.js +2 -0
- package/packages/datadog-instrumentations/src/vitest-worker.js +103 -34
- package/packages/datadog-plugin-ai/src/index.js +1 -1
- package/packages/datadog-plugin-ai/src/tracing.js +66 -3
- package/packages/datadog-plugin-ai/src/utils.js +17 -4
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +1 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +1 -0
- package/packages/datadog-plugin-child_process/src/index.js +13 -2
- package/packages/datadog-plugin-claude-agent-sdk/src/index.js +31 -0
- package/packages/datadog-plugin-claude-agent-sdk/src/tracing.js +107 -0
- package/packages/datadog-plugin-claude-agent-sdk/src/util.js +15 -0
- package/packages/datadog-plugin-cucumber/src/index.js +14 -23
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +351 -11
- package/packages/datadog-plugin-cypress/src/index.js +47 -2
- package/packages/datadog-plugin-cypress/src/plugin.js +1 -0
- package/packages/datadog-plugin-graphql/src/execute.js +25 -25
- package/packages/datadog-plugin-graphql/src/index.js +23 -1
- package/packages/datadog-plugin-graphql/src/request.js +104 -0
- package/packages/datadog-plugin-graphql/src/utils.js +186 -2
- package/packages/datadog-plugin-graphql/src/validate.js +25 -2
- package/packages/datadog-plugin-jest/src/index.js +30 -14
- package/packages/datadog-plugin-mocha/src/index.js +39 -14
- package/packages/datadog-plugin-mongodb-core/src/bulk-write.js +43 -0
- package/packages/datadog-plugin-mongodb-core/src/index.js +8 -443
- package/packages/datadog-plugin-mongodb-core/src/query.js +452 -0
- package/packages/datadog-plugin-playwright/src/index.js +3 -2
- package/packages/datadog-plugin-vitest/src/index.js +43 -9
- package/packages/dd-trace/src/aiguard/index.js +9 -14
- package/packages/dd-trace/src/aiguard/integrations/index.js +34 -0
- package/packages/dd-trace/src/aiguard/integrations/openai.js +47 -33
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +42 -28
- package/packages/dd-trace/src/appsec/graphql.js +9 -6
- package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +3 -10
- package/packages/dd-trace/src/appsec/lambda.js +8 -8
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +26 -1
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +115 -24
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +3 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +75 -7
- package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +156 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +10 -0
- package/packages/dd-trace/src/config/supported-configurations.json +37 -0
- package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
- package/packages/dd-trace/src/datastreams/writer.js +14 -2
- package/packages/dd-trace/src/encode/0.4.js +23 -2
- package/packages/dd-trace/src/encode/0.5.js +12 -1
- package/packages/dd-trace/src/encode/agentless-ci-visibility.js +11 -1
- package/packages/dd-trace/src/encode/coverage-ci-visibility.js +20 -2
- package/packages/dd-trace/src/exporters/common/writer.js +17 -1
- package/packages/dd-trace/src/guardrails/index.js +3 -1
- package/packages/dd-trace/src/guardrails/telemetry.js +40 -3
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +403 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/index.js +8 -395
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +27 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +363 -0
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +270 -0
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/utils.js +10 -0
- package/packages/dd-trace/src/msgpack/chunk.js +33 -1
- package/packages/dd-trace/src/msgpack/index.js +6 -1
- package/packages/dd-trace/src/opentelemetry/span.js +5 -0
- package/packages/dd-trace/src/plugin_manager.js +16 -0
- package/packages/dd-trace/src/plugins/ci_plugin.js +147 -7
- package/packages/dd-trace/src/plugins/index.js +5 -0
- package/packages/dd-trace/src/plugins/util/test.js +4 -0
- package/packages/dd-trace/src/profiling/exporters/agent.js +28 -3
- package/packages/dd-trace/src/service-naming/schemas/v0/graphql.js +6 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/graphql.js +8 -0
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +5 -6
|
@@ -5,12 +5,10 @@ const dc = require('dc-polyfill')
|
|
|
5
5
|
const { storage } = require('../../datadog-core')
|
|
6
6
|
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
|
|
7
7
|
const GraphQLParsePlugin = require('./parse')
|
|
8
|
-
const { extractErrorIntoSpanEvent, getSignature } = require('./utils')
|
|
8
|
+
const { extractErrorIntoSpanEvent, getOperation, getSignature } = require('./utils')
|
|
9
9
|
|
|
10
10
|
const legacyStorage = storage('legacy')
|
|
11
11
|
|
|
12
|
-
const types = new Set(['query', 'mutation', 'subscription'])
|
|
13
|
-
|
|
14
12
|
const iastResolveCh = dc.channel('apm:graphql:resolve:start')
|
|
15
13
|
const resolverStartCh = dc.channel('datadog:graphql:resolver:start')
|
|
16
14
|
const updateFieldCh = dc.channel('apm:graphql:resolve:updateField')
|
|
@@ -115,9 +113,11 @@ class GraphQLExecutePlugin extends TracingPlugin {
|
|
|
115
113
|
|
|
116
114
|
ctx.collapse = this.config.collapse
|
|
117
115
|
|
|
116
|
+
const signature = getSignature(document, name, type, this.config.signature)
|
|
117
|
+
|
|
118
118
|
const span = this.startSpan(this.operationName(), {
|
|
119
119
|
service: this.config.service || this.serviceName(),
|
|
120
|
-
resource:
|
|
120
|
+
resource: signature,
|
|
121
121
|
kind: this.constructor.kind,
|
|
122
122
|
type: this.constructor.type,
|
|
123
123
|
meta: {
|
|
@@ -241,7 +241,7 @@ class GraphQLExecutePlugin extends TracingPlugin {
|
|
|
241
241
|
if (res?.errors?.length) {
|
|
242
242
|
span.setTag('error', res.errors[0])
|
|
243
243
|
for (const err of res.errors) {
|
|
244
|
-
extractErrorIntoSpanEvent(this.
|
|
244
|
+
extractErrorIntoSpanEvent(this.config, span, err)
|
|
245
245
|
}
|
|
246
246
|
}
|
|
247
247
|
|
|
@@ -271,6 +271,9 @@ class GraphQLExecutePlugin extends TracingPlugin {
|
|
|
271
271
|
const loc = this.config.source && document && fieldNode && fieldNode.loc
|
|
272
272
|
const source = loc && document.slice(loc.start, loc.end)
|
|
273
273
|
|
|
274
|
+
// ctx form: startSpan sets field.currentStore = { ...activeStore, span }
|
|
275
|
+
// without entering it. Only the field's first resolver call runs in that
|
|
276
|
+
// store (isFirst check in wrapResolve); siblings use field.parentStore.
|
|
274
277
|
const span = this.startSpan('graphql.resolve', {
|
|
275
278
|
service: this.config.service,
|
|
276
279
|
resource: `${fieldName}:${returnType}`,
|
|
@@ -283,7 +286,9 @@ class GraphQLExecutePlugin extends TracingPlugin {
|
|
|
283
286
|
'graphql.field.type': baseTypeName,
|
|
284
287
|
'graphql.source': source,
|
|
285
288
|
},
|
|
286
|
-
},
|
|
289
|
+
}, field)
|
|
290
|
+
|
|
291
|
+
field.span = span
|
|
287
292
|
|
|
288
293
|
if (fieldNode && this.config.variables && fieldNode.arguments) {
|
|
289
294
|
const variables = this.config.variables(variableValues)
|
|
@@ -388,15 +393,22 @@ function wrapResolve (resolve) {
|
|
|
388
393
|
pathString,
|
|
389
394
|
collapsedKey: collapsedKey ?? pathString,
|
|
390
395
|
span: null,
|
|
396
|
+
// Set by startResolveSpan; currentStore is used by the first resolver
|
|
397
|
+
// call only, siblings use parentStore (see the isFirst check below).
|
|
398
|
+
parentStore: null,
|
|
399
|
+
currentStore: null,
|
|
391
400
|
}
|
|
392
401
|
rootCtx.fields.set(fieldKey, field)
|
|
393
402
|
}
|
|
394
403
|
|
|
395
404
|
// Collapsed siblings still publish updateField (master's contract: one
|
|
396
405
|
// publish per resolver call, even when the span is collapsed) and route
|
|
397
|
-
// through callInAsyncScope so the abort signal stops them mid-flight.
|
|
406
|
+
// through callInAsyncScope so the abort signal stops them mid-flight. They
|
|
407
|
+
// run in the parent store, not field.currentStore: the first sibling's
|
|
408
|
+
// synchronous resolver already finished the shared graphql.resolve span, so
|
|
409
|
+
// re-entering its store would parent user spans to a closed span.
|
|
398
410
|
if (!isFirst) {
|
|
399
|
-
return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, (err) => {
|
|
411
|
+
return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, field.parentStore, (err) => {
|
|
400
412
|
if (updateFieldCh.hasSubscribers) {
|
|
401
413
|
updateFieldCh.publish({ rootCtx, field, error: err, pathString: field.pathString })
|
|
402
414
|
}
|
|
@@ -406,9 +418,8 @@ function wrapResolve (resolve) {
|
|
|
406
418
|
const executeSpan = rootCtx.executeSpan
|
|
407
419
|
const startTime = executeSpan._getTime()
|
|
408
420
|
const span = rootCtx.plugin.startResolveSpan(field, rootCtx, executeSpan, startTime)
|
|
409
|
-
field.span = span
|
|
410
421
|
|
|
411
|
-
return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, (err, res) => {
|
|
422
|
+
return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, field.currentStore, (err, res) => {
|
|
412
423
|
const endTime = executeSpan._getTime()
|
|
413
424
|
rootCtx.plugin.finishResolveSpan(span, field, err, res, endTime || startTime)
|
|
414
425
|
if (updateFieldCh.hasSubscribers) {
|
|
@@ -446,16 +457,16 @@ function wrapFieldType (field) {
|
|
|
446
457
|
wrapFields(unwrapped)
|
|
447
458
|
}
|
|
448
459
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
460
|
+
// Runs the resolver inside `store`, including any code after an internal
|
|
461
|
+
// `await`. A `.then()` the caller attaches afterward runs outside `store`.
|
|
462
|
+
function callInAsyncScope (fn, thisArg, args, abortController, store, cb) {
|
|
452
463
|
if (abortController?.signal.aborted) {
|
|
453
464
|
cb(null, null)
|
|
454
465
|
throw new AbortError('Aborted')
|
|
455
466
|
}
|
|
456
467
|
|
|
457
468
|
try {
|
|
458
|
-
const result = fn.apply(thisArg, args)
|
|
469
|
+
const result = legacyStorage.run(store, () => fn.apply(thisArg, args))
|
|
459
470
|
if (typeof result?.then === 'function') {
|
|
460
471
|
return result.then(
|
|
461
472
|
res => { cb(null, res); return res },
|
|
@@ -658,17 +669,6 @@ function defaultFieldResolver (source, args, contextValue, info) {
|
|
|
658
669
|
}
|
|
659
670
|
}
|
|
660
671
|
|
|
661
|
-
function getOperation (document, operationName) {
|
|
662
|
-
if (!document || !Array.isArray(document.definitions)) return
|
|
663
|
-
|
|
664
|
-
for (const definition of document.definitions) {
|
|
665
|
-
if (definition && types.has(definition.operation) &&
|
|
666
|
-
(!operationName || definition.name?.value === operationName)) {
|
|
667
|
-
return definition
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
672
|
function addVariableTags (config, span, variableValues) {
|
|
673
673
|
if (!variableValues || !config.variables) return
|
|
674
674
|
|
|
@@ -6,6 +6,7 @@ const CompositePlugin = require('../../dd-trace/src/plugins/composite')
|
|
|
6
6
|
const log = require('../../dd-trace/src/log')
|
|
7
7
|
const GraphQLExecutePlugin = require('./execute')
|
|
8
8
|
const GraphQLParsePlugin = require('./parse')
|
|
9
|
+
const GraphQLRequestPlugin = require('./request')
|
|
9
10
|
const GraphQLValidatePlugin = require('./validate')
|
|
10
11
|
|
|
11
12
|
class GraphQLPlugin extends CompositePlugin {
|
|
@@ -14,6 +15,11 @@ class GraphQLPlugin extends CompositePlugin {
|
|
|
14
15
|
return {
|
|
15
16
|
execute: GraphQLExecutePlugin,
|
|
16
17
|
parse: GraphQLParsePlugin,
|
|
18
|
+
// Top-level request span for drivers (mercurius) that funnel through a
|
|
19
|
+
// single entry point and parse/execute internally. graphql-js, apollo,
|
|
20
|
+
// and yoga produce no such channel, so the plugin simply never fires for
|
|
21
|
+
// them.
|
|
22
|
+
request: GraphQLRequestPlugin,
|
|
17
23
|
validate: GraphQLValidatePlugin,
|
|
18
24
|
// resolve plugin is absorbed into execute: per-field data is recorded
|
|
19
25
|
// synchronously in wrapResolve, and all graphql.resolve spans are
|
|
@@ -30,6 +36,13 @@ class GraphQLPlugin extends CompositePlugin {
|
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
// config validator helpers
|
|
39
|
+
//
|
|
40
|
+
// `collapse`, `depth`, `variables`, and `errorExtensions` arrive pre-merged on
|
|
41
|
+
// `config`: plugin_manager seeds the `DD_TRACE_GRAPHQL_*` env values (already
|
|
42
|
+
// parsed to their declared type) as the base and a programmatic
|
|
43
|
+
// `tracer.use('graphql', …)` value overrides them. So these helpers only shape
|
|
44
|
+
// the merged value — coerce, validate, and turn `variables` into a filter — and
|
|
45
|
+
// never read the environment themselves.
|
|
33
46
|
|
|
34
47
|
function validateConfig (config) {
|
|
35
48
|
const collapse = config.collapse === undefined || !!config.collapse
|
|
@@ -41,6 +54,7 @@ function validateConfig (config) {
|
|
|
41
54
|
// v5 counted collapsed list indices toward `depth`, so the same query reached a
|
|
42
55
|
// different depth depending on `collapse`. v6 counts selection-set depth only.
|
|
43
56
|
countListIndices: DD_MAJOR < 6 && collapse,
|
|
57
|
+
errorExtensions: getErrorExtensions(config),
|
|
44
58
|
hooks: getHooks(config),
|
|
45
59
|
}
|
|
46
60
|
}
|
|
@@ -58,13 +72,21 @@ function getVariablesFilter (config) {
|
|
|
58
72
|
if (typeof config.variables === 'function') {
|
|
59
73
|
return config.variables
|
|
60
74
|
} else if (Array.isArray(config.variables)) {
|
|
61
|
-
return variables => pick(variables, config.variables)
|
|
75
|
+
return config.variables.length > 0 ? variables => pick(variables, config.variables) : null
|
|
62
76
|
} else if (config.hasOwnProperty('variables')) {
|
|
63
77
|
log.error('Expected `variables` to be an array or function.')
|
|
64
78
|
}
|
|
65
79
|
return null
|
|
66
80
|
}
|
|
67
81
|
|
|
82
|
+
function getErrorExtensions (config) {
|
|
83
|
+
if (Array.isArray(config.errorExtensions)) {
|
|
84
|
+
return config.errorExtensions.length > 0 ? config.errorExtensions : undefined
|
|
85
|
+
} else if (config.hasOwnProperty('errorExtensions')) {
|
|
86
|
+
log.error('Expected `errorExtensions` to be an array.')
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
68
90
|
const noop = () => {}
|
|
69
91
|
const noopHooks = { execute: noop, parse: noop, validate: noop, resolve: undefined }
|
|
70
92
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
|
|
4
|
+
const { extractErrorIntoSpanEvent, getCachedRequestOperation } = require('./utils')
|
|
5
|
+
|
|
6
|
+
// Top-level GraphQL request span for drivers that funnel every operation
|
|
7
|
+
// through a single entry point but parse/validate/execute internally (mercurius
|
|
8
|
+
// today). It parents the `graphql.parse`/`graphql.validate`/`graphql.execute`
|
|
9
|
+
// (or JIT) sub-spans and carries the request text + operation name/type, which
|
|
10
|
+
// otherwise have no home when the query is JIT-compiled and `graphql.execute`
|
|
11
|
+
// never fires.
|
|
12
|
+
//
|
|
13
|
+
// The entry boundary only hands us the raw `source` (string or pre-parsed AST)
|
|
14
|
+
// and `operationName`; the parsed document — and therefore the precise
|
|
15
|
+
// operation signature — is only known once mercurius parses internally. On the
|
|
16
|
+
// cold path the `validate` sub-plugin refines the resource/operation tags onto
|
|
17
|
+
// this span via `ctx.currentStore.graphqlRequestSpan` once the document is
|
|
18
|
+
// available, so we never re-parse on the hot path. On the JIT warm path no
|
|
19
|
+
// sub-span fires, so we recover the same tags from the cache the cold path
|
|
20
|
+
// populated, keyed by source + operationName.
|
|
21
|
+
class GraphQLRequestPlugin extends TracingPlugin {
|
|
22
|
+
static id = 'graphql'
|
|
23
|
+
static operation = 'request'
|
|
24
|
+
static type = 'graphql'
|
|
25
|
+
static kind = 'server'
|
|
26
|
+
static prefix = 'tracing:orchestrion:mercurius:apm:graphql:request'
|
|
27
|
+
|
|
28
|
+
bindStart (ctx) {
|
|
29
|
+
// fastifyGraphQl(source, context, variables, operationName)
|
|
30
|
+
const source = ctx.arguments?.[0]
|
|
31
|
+
const operationName = ctx.arguments?.[3]
|
|
32
|
+
|
|
33
|
+
// `source` is the request text on the common path, but mercurius also
|
|
34
|
+
// accepts a pre-parsed document AST; only a string is the query text, and
|
|
35
|
+
// `graphql.source` carries only the text form.
|
|
36
|
+
const docSource = typeof source === 'string' ? source : undefined
|
|
37
|
+
|
|
38
|
+
// Warm (JIT-compiled) path: execute never fires, so recover the operation
|
|
39
|
+
// signature/type the cold path cached, keyed by source + operationName —
|
|
40
|
+
// by query text for a string, by document identity for a pre-parsed AST.
|
|
41
|
+
// Empty on the cold path — validate hasn't refined yet — where the request
|
|
42
|
+
// span is refined from the parsed document instead.
|
|
43
|
+
const cached = getCachedRequestOperation(source, operationName)
|
|
44
|
+
|
|
45
|
+
const span = this.startSpan(this.operationName({ id: 'request' }), {
|
|
46
|
+
service: this.config.service || this.serviceName(),
|
|
47
|
+
// The cached signature is the precise resource; otherwise provisional and
|
|
48
|
+
// refined by the validate sub-plugin once the document is parsed.
|
|
49
|
+
// `operationName` is the best name at the boundary; falls back to the
|
|
50
|
+
// operation signature once validate sees the document.
|
|
51
|
+
resource: cached?.signature || operationName || undefined,
|
|
52
|
+
kind: this.constructor.kind,
|
|
53
|
+
type: this.constructor.type,
|
|
54
|
+
meta: {
|
|
55
|
+
'graphql.operation.type': cached?.type,
|
|
56
|
+
'graphql.operation.name': cached?.name || operationName,
|
|
57
|
+
'graphql.source': this.config.source ? docSource : undefined,
|
|
58
|
+
},
|
|
59
|
+
}, ctx)
|
|
60
|
+
|
|
61
|
+
// Hand the span, the requested operation name, and the raw source to the
|
|
62
|
+
// validate sub-plugin running inside this store so it can refine the
|
|
63
|
+
// resource + operation tags from the parsed document (validate is the first
|
|
64
|
+
// boundary that has it) and cache them keyed by the source the request
|
|
65
|
+
// boundary saw. The raw source is the cache key — validate sees mercurius's
|
|
66
|
+
// internally parsed document, not the caller's source, and for a pre-parsed
|
|
67
|
+
// AST the two are different objects.
|
|
68
|
+
ctx.currentStore.graphqlRequestSpan = span
|
|
69
|
+
ctx.currentStore.graphqlRequestOperationName = operationName
|
|
70
|
+
ctx.currentStore.graphqlRequestSource = source
|
|
71
|
+
|
|
72
|
+
return ctx.currentStore
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
asyncEnd (ctx) {
|
|
76
|
+
/* istanbul ignore next: currentStore is populated for the request lifecycle; activeSpan is base-plugin fallback. */
|
|
77
|
+
const span = ctx?.currentStore?.span || this.activeSpan
|
|
78
|
+
/* istanbul ignore if: startSpan always populates currentStore for the request lifecycle. */
|
|
79
|
+
if (!span) return ctx.parentStore
|
|
80
|
+
|
|
81
|
+
const result = ctx.result
|
|
82
|
+
if (result?.errors?.length) {
|
|
83
|
+
span.setTag('error', result.errors[0])
|
|
84
|
+
for (const error of result.errors) {
|
|
85
|
+
extractErrorIntoSpanEvent(this._tracerConfig, span, error)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
span.finish()
|
|
90
|
+
|
|
91
|
+
return ctx.parentStore
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
error (ctx) {
|
|
95
|
+
/* istanbul ignore next: currentStore is populated for request errors; activeSpan is base-plugin fallback. */
|
|
96
|
+
const span = ctx?.currentStore?.span || this.activeSpan
|
|
97
|
+
/* istanbul ignore else: errors are only routed after the request span has started. */
|
|
98
|
+
if (span && ctx?.error) {
|
|
99
|
+
span.setTag('error', ctx.error)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = GraphQLRequestPlugin
|
|
@@ -1,5 +1,186 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { LRUCache } = require('../../../vendor/dist/lru-cache')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {{ signature?: string, type?: string, name?: string }} RequestOperation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const operationTypes = new Set(['query', 'mutation', 'subscription'])
|
|
10
|
+
|
|
11
|
+
// Mercurius funnels every operation through `fastifyGraphQl`, but the parsed
|
|
12
|
+
// document — and therefore the operation signature/type/name — is only known
|
|
13
|
+
// once mercurius parses internally. The top-level request span opens before
|
|
14
|
+
// that, and on the JIT warm path neither parse/validate nor execute fires, so
|
|
15
|
+
// the span would otherwise be left with only the provisional resource. The cold
|
|
16
|
+
// path caches the computed metadata; the request boundary reads it back on the
|
|
17
|
+
// warm path. Bounded so a flood of distinct queries can't grow it without limit.
|
|
18
|
+
//
|
|
19
|
+
// The key is the operation name plus the raw query text, not the source alone:
|
|
20
|
+
// mercurius keys its document LRU by source but compiles the JIT for a single
|
|
21
|
+
// `operationName`, and the compiled query then serves that operation for every
|
|
22
|
+
// later request that shares the source — regardless of the `operationName` those
|
|
23
|
+
// requests ask for. A source-only key would hand a warm request for operation B
|
|
24
|
+
// the metadata of whichever operation was cached last for that source (A),
|
|
25
|
+
// mislabeling the span. Operation names cannot contain a newline, so it is a
|
|
26
|
+
// safe separator that keeps the two parts from colliding.
|
|
27
|
+
const requestOperationCache = new LRUCache({ max: 500 })
|
|
28
|
+
|
|
29
|
+
// Mercurius also accepts a pre-parsed document AST as the source, which reaches
|
|
30
|
+
// the request boundary as an object rather than query text — so there is no
|
|
31
|
+
// string to key the LRU by. Mercurius keys its own document LRU by that source
|
|
32
|
+
// object's identity, and the same object reaches the boundary on the warm path,
|
|
33
|
+
// so a WeakMap keyed by the caller-owned document recovers the metadata without
|
|
34
|
+
// mutating the document and releases with it. The value carries the requested
|
|
35
|
+
// operation name so a JIT-only sibling selection is not handed another
|
|
36
|
+
// operation's metadata (same reason the string cache keys by operation name).
|
|
37
|
+
/** @type {WeakMap<object, Map<string | undefined, RequestOperation>>} */
|
|
38
|
+
const documentOperationCache = new WeakMap()
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {string} source - The raw query text; the same key mercurius uses.
|
|
42
|
+
* @param {string | undefined} operationName - The requested operation name.
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
function requestOperationKey (source, operationName) {
|
|
46
|
+
return `${operationName ?? ''}\n${source}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {unknown} source - Query text on the common path; a pre-parsed
|
|
51
|
+
* document AST otherwise. Any other shape (mercurius rejects it before
|
|
52
|
+
* execute) has no cache entry and yields undefined.
|
|
53
|
+
* @param {string | undefined} operationName - The requested operation name.
|
|
54
|
+
* @returns {RequestOperation | undefined}
|
|
55
|
+
*/
|
|
56
|
+
function getCachedRequestOperation (source, operationName) {
|
|
57
|
+
if (typeof source === 'string') {
|
|
58
|
+
return requestOperationCache.get(requestOperationKey(source, operationName))
|
|
59
|
+
}
|
|
60
|
+
if (source === null || typeof source !== 'object') return
|
|
61
|
+
return documentOperationCache.get(source)?.get(operationName)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A string source keys the text LRU; a document AST keys the WeakMap. Any other
|
|
66
|
+
* shape has no usable key — mercurius rejects it before execute, so the warm
|
|
67
|
+
* path never reaches the request span for it either.
|
|
68
|
+
*
|
|
69
|
+
* @param {unknown} source
|
|
70
|
+
* @returns {source is string | object}
|
|
71
|
+
*/
|
|
72
|
+
function isCacheableSource (source) {
|
|
73
|
+
return typeof source === 'string' || (source !== null && typeof source === 'object')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Select the operation definition matching `operationName`, or the first one
|
|
78
|
+
* when no name is given (graphql/mercurius default selection).
|
|
79
|
+
*
|
|
80
|
+
* @param {import('graphql').DocumentNode | undefined} document
|
|
81
|
+
* @param {string | undefined} operationName
|
|
82
|
+
* @returns {import('graphql').OperationDefinitionNode | undefined}
|
|
83
|
+
*/
|
|
84
|
+
function getOperation (document, operationName) {
|
|
85
|
+
/* istanbul ignore if: validate/execute only call this with a parsed GraphQL document. */
|
|
86
|
+
if (!document || !Array.isArray(document.definitions)) return
|
|
87
|
+
|
|
88
|
+
for (const definition of document.definitions) {
|
|
89
|
+
if (operationTypes.has(definition?.operation) &&
|
|
90
|
+
(!operationName || definition.name?.value === operationName)) {
|
|
91
|
+
return definition
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Refine the top-level graphql.request span (mercurius) from the parsed
|
|
98
|
+
* document and cache the metadata so the JIT warm path — where no sub-span
|
|
99
|
+
* fires — can recover the same tags at the request boundary.
|
|
100
|
+
*
|
|
101
|
+
* This runs at the first boundary that has the document (validate on the cold
|
|
102
|
+
* path, which also precedes a pre-execute validation failure). It is idempotent
|
|
103
|
+
* across the later execute boundary via the `ddRequestRefined` flag, and a
|
|
104
|
+
* no-op for graphql-js/apollo/yoga, which never open a request span.
|
|
105
|
+
*
|
|
106
|
+
* Every named operation in the document is cached, not just the selected one:
|
|
107
|
+
* a multi-operation document parses once, and a later request may select a
|
|
108
|
+
* sibling operation that mercurius then serves exclusively through its JIT path
|
|
109
|
+
* (no execute span), so its metadata has to be ready before that happens.
|
|
110
|
+
*
|
|
111
|
+
* @param {import('../../dd-trace/src/opentracing/span') | undefined} requestSpan
|
|
112
|
+
* @param {import('graphql').DocumentNode | undefined} document
|
|
113
|
+
* @param {unknown} requestSource - The raw source the request boundary saw:
|
|
114
|
+
* query text on the common path, a pre-parsed document AST otherwise. The
|
|
115
|
+
* cache is keyed by it, not by the parsed document, so the request boundary
|
|
116
|
+
* recovers the metadata on the warm path from the same value mercurius keys
|
|
117
|
+
* its own document LRU by. Any other shape has no usable key and is not
|
|
118
|
+
* cached (the warm path never reaches this span for it either).
|
|
119
|
+
* @param {string | undefined} operationName - The requested operation name.
|
|
120
|
+
* @param {boolean} calculateSignature - The graphql plugin's `signature` config.
|
|
121
|
+
*/
|
|
122
|
+
function refineRequestSpan (requestSpan, document, requestSource, operationName, calculateSignature) {
|
|
123
|
+
/* istanbul ignore if: validate only refines after the request span and parsed document exist. */
|
|
124
|
+
if (!requestSpan || requestSpan.ddRequestRefined || !document) return
|
|
125
|
+
requestSpan.ddRequestRefined = true
|
|
126
|
+
|
|
127
|
+
const operation = getOperation(document, operationName)
|
|
128
|
+
const type = operation?.operation
|
|
129
|
+
const name = operation?.name?.value
|
|
130
|
+
const signature = getSignature(document, name, type, calculateSignature)
|
|
131
|
+
|
|
132
|
+
if (signature) requestSpan.setTag('resource.name', signature)
|
|
133
|
+
if (type) requestSpan.setTag('graphql.operation.type', type)
|
|
134
|
+
if (name) requestSpan.setTag('graphql.operation.name', name)
|
|
135
|
+
|
|
136
|
+
if (!isCacheableSource(requestSource)) return
|
|
137
|
+
|
|
138
|
+
// Cache the selected operation under the requested name (undefined selects
|
|
139
|
+
// the document's first operation, so it shares the entry with that name).
|
|
140
|
+
cacheRequestOperation(requestSource, operationName, { signature, type, name })
|
|
141
|
+
|
|
142
|
+
// Cache every named operation so a JIT-only sibling selection is labeled from
|
|
143
|
+
// this single parse instead of falling back to a bare operation name.
|
|
144
|
+
for (const definition of document.definitions) {
|
|
145
|
+
const definitionName = definition?.name?.value
|
|
146
|
+
if (definitionName === undefined || !operationTypes.has(definition.operation)) continue
|
|
147
|
+
if (definitionName === operationName) continue
|
|
148
|
+
|
|
149
|
+
cacheRequestOperation(requestSource, definitionName, {
|
|
150
|
+
signature: getSignature(document, definitionName, definition.operation, calculateSignature),
|
|
151
|
+
type: definition.operation,
|
|
152
|
+
name: definitionName,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* @param {string | import('graphql').DocumentNode} source - Query text keys the
|
|
159
|
+
* text LRU; a caller-owned document AST keys the WeakMap (never mutated).
|
|
160
|
+
* @param {string | undefined} operationName - The requested operation name.
|
|
161
|
+
* @param {RequestOperation} operation
|
|
162
|
+
*/
|
|
163
|
+
function cacheRequestOperation (source, operationName, operation) {
|
|
164
|
+
if (typeof source === 'string') {
|
|
165
|
+
requestOperationCache.set(requestOperationKey(source, operationName), operation)
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let operations = documentOperationCache.get(source)
|
|
170
|
+
if (operations === undefined) {
|
|
171
|
+
operations = new Map()
|
|
172
|
+
documentOperationCache.set(source, operations)
|
|
173
|
+
}
|
|
174
|
+
operations.set(operationName, operation)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @param {{ errorExtensions?: string[] }} config Resolved plugin config; `errorExtensions` lists the
|
|
179
|
+
* GraphQL error `extensions` keys to copy onto the span event.
|
|
180
|
+
* @param {import('../../dd-trace/src/opentracing/span')} span
|
|
181
|
+
* @param {{ name?: string, message?: string, stack?: string, locations?: Array<{ line: number, column: number }>,
|
|
182
|
+
* path?: Array<string|number>, extensions?: Record<string, unknown> }} exc
|
|
183
|
+
*/
|
|
3
184
|
function extractErrorIntoSpanEvent (config, span, exc) {
|
|
4
185
|
const attributes = {}
|
|
5
186
|
|
|
@@ -29,8 +210,8 @@ function extractErrorIntoSpanEvent (config, span, exc) {
|
|
|
29
210
|
attributes.message = exc.message
|
|
30
211
|
}
|
|
31
212
|
|
|
32
|
-
if (config.
|
|
33
|
-
for (const ext of config.
|
|
213
|
+
if (config.errorExtensions) {
|
|
214
|
+
for (const ext of config.errorExtensions) {
|
|
34
215
|
if (exc.extensions?.[ext]) {
|
|
35
216
|
const value = exc.extensions[ext]
|
|
36
217
|
|
|
@@ -77,5 +258,8 @@ function getSignature (document, operationName, operationType, calculate) {
|
|
|
77
258
|
|
|
78
259
|
module.exports = {
|
|
79
260
|
extractErrorIntoSpanEvent,
|
|
261
|
+
getCachedRequestOperation,
|
|
262
|
+
getOperation,
|
|
80
263
|
getSignature,
|
|
264
|
+
refineRequestSpan,
|
|
81
265
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { storage } = require('../../datadog-core')
|
|
3
4
|
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
|
|
4
5
|
const GraphQLParsePlugin = require('./parse')
|
|
5
|
-
const { extractErrorIntoSpanEvent } = require('./utils')
|
|
6
|
+
const { extractErrorIntoSpanEvent, refineRequestSpan } = require('./utils')
|
|
7
|
+
|
|
8
|
+
const legacyStorage = storage('legacy')
|
|
6
9
|
|
|
7
10
|
class GraphQLValidatePlugin extends TracingPlugin {
|
|
8
11
|
static id = 'graphql'
|
|
@@ -15,6 +18,26 @@ class GraphQLValidatePlugin extends TracingPlugin {
|
|
|
15
18
|
const docSource = document ? GraphQLParsePlugin.documentSources.get(document) : undefined
|
|
16
19
|
const source = this.config.source && document && docSource
|
|
17
20
|
|
|
21
|
+
// Refine the top-level graphql.request span (mercurius) from the parsed
|
|
22
|
+
// document. validate is the first boundary that has it and precedes both
|
|
23
|
+
// execute and any pre-execute rejection (unknown field, GET mutation), so a
|
|
24
|
+
// failing request still ends up with a resource and operation tags. The
|
|
25
|
+
// request span, its operation name, and the raw source ride the active
|
|
26
|
+
// store the request boundary entered (validate's own `ctx.currentStore` is
|
|
27
|
+
// not populated yet). The cache is keyed by that raw source, not the parsed
|
|
28
|
+
// document — for a pre-parsed AST mercurius validates a structuredClone, so
|
|
29
|
+
// the document here is a different object from the one the boundary saw and
|
|
30
|
+
// recovers on the warm path. No-op for graphql-js/apollo/yoga, which never
|
|
31
|
+
// open a request span.
|
|
32
|
+
const requestStore = legacyStorage.getStore()
|
|
33
|
+
refineRequestSpan(
|
|
34
|
+
requestStore?.graphqlRequestSpan,
|
|
35
|
+
document,
|
|
36
|
+
requestStore?.graphqlRequestSource,
|
|
37
|
+
requestStore?.graphqlRequestOperationName,
|
|
38
|
+
this.config.signature
|
|
39
|
+
)
|
|
40
|
+
|
|
18
41
|
this.startSpan('graphql.validate', {
|
|
19
42
|
service: this.config.service,
|
|
20
43
|
type: 'graphql',
|
|
@@ -38,7 +61,7 @@ class GraphQLValidatePlugin extends TracingPlugin {
|
|
|
38
61
|
if (errors?.length) {
|
|
39
62
|
span.setTag('error', errors[0])
|
|
40
63
|
for (const err of errors) {
|
|
41
|
-
extractErrorIntoSpanEvent(this.
|
|
64
|
+
extractErrorIntoSpanEvent(this.config, span, err)
|
|
42
65
|
}
|
|
43
66
|
}
|
|
44
67
|
|
|
@@ -61,15 +61,6 @@ const isJestWorker = !!getEnvironmentVariable('JEST_WORKER_ID')
|
|
|
61
61
|
// https://github.com/facebook/jest/blob/d6ad15b0f88a05816c2fe034dd6900d28315d570/packages/jest-worker/src/types.ts#L38
|
|
62
62
|
const CHILD_MESSAGE_END = 2
|
|
63
63
|
|
|
64
|
-
function withTimeout (promise, timeoutMs) {
|
|
65
|
-
return new Promise(resolve => {
|
|
66
|
-
realSetTimeout(resolve, timeoutMs)
|
|
67
|
-
|
|
68
|
-
// Also resolve if the original promise resolves
|
|
69
|
-
promise.then(resolve)
|
|
70
|
-
})
|
|
71
|
-
}
|
|
72
|
-
|
|
73
64
|
class JestPlugin extends CiPlugin {
|
|
74
65
|
static id = 'jest'
|
|
75
66
|
|
|
@@ -451,6 +442,7 @@ class JestPlugin extends CiPlugin {
|
|
|
451
442
|
isAtrRetry,
|
|
452
443
|
finalStatus,
|
|
453
444
|
earlyFlakeAbortReason,
|
|
445
|
+
promises,
|
|
454
446
|
}) => {
|
|
455
447
|
span.setTag(TEST_STATUS, status)
|
|
456
448
|
if (finalStatus) {
|
|
@@ -481,12 +473,33 @@ class JestPlugin extends CiPlugin {
|
|
|
481
473
|
this.getTestTelemetryTags(span)
|
|
482
474
|
)
|
|
483
475
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
476
|
+
const finish = () => {
|
|
477
|
+
span.finish()
|
|
478
|
+
finishAllTraceSpans(span)
|
|
479
|
+
this.activeTestSpan = null
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (finalStatus) {
|
|
483
|
+
if (promises?.hitBreakpointPromise) {
|
|
484
|
+
finish()
|
|
485
|
+
this.cancelDiBreakpointHitWait()
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
if (promises && this.diBreakpointHitPromise) {
|
|
489
|
+
promises.hitBreakpointPromise = this.waitForPreparedDiBreakpointHit().then(finish)
|
|
490
|
+
return
|
|
491
|
+
}
|
|
492
|
+
finish()
|
|
493
|
+
this.cancelDiBreakpointHitWait()
|
|
494
|
+
return
|
|
495
|
+
}
|
|
496
|
+
finish()
|
|
497
|
+
if (status === 'fail' && promises?.hitBreakpointPromise) {
|
|
498
|
+
this.prepareDiBreakpointHitWait()
|
|
499
|
+
}
|
|
487
500
|
})
|
|
488
501
|
|
|
489
|
-
this.addSub('ci:jest:test:err', ({ span, error, shouldSetProbe, promises }) => {
|
|
502
|
+
this.addSub('ci:jest:test:err', ({ span, error, shouldSetProbe, shouldWaitForHitProbe, promises }) => {
|
|
490
503
|
if (error && span) {
|
|
491
504
|
span.setTag(TEST_STATUS, 'fail')
|
|
492
505
|
span.setTag('error', getFormattedError(error, this.repositoryRoot))
|
|
@@ -494,8 +507,11 @@ class JestPlugin extends CiPlugin {
|
|
|
494
507
|
const probeInformation = this.addDiProbe(error)
|
|
495
508
|
if (probeInformation) {
|
|
496
509
|
const { setProbePromise } = probeInformation
|
|
497
|
-
|
|
510
|
+
this.prepareDiBreakpointHitWait()
|
|
511
|
+
promises.isProbeReady = this.waitForDiOperation(setProbePromise)
|
|
498
512
|
}
|
|
513
|
+
} else if (shouldWaitForHitProbe && this.di) {
|
|
514
|
+
promises.hitBreakpointPromise = this.waitForDiBreakpointHits()
|
|
499
515
|
}
|
|
500
516
|
}
|
|
501
517
|
})
|