dd-trace 5.110.0 → 5.111.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/README.md +13 -8
- package/ci/init.js +21 -2
- package/ci/vitest-no-worker-init-setup.mjs +430 -0
- package/ext/tags.js +2 -0
- package/index.d.ts +13 -0
- package/initialize.mjs +5 -6
- package/loader-hook.mjs +76 -55
- package/package.json +9 -9
- package/packages/datadog-instrumentations/src/fs.js +8 -6
- package/packages/datadog-instrumentations/src/graphql.js +26 -495
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +201 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/mongoose.js +14 -2
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +1293 -0
- package/packages/datadog-instrumentations/src/vitest-main.js +1567 -0
- package/packages/datadog-instrumentations/src/vitest-util.js +249 -0
- package/packages/datadog-instrumentations/src/vitest-worker.js +753 -0
- package/packages/datadog-instrumentations/src/vitest.js +11 -1855
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +2 -2
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +18 -10
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +47 -11
- package/packages/datadog-plugin-cucumber/src/index.js +1 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +10 -4
- package/packages/datadog-plugin-cypress/src/support.js +45 -1
- package/packages/datadog-plugin-express/src/code_origin.js +1 -1
- package/packages/datadog-plugin-fastify/src/code_origin.js +1 -1
- package/packages/datadog-plugin-graphql/src/execute.js +627 -11
- package/packages/datadog-plugin-graphql/src/index.js +20 -8
- package/packages/datadog-plugin-graphql/src/parse.js +24 -4
- package/packages/datadog-plugin-graphql/src/validate.js +16 -5
- package/packages/datadog-plugin-http/src/index.js +6 -8
- package/packages/datadog-plugin-jest/src/index.js +1 -1
- package/packages/datadog-plugin-mocha/src/index.js +1 -1
- package/packages/datadog-plugin-openai/src/services.js +2 -2
- package/packages/datadog-plugin-playwright/src/index.js +1 -1
- package/packages/datadog-plugin-vitest/src/index.js +69 -63
- package/packages/datadog-shimmer/src/shimmer.js +37 -16
- package/packages/dd-trace/src/aiguard/sdk.js +2 -2
- package/packages/dd-trace/src/appsec/api_security/sampler.js +3 -3
- package/packages/dd-trace/src/appsec/channels.js +3 -1
- package/packages/dd-trace/src/appsec/downstream_requests.js +4 -4
- package/packages/dd-trace/src/appsec/graphql.js +5 -5
- package/packages/dd-trace/src/appsec/iast/analyzers/nosql-injection-mongodb-analyzer.js +26 -19
- package/packages/dd-trace/src/appsec/iast/security-controls/index.js +2 -2
- package/packages/dd-trace/src/appsec/iast/telemetry/index.js +2 -1
- package/packages/dd-trace/src/appsec/index.js +10 -1
- package/packages/dd-trace/src/appsec/lambda.js +135 -0
- package/packages/dd-trace/src/appsec/reporter.js +49 -10
- package/packages/dd-trace/src/appsec/telemetry/index.js +1 -1
- package/packages/dd-trace/src/appsec/waf/index.js +16 -4
- package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +4 -4
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +21 -6
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +2 -2
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +3 -2
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +55 -13
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +2 -2
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +17 -0
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +47 -26
- package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +2 -2
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +64 -53
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +3 -3
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +23 -5
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +128 -0
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +287 -0
- package/packages/dd-trace/src/config/defaults.js +3 -2
- package/packages/dd-trace/src/config/generated-config-types.d.ts +61 -57
- package/packages/dd-trace/src/config/helper.js +1 -0
- package/packages/dd-trace/src/config/index.js +35 -22
- package/packages/dd-trace/src/config/major-overrides.js +4 -2
- package/packages/dd-trace/src/config/remote_config.js +1 -1
- package/packages/dd-trace/src/config/supported-configurations.json +90 -56
- package/packages/dd-trace/src/constants.js +7 -0
- package/packages/dd-trace/src/datastreams/processor.js +11 -3
- package/packages/dd-trace/src/exporters/agent/index.js +1 -1
- package/packages/dd-trace/src/exporters/agentless/writer.js +4 -4
- package/packages/dd-trace/src/llmobs/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/base.js +4 -4
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
- package/packages/dd-trace/src/llmobs/sdk.js +3 -3
- package/packages/dd-trace/src/llmobs/span_processor.js +1 -1
- package/packages/dd-trace/src/llmobs/tagger.js +3 -3
- package/packages/dd-trace/src/llmobs/writers/base.js +1 -1
- package/packages/dd-trace/src/opentelemetry/metrics/index.js +54 -5
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +40 -0
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +169 -0
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +38 -39
- package/packages/dd-trace/src/opentelemetry/trace/index.js +4 -0
- package/packages/dd-trace/src/opentelemetry/tracer_provider.js +1 -1
- package/packages/dd-trace/src/opentracing/tracer.js +6 -1
- package/packages/dd-trace/src/plugin_manager.js +7 -7
- package/packages/dd-trace/src/plugins/ci_plugin.js +12 -3
- package/packages/dd-trace/src/plugins/util/llm.js +6 -1
- package/packages/dd-trace/src/plugins/util/test.js +11 -9
- package/packages/dd-trace/src/profiling/exporter_cli.js +2 -2
- package/packages/dd-trace/src/profiling/exporters/event_serializer.js +3 -3
- package/packages/dd-trace/src/profiling/profilers/wall.js +1 -1
- package/packages/dd-trace/src/proxy.js +8 -8
- package/packages/dd-trace/src/span_processor.js +5 -6
- package/packages/dd-trace/src/span_stats.js +96 -78
- package/packages/dd-trace/src/startup-log.js +2 -1
- package/packages/dd-trace/src/telemetry/dependencies.js +1 -1
- package/packages/dd-trace/src/telemetry/endpoints.js +6 -4
- package/packages/dd-trace/src/telemetry/index.js +2 -2
- package/packages/dd-trace/src/telemetry/logs/index.js +1 -1
- package/packages/dd-trace/src/telemetry/send-data.js +8 -8
- package/packages/dd-trace/src/telemetry/session-propagation.js +1 -1
- package/packages/dd-trace/src/telemetry/telemetry.js +9 -9
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/shell-quote/index.js +1 -1
- package/packages/datadog-plugin-graphql/src/resolve.js +0 -170
|
@@ -1,21 +1,117 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const dc = require('dc-polyfill')
|
|
4
|
+
|
|
5
|
+
const { storage } = require('../../datadog-core')
|
|
3
6
|
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
|
|
7
|
+
const GraphQLParsePlugin = require('./parse')
|
|
4
8
|
const { extractErrorIntoSpanEvent, getSignature } = require('./utils')
|
|
5
9
|
|
|
10
|
+
const legacyStorage = storage('legacy')
|
|
11
|
+
|
|
12
|
+
const types = new Set(['query', 'mutation', 'subscription'])
|
|
13
|
+
|
|
14
|
+
const iastResolveCh = dc.channel('apm:graphql:resolve:start')
|
|
15
|
+
const resolverStartCh = dc.channel('datadog:graphql:resolver:start')
|
|
16
|
+
const updateFieldCh = dc.channel('apm:graphql:resolve:updateField')
|
|
17
|
+
|
|
18
|
+
// AppSec/WAF abort gate. Published synchronously from bindStart with a
|
|
19
|
+
// payload carrying the abortController; subscribers that call
|
|
20
|
+
// `payload.abortController.abort()` signal a pre-execute abort. bindStart
|
|
21
|
+
// observes the aborted signal by replacing `ctx.arguments[0]` with a Proxy
|
|
22
|
+
// whose getters throw AbortError — the orchestrion-emitted wrapper's
|
|
23
|
+
// `try { __apm$traced() } catch { ...; throw err }` block then propagates
|
|
24
|
+
// the AbortError to the caller of graphql.execute.
|
|
25
|
+
const startExecuteCh = dc.channel('apm:graphql:execute:start')
|
|
26
|
+
|
|
27
|
+
const contexts = new WeakMap()
|
|
28
|
+
const instrumentedArgs = new WeakSet()
|
|
29
|
+
|
|
30
|
+
const patchedResolvers = new WeakSet()
|
|
31
|
+
const patchedTypes = new WeakSet()
|
|
32
|
+
|
|
33
|
+
// Module-level fast path: skip the resolver-side WeakMap lookup entirely
|
|
34
|
+
// when depth=0 disables resolver instrumentation.
|
|
35
|
+
let depthDisabled = false
|
|
36
|
+
|
|
37
|
+
class AbortError extends Error {
|
|
38
|
+
constructor (message) {
|
|
39
|
+
super(message)
|
|
40
|
+
this.name = 'AbortError'
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
6
44
|
class GraphQLExecutePlugin extends TracingPlugin {
|
|
7
45
|
static id = 'graphql'
|
|
8
46
|
static operation = 'execute'
|
|
9
47
|
static type = 'graphql'
|
|
10
48
|
static kind = 'server'
|
|
49
|
+
static prefix = 'tracing:orchestrion:graphql:apm:graphql:execute'
|
|
50
|
+
|
|
51
|
+
// @graphql-tools/executor (used by graphql-yoga) emits on a different channel
|
|
52
|
+
// prefix because the module name differs. Subscribe to both so Yoga execution
|
|
53
|
+
// produces graphql.execute spans.
|
|
54
|
+
static extraPrefixes = [
|
|
55
|
+
'tracing:orchestrion:@graphql-tools/executor:apm:graphql:execute',
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {{ depth?: number }} config
|
|
60
|
+
*/
|
|
61
|
+
configure (config) {
|
|
62
|
+
super.configure(config)
|
|
63
|
+
depthDisabled = config.depth === 0
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
addTraceSubs () {
|
|
67
|
+
super.addTraceSubs()
|
|
68
|
+
|
|
69
|
+
for (const prefix of this.constructor.extraPrefixes) {
|
|
70
|
+
const events = ['start', 'end', 'asyncStart', 'asyncEnd', 'error', 'finish']
|
|
71
|
+
|
|
72
|
+
for (const event of events) {
|
|
73
|
+
const bindName = `bind${event.charAt(0).toUpperCase()}${event.slice(1)}`
|
|
74
|
+
|
|
75
|
+
if (this[event]) {
|
|
76
|
+
this.addSub(`${prefix}:${event}`, message => {
|
|
77
|
+
this[event](message)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (this[bindName]) {
|
|
82
|
+
this.addBind(`${prefix}:${event}`, message => this[bindName](message))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
11
87
|
|
|
12
88
|
bindStart (ctx) {
|
|
13
|
-
const
|
|
89
|
+
const rawArgs = ctx.arguments
|
|
90
|
+
const objectForm = isObjectForm(rawArgs)
|
|
91
|
+
const args = readArgs(rawArgs, objectForm)
|
|
92
|
+
|
|
93
|
+
// Re-entrant execute() short-circuit (yoga's normalizedExecutor calls
|
|
94
|
+
// execute internally with the same arguments object — without this we'd
|
|
95
|
+
// double-span). The contextValue check catches object contexts; the args
|
|
96
|
+
// check also catches primitive contexts.
|
|
97
|
+
if (instrumentedArgs.has(rawArgs?.[0])) {
|
|
98
|
+
ctx.ddSkipped = true
|
|
99
|
+
return ctx.currentStore
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const { contextValue } = args
|
|
103
|
+
if (contextValue && typeof contextValue === 'object' && contexts.has(contextValue)) {
|
|
104
|
+
ctx.ddSkipped = true
|
|
105
|
+
return ctx.currentStore
|
|
106
|
+
}
|
|
14
107
|
|
|
15
|
-
const type = operation && operation.operation
|
|
16
|
-
const name = operation && operation.name && operation.name.value
|
|
17
108
|
const document = args.document
|
|
18
|
-
const
|
|
109
|
+
const docSource = document ? GraphQLParsePlugin.documentSources.get(document) : undefined
|
|
110
|
+
const operation = getOperation(document, args.operationName)
|
|
111
|
+
|
|
112
|
+
const type = operation?.operation
|
|
113
|
+
const name = operation?.name?.value
|
|
114
|
+
const source = this.config.source && docSource
|
|
19
115
|
|
|
20
116
|
ctx.collapse = this.config.collapse
|
|
21
117
|
|
|
@@ -33,25 +129,545 @@ class GraphQLExecutePlugin extends TracingPlugin {
|
|
|
33
129
|
|
|
34
130
|
addVariableTags(this.config, span, args.variableValues)
|
|
35
131
|
|
|
132
|
+
const abortController = new AbortController()
|
|
133
|
+
|
|
134
|
+
// AppSec/WAF synchronous-abort gate. Publish before any resolver-wrapping
|
|
135
|
+
// work — if a subscriber aborts, none of that work matters because we'll
|
|
136
|
+
// make execute's body throw AbortError before it reaches any resolvers.
|
|
137
|
+
// bindStart runs as a bindStore transform on
|
|
138
|
+
// tracing:orchestrion:graphql:apm:graphql:execute:start, which fires
|
|
139
|
+
// BEFORE the orchestrion-emitted wrapper publishes its own :start and
|
|
140
|
+
// BEFORE the wrapped fn runs. The subscriber gets the abortController
|
|
141
|
+
// synchronously; on return we observe `signal.aborted` and act.
|
|
142
|
+
if (startExecuteCh.hasSubscribers) {
|
|
143
|
+
startExecuteCh.publish({ abortController, args })
|
|
144
|
+
if (abortController.signal.aborted) {
|
|
145
|
+
// Replace ctx.arguments[0] with a Proxy that throws AbortError on any
|
|
146
|
+
// property access. The orchestrion wrapper calls
|
|
147
|
+
// `__apm$wrapped.apply(this, ctx.arguments)`; graphql.execute's body
|
|
148
|
+
// begins with `const { schema, document, ... } = args` so the
|
|
149
|
+
// destructure triggers the trap immediately. The wrapper catches and
|
|
150
|
+
// rethrows, propagating AbortError to graphql.execute's caller.
|
|
151
|
+
ctx.arguments[0] = new Proxy({}, {
|
|
152
|
+
get () { throw new AbortError('Aborted') },
|
|
153
|
+
has () { throw new AbortError('Aborted') },
|
|
154
|
+
})
|
|
155
|
+
ctx.ddAborted = true
|
|
156
|
+
return ctx.currentStore
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
ctx.ddArgs = setWrappedFieldResolver(rawArgs, args, objectForm, defaultFieldResolver)
|
|
161
|
+
if (ctx.ddArgs && typeof ctx.ddArgs === 'object') {
|
|
162
|
+
instrumentedArgs.add(ctx.ddArgs)
|
|
163
|
+
ctx.ddInstrumentedArgs = ctx.ddArgs
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const schema = args.schema
|
|
167
|
+
if (schema) {
|
|
168
|
+
wrapFields(schema._queryType)
|
|
169
|
+
wrapFields(schema._mutationType)
|
|
170
|
+
wrapFields(schema._subscriptionType)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const rootCtx = {
|
|
174
|
+
source: docSource,
|
|
175
|
+
config: this.config,
|
|
176
|
+
fields: new Map(),
|
|
177
|
+
pathCache: new Map(),
|
|
178
|
+
collapsedPathCache: this.config.collapse ? { byPath: new Map(), byParent: new Map() } : undefined,
|
|
179
|
+
abortController,
|
|
180
|
+
executeSpan: span,
|
|
181
|
+
plugin: this,
|
|
182
|
+
}
|
|
183
|
+
ctx.ddRootCtx = rootCtx
|
|
184
|
+
if (isWeakMapKey(contextValue)) {
|
|
185
|
+
contexts.set(contextValue, rootCtx)
|
|
186
|
+
ctx.ddContextValue = contextValue
|
|
187
|
+
} else {
|
|
188
|
+
// Primitive / non-keyable contextValue: stash rootCtx on the
|
|
189
|
+
// orchestrion-scoped store that runStores enters for execute. Wrapped
|
|
190
|
+
// resolvers read it via legacyStorage.getStore() and it unwinds with the
|
|
191
|
+
// frame — no enterWith store that leaks past execute.
|
|
192
|
+
ctx.currentStore.graphqlRootCtx = rootCtx
|
|
193
|
+
}
|
|
194
|
+
|
|
36
195
|
return ctx.currentStore
|
|
37
196
|
}
|
|
38
197
|
|
|
39
|
-
|
|
40
|
-
|
|
198
|
+
end (ctx) {
|
|
199
|
+
if (ctx.ddSkipped) return ctx.parentStore
|
|
200
|
+
|
|
201
|
+
const span = ctx?.currentStore?.span || this.activeSpan
|
|
202
|
+
if (!span) return
|
|
203
|
+
|
|
204
|
+
// Synchronous execute() throw (e.g. execute(null, doc)) — error handler
|
|
205
|
+
// already tagged the span, just finish it.
|
|
206
|
+
if (ctx.error) {
|
|
207
|
+
this.#drain(ctx, span)
|
|
208
|
+
return ctx.parentStore
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const result = ctx.result
|
|
212
|
+
|
|
213
|
+
if (typeof result?.then === 'function') {
|
|
214
|
+
result.then(
|
|
215
|
+
(res) => this.#finishSpan(ctx, span, res),
|
|
216
|
+
(err) => {
|
|
217
|
+
span.setTag('error', err)
|
|
218
|
+
this.#drain(ctx, span)
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
} else {
|
|
222
|
+
this.#finishSpan(ctx, span, result)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return ctx.parentStore
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
error (ctx) {
|
|
229
|
+
// Pre-execute WAF abort isn't an error condition — opSpan.error must
|
|
230
|
+
// stay 0 per master's contract.
|
|
231
|
+
if (ctx.ddAborted) return
|
|
41
232
|
const span = ctx?.currentStore?.span || this.activeSpan
|
|
42
|
-
|
|
43
|
-
|
|
233
|
+
if (span && ctx?.error) {
|
|
234
|
+
span.setTag('error', ctx.error)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#finishSpan (ctx, span, res) {
|
|
239
|
+
this.config.hooks.execute(span, ctx.ddArgs, res)
|
|
240
|
+
|
|
241
|
+
if (res?.errors?.length) {
|
|
242
|
+
span.setTag('error', res.errors[0])
|
|
44
243
|
for (const err of res.errors) {
|
|
45
244
|
extractErrorIntoSpanEvent(this._tracerConfig, span, err)
|
|
46
245
|
}
|
|
47
246
|
}
|
|
48
|
-
super.finish(ctx)
|
|
49
247
|
|
|
50
|
-
|
|
248
|
+
this.#drain(ctx, span)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#drain (ctx, span) {
|
|
252
|
+
span.finish()
|
|
253
|
+
if (ctx.ddContextValue) {
|
|
254
|
+
contexts.delete(ctx.ddContextValue)
|
|
255
|
+
}
|
|
256
|
+
if (ctx.ddInstrumentedArgs) {
|
|
257
|
+
instrumentedArgs.delete(ctx.ddInstrumentedArgs)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Public — called from wrapResolve (free function, crosses class boundary).
|
|
262
|
+
// Resolve-span creation is inline at first-encounter; deferring to a batch
|
|
263
|
+
// produces a bursty encoder stall when many spans finish together.
|
|
264
|
+
startResolveSpan (field, rootCtx, executeSpan, startTime) {
|
|
265
|
+
const { fieldNode, fieldName, returnType, baseTypeName, variableValues, collapsedKey } = field
|
|
266
|
+
|
|
267
|
+
const parent = getParentField(rootCtx, field)
|
|
268
|
+
const childOf = parent?.span || executeSpan
|
|
269
|
+
|
|
270
|
+
const document = rootCtx.source
|
|
271
|
+
const loc = this.config.source && document && fieldNode && fieldNode.loc
|
|
272
|
+
const source = loc && document.slice(loc.start, loc.end)
|
|
273
|
+
|
|
274
|
+
const span = this.startSpan('graphql.resolve', {
|
|
275
|
+
service: this.config.service,
|
|
276
|
+
resource: `${fieldName}:${returnType}`,
|
|
277
|
+
childOf,
|
|
278
|
+
type: 'graphql',
|
|
279
|
+
startTime,
|
|
280
|
+
meta: {
|
|
281
|
+
'graphql.field.name': fieldName,
|
|
282
|
+
'graphql.field.path': collapsedKey,
|
|
283
|
+
'graphql.field.type': baseTypeName,
|
|
284
|
+
'graphql.source': source,
|
|
285
|
+
},
|
|
286
|
+
}, false)
|
|
287
|
+
|
|
288
|
+
if (fieldNode && this.config.variables && fieldNode.arguments) {
|
|
289
|
+
const variables = this.config.variables(variableValues)
|
|
290
|
+
for (const arg of fieldNode.arguments) {
|
|
291
|
+
if (arg.value?.name && arg.value.kind === 'Variable' && variables[arg.value.name.value]) {
|
|
292
|
+
const name = arg.value.name.value
|
|
293
|
+
span.setTag(`graphql.variables.${name}`, variables[name])
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return span
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Public — called from wrapResolve. endTime reflects when the resolver
|
|
302
|
+
// actually completed, not when the field record was created.
|
|
303
|
+
finishResolveSpan (span, field, error, result, endTime) {
|
|
304
|
+
if (error) span.setTag('error', error)
|
|
305
|
+
|
|
306
|
+
if (this.config.hooks.resolve) {
|
|
307
|
+
this.config.hooks.resolve(span, {
|
|
308
|
+
fieldName: field.fieldName,
|
|
309
|
+
path: field.pathString,
|
|
310
|
+
error: error || null,
|
|
311
|
+
// Any thenable from any realm — keep undefined so the hook doesn't
|
|
312
|
+
// accidentally see the unresolved promise.
|
|
313
|
+
result: typeof result?.then === 'function' ? undefined : result,
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
span.finish(endTime)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// --- resolver wrapping --------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
function wrapResolve (resolve) {
|
|
324
|
+
if (typeof resolve !== 'function' || patchedResolvers.has(resolve)) return resolve
|
|
325
|
+
|
|
326
|
+
function resolveAsync (source, args, contextValue, info) {
|
|
327
|
+
const hasIastSub = iastResolveCh.hasSubscribers
|
|
328
|
+
const hasResolverSub = resolverStartCh.hasSubscribers
|
|
329
|
+
|
|
330
|
+
// Combined fast-path: depth=0 AND no IAST/AppSec subscriber means nothing
|
|
331
|
+
// to do — skip rootCtx lookup, path walk, publish gates.
|
|
332
|
+
if (depthDisabled && !hasIastSub && !hasResolverSub) {
|
|
333
|
+
return resolve.apply(this, arguments)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const rootCtx = contexts.get(contextValue) ?? legacyStorage.getStore()?.graphqlRootCtx
|
|
337
|
+
if (!rootCtx) return resolve.apply(this, arguments)
|
|
338
|
+
|
|
339
|
+
const infoPath = info?.path
|
|
340
|
+
const config = rootCtx.config
|
|
341
|
+
|
|
342
|
+
// pathString built incrementally off the parent's cached value
|
|
343
|
+
// (rootCtx.pathCache, keyed by path node) — avoids re-walking the whole
|
|
344
|
+
// path linked-list on every resolver call, which is O(depth) per call for
|
|
345
|
+
// deeply nested resolvers. Shared between the IAST publish and the field
|
|
346
|
+
// record. Collapse-aware: list-index segments become '*'.
|
|
347
|
+
let pathString
|
|
348
|
+
let collapsedKey
|
|
349
|
+
if (infoPath) {
|
|
350
|
+
pathString = buildCachedPathString(infoPath, rootCtx.pathCache, config.collapse)
|
|
351
|
+
if (config.collapse) collapsedKey = pathString
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// IAST and AppSec subscribers see EVERY resolver call, regardless of
|
|
355
|
+
// depth or collapse. The depth knob caps span creation only.
|
|
356
|
+
if (hasIastSub) {
|
|
357
|
+
iastResolveCh.publish({ rootCtx, args, info, path: pathToArray(infoPath), pathString })
|
|
358
|
+
}
|
|
359
|
+
if (hasResolverSub) {
|
|
360
|
+
resolverStartCh.publish({
|
|
361
|
+
abortController: rootCtx.abortController,
|
|
362
|
+
resolverInfo: getResolverInfo(info, args),
|
|
363
|
+
})
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (depthDisabled || !shouldInstrumentNode(config, infoPath)) {
|
|
367
|
+
if (rootCtx.abortController?.signal.aborted) {
|
|
368
|
+
throw new AbortError('Aborted')
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return resolve.apply(this, arguments)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const fieldKey = config.collapse ? buildCachedCollapsedPath(infoPath, rootCtx.collapsedPathCache) : infoPath
|
|
375
|
+
let field = rootCtx.fields.get(fieldKey)
|
|
376
|
+
const isFirst = !field
|
|
377
|
+
|
|
378
|
+
if (isFirst) {
|
|
379
|
+
field = {
|
|
380
|
+
fieldNode: info.fieldNodes?.[0],
|
|
381
|
+
fieldName: info.fieldName,
|
|
382
|
+
returnType: info.returnType,
|
|
383
|
+
baseTypeName: getBaseTypeName(info.returnType),
|
|
384
|
+
variableValues: info.variableValues,
|
|
385
|
+
args,
|
|
386
|
+
infoPath,
|
|
387
|
+
fieldKey,
|
|
388
|
+
pathString,
|
|
389
|
+
collapsedKey: collapsedKey ?? pathString,
|
|
390
|
+
span: null,
|
|
391
|
+
}
|
|
392
|
+
rootCtx.fields.set(fieldKey, field)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Collapsed siblings still publish updateField (master's contract: one
|
|
396
|
+
// publish per resolver call, even when the span is collapsed) and route
|
|
397
|
+
// through callInAsyncScope so the abort signal stops them mid-flight.
|
|
398
|
+
if (!isFirst) {
|
|
399
|
+
return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, (err) => {
|
|
400
|
+
if (updateFieldCh.hasSubscribers) {
|
|
401
|
+
updateFieldCh.publish({ rootCtx, field, error: err, pathString: field.pathString })
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const executeSpan = rootCtx.executeSpan
|
|
407
|
+
const startTime = executeSpan._getTime()
|
|
408
|
+
const span = rootCtx.plugin.startResolveSpan(field, rootCtx, executeSpan, startTime)
|
|
409
|
+
field.span = span
|
|
410
|
+
|
|
411
|
+
return callInAsyncScope(resolve, this, arguments, rootCtx.abortController, (err, res) => {
|
|
412
|
+
const endTime = executeSpan._getTime()
|
|
413
|
+
rootCtx.plugin.finishResolveSpan(span, field, err, res, endTime || startTime)
|
|
414
|
+
if (updateFieldCh.hasSubscribers) {
|
|
415
|
+
updateFieldCh.publish({ rootCtx, field, error: err, pathString: field.pathString })
|
|
416
|
+
}
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
patchedResolvers.add(resolveAsync)
|
|
421
|
+
return resolveAsync
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function wrapFields (type) {
|
|
425
|
+
if (!type?._fields || patchedTypes.has(type)) return
|
|
426
|
+
|
|
427
|
+
patchedTypes.add(type)
|
|
428
|
+
|
|
429
|
+
for (const field of Object.values(type._fields)) {
|
|
430
|
+
wrapFieldResolve(field)
|
|
431
|
+
wrapFieldType(field)
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function wrapFieldResolve (field) {
|
|
436
|
+
if (!field?.resolve) return
|
|
437
|
+
field.resolve = wrapResolve(field.resolve)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function wrapFieldType (field) {
|
|
441
|
+
if (!field?.type) return
|
|
442
|
+
|
|
443
|
+
let unwrapped = field.type
|
|
444
|
+
while (unwrapped.ofType) unwrapped = unwrapped.ofType
|
|
445
|
+
|
|
446
|
+
wrapFields(unwrapped)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function callInAsyncScope (fn, thisArg, args, abortController, cb) {
|
|
450
|
+
cb ??= () => {}
|
|
451
|
+
|
|
452
|
+
if (abortController?.signal.aborted) {
|
|
453
|
+
cb(null, null)
|
|
454
|
+
throw new AbortError('Aborted')
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
try {
|
|
458
|
+
const result = fn.apply(thisArg, args)
|
|
459
|
+
if (typeof result?.then === 'function') {
|
|
460
|
+
return result.then(
|
|
461
|
+
res => { cb(null, res); return res },
|
|
462
|
+
err => { cb(err); throw err }
|
|
463
|
+
)
|
|
464
|
+
}
|
|
465
|
+
cb(null, result)
|
|
466
|
+
return result
|
|
467
|
+
} catch (err) {
|
|
468
|
+
cb(err)
|
|
469
|
+
throw err
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function pathToArray (path) {
|
|
474
|
+
let length = 0
|
|
475
|
+
for (let curr = path; curr; curr = curr.prev) {
|
|
476
|
+
length += 1
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const flattened = new Array(length)
|
|
480
|
+
let index = length
|
|
481
|
+
for (let curr = path; curr; curr = curr.prev) {
|
|
482
|
+
flattened[--index] = curr.key
|
|
483
|
+
}
|
|
484
|
+
return flattened
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Build the dotted pathString for a resolver's path node, caching per node on
|
|
488
|
+
// rootCtx.pathCache so each call reuses the parent's already-built string
|
|
489
|
+
// instead of re-walking the whole path linked-list (O(1) amortized per call).
|
|
490
|
+
// Collapse-aware: numeric (list-index) segments become '*'. The recursion
|
|
491
|
+
// handles the cold path where a parent node never hit a resolver (graphql
|
|
492
|
+
// inserts a synthetic array-index node between a list field and its items).
|
|
493
|
+
function buildCachedPathString (path, cache, collapse) {
|
|
494
|
+
const cached = cache.get(path)
|
|
495
|
+
if (cached !== undefined) return cached
|
|
496
|
+
|
|
497
|
+
const key = path.key
|
|
498
|
+
const segment = collapse && typeof key !== 'string' ? '*' : key
|
|
499
|
+
const prev = path.prev
|
|
500
|
+
|
|
501
|
+
const pathString = prev === undefined
|
|
502
|
+
? String(segment)
|
|
503
|
+
: `${buildCachedPathString(prev, cache, collapse)}.${segment}`
|
|
504
|
+
cache.set(path, pathString)
|
|
505
|
+
return pathString
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function buildCachedCollapsedPath (path, cache) {
|
|
509
|
+
if (!path) return
|
|
510
|
+
|
|
511
|
+
const cached = cache.byPath.get(path)
|
|
512
|
+
if (cached !== undefined) return cached
|
|
513
|
+
|
|
514
|
+
const segment = typeof path.key === 'string' ? path.key : '*'
|
|
515
|
+
const prev = path.prev === undefined
|
|
516
|
+
? undefined
|
|
517
|
+
: buildCachedCollapsedPath(path.prev, cache)
|
|
518
|
+
|
|
519
|
+
let siblings = cache.byParent.get(prev)
|
|
520
|
+
if (siblings === undefined) {
|
|
521
|
+
siblings = new Map()
|
|
522
|
+
cache.byParent.set(prev, siblings)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
let collapsedPath = siblings.get(segment)
|
|
526
|
+
if (collapsedPath === undefined) {
|
|
527
|
+
collapsedPath = { key: segment, prev }
|
|
528
|
+
siblings.set(segment, collapsedPath)
|
|
51
529
|
}
|
|
530
|
+
|
|
531
|
+
cache.byPath.set(path, collapsedPath)
|
|
532
|
+
return collapsedPath
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Depth filtering directly on the linked-list node — no array allocation needed.
|
|
536
|
+
// config.depth < 0 means no limit. Only selection-set segments (string keys)
|
|
537
|
+
// count toward depth; list indices are execution artifacts and are transparent.
|
|
538
|
+
// On the v5 line `countListIndices` keeps the legacy behaviour of counting every
|
|
539
|
+
// node when collapsing folds the numeric indices into '*'.
|
|
540
|
+
function shouldInstrumentNode (config, path) {
|
|
541
|
+
if (config.depth < 0) return true
|
|
542
|
+
|
|
543
|
+
let depth = 0
|
|
544
|
+
if (config.countListIndices) {
|
|
545
|
+
for (let curr = path; curr; curr = curr.prev) depth++
|
|
546
|
+
} else {
|
|
547
|
+
for (let curr = path; curr; curr = curr.prev) {
|
|
548
|
+
if (typeof curr.key === 'string') depth++
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return config.depth >= depth
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function getParentField (rootCtx, field) {
|
|
556
|
+
for (let curr = field.fieldKey?.prev; curr; curr = curr.prev) {
|
|
557
|
+
const innerField = rootCtx.fields.get(curr)
|
|
558
|
+
if (innerField) return innerField
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
return null
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Build the resolverInfo payload that AppSec's datadog:graphql:resolver:start
|
|
565
|
+
// subscriber expects: { [fieldName]: { ...args, ...directives } }.
|
|
566
|
+
function getResolverInfo (info, args) {
|
|
567
|
+
let resolverVars = args ? { ...args } : undefined
|
|
568
|
+
|
|
569
|
+
const directives = info.fieldNodes?.[0]?.directives
|
|
570
|
+
if (Array.isArray(directives)) {
|
|
571
|
+
for (const directive of directives) {
|
|
572
|
+
if (directive.arguments.length === 0) continue
|
|
573
|
+
|
|
574
|
+
const argList = {}
|
|
575
|
+
for (const argument of directive.arguments) {
|
|
576
|
+
argList[argument.name.value] = argument.value.value
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
resolverVars ??= {}
|
|
580
|
+
resolverVars[directive.name.value] = argList
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return resolverVars === undefined ? null : { [info.fieldName]: resolverVars }
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// --- arg / context normalization ---------------------------------------------
|
|
588
|
+
|
|
589
|
+
// graphql.execute accepts either a single args object or positional arguments;
|
|
590
|
+
// the object form is a lone non-array object in slot 0.
|
|
591
|
+
function isObjectForm (args) {
|
|
592
|
+
return args?.length === 1 && args[0] && typeof args[0] === 'object' && !Array.isArray(args[0])
|
|
52
593
|
}
|
|
53
594
|
|
|
54
|
-
|
|
595
|
+
function readArgs (args, objectForm) {
|
|
596
|
+
if (!args || args.length === 0) return {}
|
|
597
|
+
|
|
598
|
+
if (objectForm) {
|
|
599
|
+
return args[0]
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return {
|
|
603
|
+
schema: args[0],
|
|
604
|
+
document: args[1],
|
|
605
|
+
rootValue: args[2],
|
|
606
|
+
contextValue: args[3],
|
|
607
|
+
variableValues: args[4],
|
|
608
|
+
operationName: args[5],
|
|
609
|
+
fieldResolver: args[6],
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// No user input may be modified. Object-form clones rawArgs[0]; positional
|
|
614
|
+
// form rewrites its own arguments slots (no caller-observable mutation).
|
|
615
|
+
// Returns the readArgs-shaped view of the (possibly cloned) args so the caller
|
|
616
|
+
// doesn't have to re-readArgs after the swap.
|
|
617
|
+
function setWrappedFieldResolver (rawArgs, args, objectForm, defaultFieldResolver) {
|
|
618
|
+
if (!rawArgs || rawArgs.length === 0) return args
|
|
619
|
+
|
|
620
|
+
if (objectForm) {
|
|
621
|
+
const clone = {
|
|
622
|
+
...args,
|
|
623
|
+
fieldResolver: wrapResolve(args.fieldResolver || defaultFieldResolver),
|
|
624
|
+
}
|
|
625
|
+
rawArgs[0] = clone
|
|
626
|
+
return clone
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
rawArgs[6] = wrapResolve(args.fieldResolver || defaultFieldResolver)
|
|
630
|
+
if (rawArgs.length < 7) rawArgs.length = 7
|
|
631
|
+
args.fieldResolver = rawArgs[6]
|
|
632
|
+
return args
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function isWeakMapKey (value) {
|
|
636
|
+
return value !== null && (typeof value === 'object' || typeof value === 'function')
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Unwrap GraphQL List/NonNull wrappers to get the underlying named type's name.
|
|
640
|
+
// e.g. [Human] → 'Human', [Pet!] → 'Pet', String → 'String'
|
|
641
|
+
function getBaseTypeName (type) {
|
|
642
|
+
let cursor = type
|
|
643
|
+
while (cursor && cursor.ofType) cursor = cursor.ofType
|
|
644
|
+
return cursor?.name
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Fallback resolver used when graphql.execute() is called without an explicit
|
|
648
|
+
// fieldResolver and the schema field has no .resolve. Mirrors graphql's own
|
|
649
|
+
// defaultFieldResolver: property access on source, calling it if it's a function.
|
|
650
|
+
// Defined locally so it survives dd-trace plugin-manager reloads (agent.load()
|
|
651
|
+
// recreates globalThis[Symbol.for('dd-trace')], so capturing defaultFieldResolver
|
|
652
|
+
// via ddGlobal at IITM hook time would lose the reference across test suites).
|
|
653
|
+
function defaultFieldResolver (source, args, contextValue, info) {
|
|
654
|
+
if ((typeof source === 'object' && source !== null) || typeof source === 'function') {
|
|
655
|
+
const property = source[info.fieldName]
|
|
656
|
+
if (typeof property === 'function') return source[info.fieldName](args, contextValue, info)
|
|
657
|
+
return property
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
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
|
+
}
|
|
55
671
|
|
|
56
672
|
function addVariableTags (config, span, variableValues) {
|
|
57
673
|
if (!variableValues || !config.variables) return
|