dd-trace 6.1.0 → 6.3.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 +1 -1
- package/index.d.ts +7 -0
- package/initialize.mjs +4 -2
- package/package.json +5 -5
- package/packages/datadog-instrumentations/src/cookie.js +7 -1
- package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +5 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +31 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/modelcontextprotocol-sdk.js +30 -54
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +19 -5
- package/packages/datadog-instrumentations/src/http/server.js +27 -0
- package/packages/datadog-instrumentations/src/mercurius.js +11 -0
- package/packages/datadog-instrumentations/src/modelcontextprotocol-sdk.js +248 -1
- package/packages/datadog-instrumentations/src/otel-sdk-trace.js +15 -0
- package/packages/datadog-instrumentations/src/playwright.js +40 -3
- package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
- package/packages/datadog-instrumentations/src/vitest-util.js +0 -3
- package/packages/datadog-instrumentations/src/vitest-worker.js +0 -1
- package/packages/datadog-plugin-azure-functions/src/index.js +2 -1
- package/packages/datadog-plugin-graphql/src/execute.js +4 -15
- package/packages/datadog-plugin-graphql/src/index.js +6 -0
- package/packages/datadog-plugin-graphql/src/request.js +104 -0
- package/packages/datadog-plugin-graphql/src/utils.js +177 -0
- package/packages/datadog-plugin-graphql/src/validate.js +24 -1
- package/packages/datadog-plugin-modelcontextprotocol-sdk/src/tracing.js +144 -26
- package/packages/datadog-plugin-modelcontextprotocol-sdk/src/utils.js +30 -0
- package/packages/datadog-plugin-playwright/src/index.js +1 -1
- package/packages/datadog-plugin-vitest/src/index.js +0 -8
- package/packages/datadog-shimmer/src/shimmer.js +9 -2
- package/packages/dd-trace/src/appsec/channels.js +1 -0
- package/packages/dd-trace/src/appsec/index.js +6 -3
- package/packages/dd-trace/src/appsec/lambda.js +8 -8
- package/packages/dd-trace/src/appsec/waf/waf_manager.js +1 -1
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +23 -16
- package/packages/dd-trace/src/config/config-types.d.ts +8 -0
- package/packages/dd-trace/src/config/defaults.js +20 -15
- package/packages/dd-trace/src/config/index.js +13 -3
- package/packages/dd-trace/src/config/parsers.js +79 -0
- 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/constants/tags.js +1 -0
- package/packages/dd-trace/src/llmobs/tagger.js +15 -1
- 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_processor.js +7 -5
- package/packages/dd-trace/src/opentelemetry/tracer_provider.js +32 -16
- package/packages/dd-trace/src/plugins/index.js +3 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +20 -11
- 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
|
@@ -105,6 +105,7 @@ const efdStartedOriginalTestKeys = new Set()
|
|
|
105
105
|
const efdSlowAbortedTests = new Set()
|
|
106
106
|
const ddPropertiesByTestId = new Map()
|
|
107
107
|
const ddPropertiesRequestsByTestId = new Map()
|
|
108
|
+
const disabledTestIds = new Set()
|
|
108
109
|
let rootDir = ''
|
|
109
110
|
let sessionProjects = []
|
|
110
111
|
|
|
@@ -113,6 +114,8 @@ const EFD_RETRY_COUNT_REQUEST = 'ddEfdRetryCountRequest'
|
|
|
113
114
|
const EFD_RETRY_COUNT_RESPONSE = 'ddEfdRetryCountResponse'
|
|
114
115
|
const DD_PROPERTIES_REQUEST = 'ddPropertiesRequest'
|
|
115
116
|
const DD_PROPERTIES_RESPONSE = 'ddProperties'
|
|
117
|
+
const kDdPlaywrightDisabledTestIds = Symbol('ddPlaywrightDisabledTestIds')
|
|
118
|
+
const kDdPlaywrightWorkerHostInstrumented = Symbol('ddPlaywrightWorkerHostInstrumented')
|
|
116
119
|
const kDdPlaywrightWorkerInstrumented = Symbol('ddPlaywrightWorkerInstrumented')
|
|
117
120
|
|
|
118
121
|
function isValidKnownTests (receivedKnownTests) {
|
|
@@ -991,14 +994,36 @@ function onDispatcherCreateWorker (dispatcher, worker) {
|
|
|
991
994
|
const projects = getProjectsFromDispatcher(dispatcher)
|
|
992
995
|
sessionProjects = projects
|
|
993
996
|
|
|
997
|
+
if (disabledTestIds.size && !worker[kDdPlaywrightWorkerHostInstrumented] &&
|
|
998
|
+
typeof worker.runTestGroup === 'function') {
|
|
999
|
+
Object.defineProperty(worker, kDdPlaywrightWorkerHostInstrumented, { value: true })
|
|
1000
|
+
shimmer.wrap(worker, 'runTestGroup', runTestGroup => function (runPayload) {
|
|
1001
|
+
// Serial retries can restore disabled tests that were filtered from the initial dispatcher groups.
|
|
1002
|
+
let disabledIds
|
|
1003
|
+
for (const { testId } of runPayload.entries) {
|
|
1004
|
+
if (disabledTestIds.has(testId)) {
|
|
1005
|
+
disabledIds ??= []
|
|
1006
|
+
disabledIds.push(testId)
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
if (disabledIds) {
|
|
1010
|
+
runPayload._ddDisabledTestIds = disabledIds
|
|
1011
|
+
}
|
|
1012
|
+
return runTestGroup.apply(this, arguments)
|
|
1013
|
+
})
|
|
1014
|
+
}
|
|
1015
|
+
|
|
994
1016
|
worker.on('testBegin', ({ testId }) => {
|
|
995
1017
|
const test = getTestByTestId(dispatcher, testId)
|
|
1018
|
+
if (!test) return
|
|
1019
|
+
|
|
996
1020
|
const browser = getBrowserNameFromProjects(projects, test)
|
|
997
1021
|
const shouldCreateTestSpan = test.expectedStatus === 'skipped'
|
|
998
1022
|
testBeginHandler(test, browser, shouldCreateTestSpan)
|
|
999
1023
|
})
|
|
1000
1024
|
worker.on('testEnd', ({ testId, status, errors, annotations }) => {
|
|
1001
1025
|
const test = getTestByTestId(dispatcher, testId)
|
|
1026
|
+
if (!test) return
|
|
1002
1027
|
|
|
1003
1028
|
const isTimeout = status === 'timedOut'
|
|
1004
1029
|
const testStatus = STATUS_TO_TEST_STATUS[status]
|
|
@@ -1308,6 +1333,7 @@ function runAllTestsWrapper (runAllTests, playwrightVersion) {
|
|
|
1308
1333
|
efdSlowAbortedTests.clear()
|
|
1309
1334
|
ddPropertiesByTestId.clear()
|
|
1310
1335
|
ddPropertiesRequestsByTestId.clear()
|
|
1336
|
+
disabledTestIds.clear()
|
|
1311
1337
|
|
|
1312
1338
|
// TODO: we can trick playwright into thinking the session passed by returning
|
|
1313
1339
|
// 'passed' here. We might be able to use this for both EFD and Test Management tests.
|
|
@@ -1552,6 +1578,7 @@ function processRootSuite (createRootSuiteReturnValue) {
|
|
|
1552
1578
|
if (testProperties.disabled) {
|
|
1553
1579
|
test._ddIsDisabled = true
|
|
1554
1580
|
if (!testProperties.attemptToFix) {
|
|
1581
|
+
disabledTestIds.add(test.id)
|
|
1555
1582
|
test.expectedStatus = 'skipped'
|
|
1556
1583
|
// setting test.expectedStatus to 'skipped' does not work for every case,
|
|
1557
1584
|
// so we need to filter out disabled tests in dispatcherRunWrapperNew,
|
|
@@ -1795,7 +1822,17 @@ function instrumentWorkerMainMethods (workerMain) {
|
|
|
1795
1822
|
let steps = []
|
|
1796
1823
|
const stepInfoByStepId = {}
|
|
1797
1824
|
|
|
1825
|
+
shimmer.wrap(workerMain, 'runTestGroup', runTestGroup => function (runPayload) {
|
|
1826
|
+
const disabledIds = runPayload._ddDisabledTestIds
|
|
1827
|
+
this[kDdPlaywrightDisabledTestIds] = disabledIds ? new Set(disabledIds) : undefined
|
|
1828
|
+
return runTestGroup.apply(this, arguments)
|
|
1829
|
+
})
|
|
1830
|
+
|
|
1798
1831
|
shimmer.wrap(workerMain, '_runTest', _runTest => async function (test) {
|
|
1832
|
+
if (this[kDdPlaywrightDisabledTestIds]?.has(test.id)) {
|
|
1833
|
+
test._ddIsDisabled = true
|
|
1834
|
+
test.expectedStatus = 'skipped'
|
|
1835
|
+
}
|
|
1799
1836
|
await waitForEfdRetryCount(test)
|
|
1800
1837
|
if (shouldSkipEfdRetry(test)) {
|
|
1801
1838
|
test._ddShouldSkipEfdRetry = true
|
|
@@ -2042,9 +2079,9 @@ function generateSummaryWrapper (generateSummary) {
|
|
|
2042
2079
|
return function (...args) {
|
|
2043
2080
|
for (const test of this.suite.allTests()) {
|
|
2044
2081
|
// https://github.com/microsoft/playwright/blob/bf92ffecff6f30a292b53430dbaee0207e0c61ad/packages/playwright/src/reporters/base.ts#L279
|
|
2045
|
-
const
|
|
2046
|
-
(!test.results.length || test.expectedStatus !== 'skipped')
|
|
2047
|
-
if (
|
|
2082
|
+
const shouldReportAsSkipped = test.outcome() === 'skipped' &&
|
|
2083
|
+
(disabledTestIds.has(test.id) || !test.results.length || test.expectedStatus !== 'skipped')
|
|
2084
|
+
if (shouldReportAsSkipped && !testsReportedInGenerateSummary.has(test)) {
|
|
2048
2085
|
testsReportedInGenerateSummary.add(test)
|
|
2049
2086
|
const {
|
|
2050
2087
|
_requireFile: testSuiteAbsolutePath,
|
|
@@ -457,7 +457,6 @@ async function runMainProcessSetup (ctx, frameworkVersion, testSpecifications, s
|
|
|
457
457
|
testCommand,
|
|
458
458
|
repositoryRoot: receivedRepositoryRoot,
|
|
459
459
|
codeOwnersEntries,
|
|
460
|
-
testEnvironmentMetadata,
|
|
461
460
|
} = testSessionConfiguration
|
|
462
461
|
repositoryRoot = receivedRepositoryRoot || repositoryRoot
|
|
463
462
|
if (!shouldInstallNoWorkerInit) {
|
|
@@ -467,7 +466,6 @@ async function runMainProcessSetup (ctx, frameworkVersion, testSpecifications, s
|
|
|
467
466
|
_ddTestCommand: testCommand,
|
|
468
467
|
_ddRepositoryRoot: repositoryRoot,
|
|
469
468
|
_ddCodeOwnersEntries: codeOwnersEntries,
|
|
470
|
-
_ddTestEnvironmentMetadata: testEnvironmentMetadata,
|
|
471
469
|
}, 'Could not send test session configuration to workers.')
|
|
472
470
|
}
|
|
473
471
|
}
|
|
@@ -116,7 +116,6 @@ function getProvidedContext () {
|
|
|
116
116
|
_ddTestCommand: testCommand,
|
|
117
117
|
_ddRepositoryRoot: repositoryRoot,
|
|
118
118
|
_ddCodeOwnersEntries: codeOwnersEntries,
|
|
119
|
-
_ddTestEnvironmentMetadata: testEnvironmentMetadata,
|
|
120
119
|
} = globalThis.__vitest_worker__.providedContext
|
|
121
120
|
|
|
122
121
|
return {
|
|
@@ -138,7 +137,6 @@ function getProvidedContext () {
|
|
|
138
137
|
testCommand,
|
|
139
138
|
repositoryRoot,
|
|
140
139
|
codeOwnersEntries,
|
|
141
|
-
testEnvironmentMetadata,
|
|
142
140
|
}
|
|
143
141
|
} catch {
|
|
144
142
|
log.error('Vitest workers could not parse provided context, so some features will not work.')
|
|
@@ -161,7 +159,6 @@ function getProvidedContext () {
|
|
|
161
159
|
testCommand: undefined,
|
|
162
160
|
repositoryRoot: undefined,
|
|
163
161
|
codeOwnersEntries: undefined,
|
|
164
|
-
testEnvironmentMetadata: undefined,
|
|
165
162
|
}
|
|
166
163
|
}
|
|
167
164
|
}
|
|
@@ -657,7 +657,6 @@ addHook({
|
|
|
657
657
|
testCommand: providedContext.testCommand,
|
|
658
658
|
repositoryRoot: providedContext.repositoryRoot,
|
|
659
659
|
codeOwnersEntries: providedContext.codeOwnersEntries,
|
|
660
|
-
testEnvironmentMetadata: providedContext.testEnvironmentMetadata,
|
|
661
660
|
}
|
|
662
661
|
testSuiteStartCh.runStores(testSuiteCtx, () => {})
|
|
663
662
|
const startTestsResponse = await startTests.apply(this, arguments)
|
|
@@ -65,6 +65,7 @@ class AzureFunctionsPlugin extends TracingPlugin {
|
|
|
65
65
|
service: this.serviceName(),
|
|
66
66
|
type: 'serverless',
|
|
67
67
|
meta,
|
|
68
|
+
childOf: null,
|
|
68
69
|
}, ctx)
|
|
69
70
|
|
|
70
71
|
if (isMessagingService) {
|
|
@@ -142,7 +143,7 @@ function mapTriggerTag (methodName) {
|
|
|
142
143
|
// message & messages & batch with cardinality of 1 == applicationProperties
|
|
143
144
|
// messages with cardinality of many == applicationPropertiesArray
|
|
144
145
|
function setSpanLinks (triggerType, tracer, span, ctx) {
|
|
145
|
-
const cardinality = ctx.invocationContext.options.trigger.cardinality
|
|
146
|
+
const cardinality = ctx.invocationContext.options.trigger.cardinality ?? 'one'
|
|
146
147
|
const triggerMetadata = ctx.invocationContext.triggerMetadata
|
|
147
148
|
const isServiceBus = triggerType === 'ServiceBus'
|
|
148
149
|
|
|
@@ -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: {
|
|
@@ -669,17 +669,6 @@ function defaultFieldResolver (source, args, contextValue, info) {
|
|
|
669
669
|
}
|
|
670
670
|
}
|
|
671
671
|
|
|
672
|
-
function getOperation (document, operationName) {
|
|
673
|
-
if (!document || !Array.isArray(document.definitions)) return
|
|
674
|
-
|
|
675
|
-
for (const definition of document.definitions) {
|
|
676
|
-
if (definition && types.has(definition.operation) &&
|
|
677
|
-
(!operationName || definition.name?.value === operationName)) {
|
|
678
|
-
return definition
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
672
|
function addVariableTags (config, span, variableValues) {
|
|
684
673
|
if (!variableValues || !config.variables) return
|
|
685
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
|
|
@@ -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.config, 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,179 @@
|
|
|
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
|
+
|
|
3
177
|
/**
|
|
4
178
|
* @param {{ errorExtensions?: string[] }} config Resolved plugin config; `errorExtensions` lists the
|
|
5
179
|
* GraphQL error `extensions` keys to copy onto the span event.
|
|
@@ -84,5 +258,8 @@ function getSignature (document, operationName, operationType, calculate) {
|
|
|
84
258
|
|
|
85
259
|
module.exports = {
|
|
86
260
|
extractErrorIntoSpanEvent,
|
|
261
|
+
getCachedRequestOperation,
|
|
262
|
+
getOperation,
|
|
87
263
|
getSignature,
|
|
264
|
+
refineRequestSpan,
|
|
88
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',
|