dd-trace 6.3.0 → 6.5.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.electron.md +7 -0
- package/README.md +17 -0
- package/ci/diagnose.js +2100 -0
- package/ci/init.js +23 -2
- package/ci/runbook.md +198 -0
- package/ci/test-optimization-validation/approval-artifacts.js +143 -0
- package/ci/test-optimization-validation/approval.js +299 -0
- package/ci/test-optimization-validation/artifact-id.js +20 -0
- package/ci/test-optimization-validation/bounded-json.js +121 -0
- package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
- package/ci/test-optimization-validation/ci-discovery.js +181 -0
- package/ci/test-optimization-validation/ci-remediation.js +210 -0
- package/ci/test-optimization-validation/cli.js +903 -0
- package/ci/test-optimization-validation/command-blocker.js +81 -0
- package/ci/test-optimization-validation/command-output-policy.js +206 -0
- package/ci/test-optimization-validation/command-runner.js +707 -0
- package/ci/test-optimization-validation/command-suitability.js +471 -0
- package/ci/test-optimization-validation/executable-approval.js +48 -0
- package/ci/test-optimization-validation/executable.js +480 -0
- package/ci/test-optimization-validation/generated-file-policy.js +63 -0
- package/ci/test-optimization-validation/generated-files.js +351 -0
- package/ci/test-optimization-validation/generated-verifier.js +196 -0
- package/ci/test-optimization-validation/init-probe-preload.js +163 -0
- package/ci/test-optimization-validation/init-probe.js +246 -0
- package/ci/test-optimization-validation/late-initialization.js +63 -0
- package/ci/test-optimization-validation/local-command.js +163 -0
- package/ci/test-optimization-validation/manifest-loader.js +133 -0
- package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
- package/ci/test-optimization-validation/manifest-schema.js +862 -0
- package/ci/test-optimization-validation/offline-fixtures.js +327 -0
- package/ci/test-optimization-validation/offline-output.js +406 -0
- package/ci/test-optimization-validation/payload-normalizer.js +72 -0
- package/ci/test-optimization-validation/plan-writer.js +1120 -0
- package/ci/test-optimization-validation/preflight-runner.js +114 -0
- package/ci/test-optimization-validation/redaction.js +331 -0
- package/ci/test-optimization-validation/report-writer.js +1508 -0
- package/ci/test-optimization-validation/safe-files.js +203 -0
- package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
- package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
- package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
- package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
- package/ci/test-optimization-validation/setup-runner.js +97 -0
- package/ci/test-optimization-validation/static-diagnosis.js +159 -0
- package/ci/test-optimization-validation/test-output.js +129 -0
- package/ci/test-optimization-validation-manifest.schema.json +1 -0
- package/ci/validate-test-optimization.js +3 -0
- package/ext/exporters.js +1 -0
- package/index.d.ts +121 -8
- package/index.electron.js +3 -0
- package/init.js +18 -0
- package/initialize.mjs +1 -1
- package/loader-hook.mjs +28 -18
- package/openfeature.d.ts +1 -0
- package/openfeature.js +4 -0
- package/package.json +15 -8
- package/packages/datadog-instrumentations/src/ai.js +37 -26
- package/packages/datadog-instrumentations/src/child_process.js +1 -1
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
- package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
- package/packages/datadog-instrumentations/src/cucumber.js +24 -10
- package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
- package/packages/datadog-instrumentations/src/express.js +20 -2
- package/packages/datadog-instrumentations/src/fastify.js +7 -11
- package/packages/datadog-instrumentations/src/grpc/server.js +18 -0
- package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
- package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
- package/packages/datadog-instrumentations/src/helpers/router-helper.js +8 -8
- package/packages/datadog-instrumentations/src/http2/server.js +143 -17
- package/packages/datadog-instrumentations/src/jest.js +36 -4
- package/packages/datadog-instrumentations/src/mariadb.js +1 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
- package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
- package/packages/datadog-instrumentations/src/mongodb.js +3 -3
- package/packages/datadog-instrumentations/src/mongoose.js +3 -3
- package/packages/datadog-instrumentations/src/mquery.js +2 -2
- package/packages/datadog-instrumentations/src/mysql.js +1 -1
- package/packages/datadog-instrumentations/src/next.js +24 -0
- package/packages/datadog-instrumentations/src/nyc.js +0 -2
- package/packages/datadog-instrumentations/src/oracledb.js +2 -2
- package/packages/datadog-instrumentations/src/pg.js +2 -2
- package/packages/datadog-instrumentations/src/playwright.js +159 -22
- package/packages/datadog-instrumentations/src/process.js +3 -0
- package/packages/datadog-instrumentations/src/router.js +191 -106
- package/packages/datadog-instrumentations/src/selenium.js +52 -26
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
- package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
- package/packages/datadog-instrumentations/src/ws.js +2 -1
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +41 -16
- package/packages/datadog-plugin-aws-sdk/src/services/sns.js +21 -21
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +14 -8
- package/packages/datadog-plugin-aws-sdk/src/util.js +2 -24
- package/packages/datadog-plugin-cucumber/src/index.js +1 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +24 -62
- package/packages/datadog-plugin-cypress/src/index.js +6 -1
- package/packages/datadog-plugin-cypress/src/support.js +92 -26
- package/packages/datadog-plugin-electron/src/ipc.js +1 -1
- package/packages/datadog-plugin-graphql/src/execute.js +177 -59
- package/packages/datadog-plugin-graphql/src/parse.js +22 -1
- package/packages/datadog-plugin-graphql/src/request.js +32 -1
- package/packages/datadog-plugin-graphql/src/utils.js +42 -0
- package/packages/datadog-plugin-graphql/src/validate.js +15 -3
- package/packages/datadog-plugin-http2/src/client.js +1 -1
- package/packages/datadog-plugin-http2/src/server.js +38 -6
- package/packages/datadog-plugin-jest/src/util.js +21 -6
- package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
- package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
- package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
- package/packages/datadog-plugin-playwright/src/index.js +177 -64
- package/packages/datadog-plugin-router/src/index.js +11 -2
- package/packages/datadog-plugin-selenium/src/index.js +5 -36
- package/packages/datadog-plugin-vitest/src/index.js +4 -2
- package/packages/datadog-plugin-ws/src/close.js +2 -1
- package/packages/datadog-plugin-ws/src/producer.js +2 -1
- package/packages/datadog-plugin-ws/src/receiver.js +2 -1
- package/packages/datadog-plugin-ws/src/server.js +2 -1
- package/packages/dd-trace/index.electron.js +3 -0
- package/packages/dd-trace/index.js +2 -37
- package/packages/dd-trace/src/aiguard/sdk.js +1 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
- package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
- package/packages/dd-trace/src/azure_metadata.js +2 -1
- package/packages/dd-trace/src/bootstrap.js +39 -0
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +55 -5
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +22 -6
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +5 -0
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +62 -7
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -2
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
- package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
- package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +173 -9
- package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +20 -0
- package/packages/dd-trace/src/config/helper.js +1 -0
- package/packages/dd-trace/src/config/index.js +10 -0
- package/packages/dd-trace/src/config/supported-configurations.json +83 -0
- package/packages/dd-trace/src/constants.js +7 -0
- package/packages/dd-trace/src/datastreams/pathway.js +6 -4
- package/packages/dd-trace/src/exporter.js +2 -0
- package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
- package/packages/dd-trace/src/exporters/common/request.js +59 -30
- package/packages/dd-trace/src/exporters/common/url.js +15 -1
- package/packages/dd-trace/src/feature-registry.js +29 -0
- package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
- package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
- package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
- package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
- package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
- package/packages/dd-trace/src/llmobs/index.js +10 -2
- package/packages/dd-trace/src/llmobs/noop.js +6 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
- package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
- package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
- package/packages/dd-trace/src/llmobs/sdk.js +17 -0
- package/packages/dd-trace/src/llmobs/tagger.js +79 -7
- package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
- package/packages/dd-trace/src/llmobs/util.js +32 -0
- package/packages/dd-trace/src/noop/proxy.js +5 -4
- package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
- package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
- package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
- package/packages/dd-trace/src/openfeature/index.js +0 -2
- package/packages/dd-trace/src/openfeature/noop.js +1 -28
- package/packages/dd-trace/src/openfeature/register.js +35 -0
- package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
- package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
- package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
- package/packages/dd-trace/src/opentelemetry/span.js +5 -0
- package/packages/dd-trace/src/opentracing/propagation/log.js +11 -2
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +80 -14
- package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +10 -1
- package/packages/dd-trace/src/opentracing/tracer.js +7 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
- package/packages/dd-trace/src/plugins/util/git.js +3 -2
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
- package/packages/dd-trace/src/plugins/util/test.js +65 -8
- package/packages/dd-trace/src/plugins/util/web.js +12 -0
- package/packages/dd-trace/src/proxy.js +99 -10
- package/packages/dd-trace/src/span_processor.js +5 -0
- package/packages/dd-trace/src/standalone/index.js +0 -36
- package/packages/dd-trace/src/telemetry/send-data.js +2 -2
- package/packages/dd-trace/src/util.js +26 -0
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
- 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-instrumentations/src/helpers/require-optional-peer.js +0 -17
|
@@ -5,7 +5,7 @@ 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, getOperation, getSignature } = require('./utils')
|
|
8
|
+
const { extractErrorIntoSpanEvent, getOperation, getSignature, isApolloHealthCheck } = require('./utils')
|
|
9
9
|
|
|
10
10
|
const legacyStorage = storage('legacy')
|
|
11
11
|
|
|
@@ -26,12 +26,24 @@ const contexts = new WeakMap()
|
|
|
26
26
|
const instrumentedArgs = new WeakSet()
|
|
27
27
|
|
|
28
28
|
const patchedResolvers = new WeakSet()
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
// Visited types per caller-owned schema. The walk reaches union members and
|
|
31
|
+
// interface implementations through the schema (`getTypes`/`getPossibleTypes`),
|
|
32
|
+
// so it differs per schema: a global guard would stop the second schema at any
|
|
33
|
+
// type the first already walked and leave its own implementations unwrapped.
|
|
34
|
+
// `patchedResolvers` keeps wrapping idempotent, so re-walking a shared type is
|
|
35
|
+
// safe and this set only terminates cycles.
|
|
36
|
+
const walkedTypes = new WeakMap()
|
|
30
37
|
|
|
31
38
|
// Module-level fast path: skip the resolver-side WeakMap lookup entirely
|
|
32
39
|
// when depth=0 disables resolver instrumentation.
|
|
33
40
|
let depthDisabled = false
|
|
34
41
|
|
|
42
|
+
// Initial key for the per-operation variables-filter cache. A unique sentinel
|
|
43
|
+
// so the first #filterVariables call never falsely matches, even when the
|
|
44
|
+
// operation's variableValues is undefined.
|
|
45
|
+
const NO_VARIABLES_CACHED = Symbol('noVariablesCached')
|
|
46
|
+
|
|
35
47
|
class AbortError extends Error {
|
|
36
48
|
constructor (message) {
|
|
37
49
|
super(message)
|
|
@@ -111,6 +123,16 @@ class GraphQLExecutePlugin extends TracingPlugin {
|
|
|
111
123
|
const name = operation?.name?.value
|
|
112
124
|
const source = this.config.source && docSource
|
|
113
125
|
|
|
126
|
+
// Apollo Server may execute a cached document without parsing it first.
|
|
127
|
+
// Match the full gateway operation here so caller-owned AST transformations
|
|
128
|
+
// cannot suppress execute/resolver AppSec and IAST channels.
|
|
129
|
+
if (name === '__ApolloServiceHealthCheck__' &&
|
|
130
|
+
document.definitions.length === 1 &&
|
|
131
|
+
isApolloHealthCheck(operation)) {
|
|
132
|
+
ctx.ddSkipped = true
|
|
133
|
+
return ctx.currentStore
|
|
134
|
+
}
|
|
135
|
+
|
|
114
136
|
ctx.collapse = this.config.collapse
|
|
115
137
|
|
|
116
138
|
const signature = getSignature(document, name, type, this.config.signature)
|
|
@@ -165,9 +187,9 @@ class GraphQLExecutePlugin extends TracingPlugin {
|
|
|
165
187
|
|
|
166
188
|
const schema = args.schema
|
|
167
189
|
if (schema) {
|
|
168
|
-
wrapFields(schema._queryType)
|
|
169
|
-
wrapFields(schema._mutationType)
|
|
170
|
-
wrapFields(schema._subscriptionType)
|
|
190
|
+
wrapFields(schema._queryType, schema)
|
|
191
|
+
wrapFields(schema._mutationType, schema)
|
|
192
|
+
wrapFields(schema._subscriptionType, schema)
|
|
171
193
|
}
|
|
172
194
|
|
|
173
195
|
const rootCtx = {
|
|
@@ -175,10 +197,13 @@ class GraphQLExecutePlugin extends TracingPlugin {
|
|
|
175
197
|
config: this.config,
|
|
176
198
|
fields: new Map(),
|
|
177
199
|
pathCache: new Map(),
|
|
178
|
-
collapsedPathCache: this.config.collapse ? { byPath: new Map(), byParent: new Map() } : undefined,
|
|
179
200
|
abortController,
|
|
180
201
|
executeSpan: span,
|
|
181
202
|
plugin: this,
|
|
203
|
+
// graphql.resolve variable tags: memoize the filtered result for the
|
|
204
|
+
// last-seen variableValues object (see #filterVariables).
|
|
205
|
+
filteredVariablesKey: NO_VARIABLES_CACHED,
|
|
206
|
+
filteredVariables: undefined,
|
|
182
207
|
}
|
|
183
208
|
ctx.ddRootCtx = rootCtx
|
|
184
209
|
if (isWeakMapKey(contextValue)) {
|
|
@@ -202,9 +227,13 @@ class GraphQLExecutePlugin extends TracingPlugin {
|
|
|
202
227
|
if (!span) return
|
|
203
228
|
|
|
204
229
|
// Synchronous execute() throw (e.g. execute(null, doc)) — error handler
|
|
205
|
-
// already tagged the span
|
|
230
|
+
// already tagged the span.
|
|
206
231
|
if (ctx.error) {
|
|
207
|
-
|
|
232
|
+
if (ctx.ddAborted) {
|
|
233
|
+
span.finish()
|
|
234
|
+
} else {
|
|
235
|
+
this.#finishSpan(ctx, span)
|
|
236
|
+
}
|
|
208
237
|
return ctx.parentStore
|
|
209
238
|
}
|
|
210
239
|
|
|
@@ -213,10 +242,7 @@ class GraphQLExecutePlugin extends TracingPlugin {
|
|
|
213
242
|
if (typeof result?.then === 'function') {
|
|
214
243
|
result.then(
|
|
215
244
|
(res) => this.#finishSpan(ctx, span, res),
|
|
216
|
-
(err) =>
|
|
217
|
-
span.setTag('error', err)
|
|
218
|
-
this.#drain(ctx, span)
|
|
219
|
-
}
|
|
245
|
+
(err) => this.#finishSpan(ctx, span, undefined, err)
|
|
220
246
|
)
|
|
221
247
|
} else {
|
|
222
248
|
this.#finishSpan(ctx, span, result)
|
|
@@ -235,8 +261,16 @@ class GraphQLExecutePlugin extends TracingPlugin {
|
|
|
235
261
|
}
|
|
236
262
|
}
|
|
237
263
|
|
|
238
|
-
|
|
239
|
-
|
|
264
|
+
/**
|
|
265
|
+
* @param {object} ctx
|
|
266
|
+
* @param {import('../../dd-trace/src/opentracing/span')} span
|
|
267
|
+
* @param {import('graphql').ExecutionResult} [res]
|
|
268
|
+
* @param {unknown} [error]
|
|
269
|
+
*/
|
|
270
|
+
#finishSpan (ctx, span, res, error) {
|
|
271
|
+
if (error !== undefined) {
|
|
272
|
+
span.setTag('error', error)
|
|
273
|
+
}
|
|
240
274
|
|
|
241
275
|
if (res?.errors?.length) {
|
|
242
276
|
span.setTag('error', res.errors[0])
|
|
@@ -245,17 +279,16 @@ class GraphQLExecutePlugin extends TracingPlugin {
|
|
|
245
279
|
}
|
|
246
280
|
}
|
|
247
281
|
|
|
248
|
-
this.#drain(ctx, span)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
#drain (ctx, span) {
|
|
252
|
-
span.finish()
|
|
253
282
|
if (ctx.ddContextValue) {
|
|
254
283
|
contexts.delete(ctx.ddContextValue)
|
|
255
284
|
}
|
|
256
285
|
if (ctx.ddInstrumentedArgs) {
|
|
257
286
|
instrumentedArgs.delete(ctx.ddInstrumentedArgs)
|
|
258
287
|
}
|
|
288
|
+
|
|
289
|
+
this.config.hooks.execute(span, ctx.ddArgs, res)
|
|
290
|
+
|
|
291
|
+
span.finish()
|
|
259
292
|
}
|
|
260
293
|
|
|
261
294
|
// Public — called from wrapResolve (free function, crosses class boundary).
|
|
@@ -281,6 +314,7 @@ class GraphQLExecutePlugin extends TracingPlugin {
|
|
|
281
314
|
type: 'graphql',
|
|
282
315
|
startTime,
|
|
283
316
|
meta: {
|
|
317
|
+
'graphql.field.coordinates': `${field.parentTypeName}.${fieldName}`,
|
|
284
318
|
'graphql.field.name': fieldName,
|
|
285
319
|
'graphql.field.path': collapsedKey,
|
|
286
320
|
'graphql.field.type': baseTypeName,
|
|
@@ -291,7 +325,7 @@ class GraphQLExecutePlugin extends TracingPlugin {
|
|
|
291
325
|
field.span = span
|
|
292
326
|
|
|
293
327
|
if (fieldNode && this.config.variables && fieldNode.arguments) {
|
|
294
|
-
const variables = this
|
|
328
|
+
const variables = this.#filterVariables(rootCtx, variableValues)
|
|
295
329
|
for (const arg of fieldNode.arguments) {
|
|
296
330
|
if (arg.value?.name && arg.value.kind === 'Variable' && variables[arg.value.name.value]) {
|
|
297
331
|
const name = arg.value.name.value
|
|
@@ -303,6 +337,27 @@ class GraphQLExecutePlugin extends TracingPlugin {
|
|
|
303
337
|
return span
|
|
304
338
|
}
|
|
305
339
|
|
|
340
|
+
// Memoize the user variables filter against the last-seen variableValues
|
|
341
|
+
// object. graphql hands every resolver in one execute the same coerced
|
|
342
|
+
// variableValues object, so all arg-bearing fields hit the identity fast
|
|
343
|
+
// path and the filter runs once per operation. A nested execute() sharing
|
|
344
|
+
// the same object contextValue reuses the outer rootCtx but carries its own
|
|
345
|
+
// variableValues; comparing by identity recomputes for it (and any later
|
|
346
|
+
// fields on that inner object reuse the slot), so each field's tags stay
|
|
347
|
+
// correct. A single slot beats a WeakMap here: no per-operation allocation,
|
|
348
|
+
// and the common single-object case is a bare `===` (see the microbenchmark
|
|
349
|
+
// numbers in the commit body).
|
|
350
|
+
#filterVariables (rootCtx, variableValues) {
|
|
351
|
+
if (rootCtx.filteredVariablesKey === variableValues) {
|
|
352
|
+
return rootCtx.filteredVariables
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const filtered = this.config.variables(variableValues)
|
|
356
|
+
rootCtx.filteredVariablesKey = variableValues
|
|
357
|
+
rootCtx.filteredVariables = filtered
|
|
358
|
+
return filtered
|
|
359
|
+
}
|
|
360
|
+
|
|
306
361
|
// Public — called from wrapResolve. endTime reflects when the resolver
|
|
307
362
|
// actually completed, not when the field record was created.
|
|
308
363
|
finishResolveSpan (span, field, error, result, endTime) {
|
|
@@ -376,20 +431,35 @@ function wrapResolve (resolve) {
|
|
|
376
431
|
return resolve.apply(this, arguments)
|
|
377
432
|
}
|
|
378
433
|
|
|
379
|
-
const fieldKey = config.collapse ?
|
|
434
|
+
const fieldKey = config.collapse ? pathString : infoPath
|
|
435
|
+
const parentTypeName = info.parentType.name
|
|
380
436
|
let field = rootCtx.fields.get(fieldKey)
|
|
437
|
+
const collapsedField = field
|
|
438
|
+
if (config.collapse && field !== undefined && field.parentTypeName !== parentTypeName) {
|
|
439
|
+
const parentTypeFields = field.parentTypeFields
|
|
440
|
+
if (parentTypeFields?.parentTypeName === undefined) {
|
|
441
|
+
field = parentTypeFields?.get(parentTypeName)
|
|
442
|
+
} else if (parentTypeFields.parentTypeName === parentTypeName) {
|
|
443
|
+
field = parentTypeFields
|
|
444
|
+
} else {
|
|
445
|
+
field = undefined
|
|
446
|
+
}
|
|
447
|
+
if (field && infoPath.typename === undefined) {
|
|
448
|
+
cacheFieldByPath(rootCtx, infoPath, field)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
381
451
|
const isFirst = !field
|
|
382
452
|
|
|
383
453
|
if (isFirst) {
|
|
384
454
|
field = {
|
|
385
455
|
fieldNode: info.fieldNodes?.[0],
|
|
386
456
|
fieldName: info.fieldName,
|
|
457
|
+
parentTypeName,
|
|
387
458
|
returnType: info.returnType,
|
|
388
459
|
baseTypeName: getBaseTypeName(info.returnType),
|
|
389
460
|
variableValues: info.variableValues,
|
|
390
461
|
args,
|
|
391
462
|
infoPath,
|
|
392
|
-
fieldKey,
|
|
393
463
|
pathString,
|
|
394
464
|
collapsedKey: collapsedKey ?? pathString,
|
|
395
465
|
span: null,
|
|
@@ -398,7 +468,25 @@ function wrapResolve (resolve) {
|
|
|
398
468
|
parentStore: null,
|
|
399
469
|
currentStore: null,
|
|
400
470
|
}
|
|
401
|
-
|
|
471
|
+
if (config.collapse && collapsedField) {
|
|
472
|
+
const parentTypeFields = collapsedField.parentTypeFields
|
|
473
|
+
if (parentTypeFields === undefined) {
|
|
474
|
+
collapsedField.parentTypeFields = field
|
|
475
|
+
} else if (parentTypeFields.parentTypeName === undefined) {
|
|
476
|
+
parentTypeFields.set(parentTypeName, field)
|
|
477
|
+
} else {
|
|
478
|
+
const fieldsByParentType = new Map()
|
|
479
|
+
.set(collapsedField.parentTypeName, collapsedField)
|
|
480
|
+
.set(parentTypeFields.parentTypeName, parentTypeFields)
|
|
481
|
+
.set(parentTypeName, field)
|
|
482
|
+
collapsedField.parentTypeFields = fieldsByParentType
|
|
483
|
+
}
|
|
484
|
+
if (infoPath.typename === undefined) {
|
|
485
|
+
cacheFieldByPath(rootCtx, infoPath, field)
|
|
486
|
+
}
|
|
487
|
+
} else {
|
|
488
|
+
rootCtx.fields.set(fieldKey, field)
|
|
489
|
+
}
|
|
402
490
|
}
|
|
403
491
|
|
|
404
492
|
// Collapsed siblings still publish updateField (master's contract: one
|
|
@@ -432,15 +520,44 @@ function wrapResolve (resolve) {
|
|
|
432
520
|
return resolveAsync
|
|
433
521
|
}
|
|
434
522
|
|
|
435
|
-
function wrapFields (type) {
|
|
436
|
-
if (!type
|
|
523
|
+
function wrapFields (type, schema) {
|
|
524
|
+
if (!type || !markWalked(schema, type)) return
|
|
525
|
+
|
|
526
|
+
const tag = type[Symbol.toStringTag]
|
|
527
|
+
|
|
528
|
+
// Union types (e.g. Apollo Federation's `_Entity`) hold their members on
|
|
529
|
+
// `_types`, not `_fields`. Their member object types are reachable only here,
|
|
530
|
+
// so descend into each to wrap the entity resolvers a `_entities` query runs.
|
|
531
|
+
if (tag === 'GraphQLUnionType') {
|
|
532
|
+
for (const member of type.getTypes()) wrapFields(member, schema)
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (type._fields) {
|
|
537
|
+
for (const field of Object.values(type._fields)) {
|
|
538
|
+
wrapFieldResolve(field)
|
|
539
|
+
wrapFieldType(field, schema)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
437
542
|
|
|
438
|
-
|
|
543
|
+
// Interface implementations carry their own resolvers and are reachable only
|
|
544
|
+
// through `getPossibleTypes`; an interface return type alone never wraps them.
|
|
545
|
+
if (schema && tag === 'GraphQLInterfaceType') {
|
|
546
|
+
for (const impl of schema.getPossibleTypes(type)) wrapFields(impl, schema)
|
|
547
|
+
}
|
|
548
|
+
}
|
|
439
549
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
550
|
+
// Marks the guard on entry so recursive types (a field looping back to its own
|
|
551
|
+
// type, an interface an implementation returns) terminate the walk.
|
|
552
|
+
function markWalked (schema, type) {
|
|
553
|
+
let walked = walkedTypes.get(schema)
|
|
554
|
+
if (walked === undefined) {
|
|
555
|
+
walked = new WeakSet()
|
|
556
|
+
walkedTypes.set(schema, walked)
|
|
443
557
|
}
|
|
558
|
+
if (walked.has(type)) return false
|
|
559
|
+
walked.add(type)
|
|
560
|
+
return true
|
|
444
561
|
}
|
|
445
562
|
|
|
446
563
|
function wrapFieldResolve (field) {
|
|
@@ -448,13 +565,13 @@ function wrapFieldResolve (field) {
|
|
|
448
565
|
field.resolve = wrapResolve(field.resolve)
|
|
449
566
|
}
|
|
450
567
|
|
|
451
|
-
function wrapFieldType (field) {
|
|
568
|
+
function wrapFieldType (field, schema) {
|
|
452
569
|
if (!field?.type) return
|
|
453
570
|
|
|
454
571
|
let unwrapped = field.type
|
|
455
572
|
while (unwrapped.ofType) unwrapped = unwrapped.ofType
|
|
456
573
|
|
|
457
|
-
wrapFields(unwrapped)
|
|
574
|
+
wrapFields(unwrapped, schema)
|
|
458
575
|
}
|
|
459
576
|
|
|
460
577
|
// Runs the resolver inside `store`, including any code after an internal
|
|
@@ -516,31 +633,18 @@ function buildCachedPathString (path, cache, collapse) {
|
|
|
516
633
|
return pathString
|
|
517
634
|
}
|
|
518
635
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
if (siblings === undefined) {
|
|
532
|
-
siblings = new Map()
|
|
533
|
-
cache.byParent.set(prev, siblings)
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
let collapsedPath = siblings.get(segment)
|
|
537
|
-
if (collapsedPath === undefined) {
|
|
538
|
-
collapsedPath = { key: segment, prev }
|
|
539
|
-
siblings.set(segment, collapsedPath)
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
cache.byPath.set(path, collapsedPath)
|
|
543
|
-
return collapsedPath
|
|
636
|
+
/**
|
|
637
|
+
* @param {{ hasFieldsByPath?: boolean, fields: Map<string|object, object> }} rootCtx
|
|
638
|
+
* @param {object} path
|
|
639
|
+
* @param {object} field
|
|
640
|
+
*/
|
|
641
|
+
function cacheFieldByPath (rootCtx, path, field) {
|
|
642
|
+
// Leaf fields cannot parent resolver spans, so their concrete paths are never read.
|
|
643
|
+
if (field.fieldNode?.selectionSet === undefined) return
|
|
644
|
+
|
|
645
|
+
// Concrete info path objects cannot collide with collapsed path string keys.
|
|
646
|
+
rootCtx.hasFieldsByPath = true
|
|
647
|
+
rootCtx.fields.set(path, field)
|
|
544
648
|
}
|
|
545
649
|
|
|
546
650
|
// Depth filtering directly on the linked-list node — no array allocation needed.
|
|
@@ -564,9 +668,23 @@ function shouldInstrumentNode (config, path) {
|
|
|
564
668
|
}
|
|
565
669
|
|
|
566
670
|
function getParentField (rootCtx, field) {
|
|
567
|
-
for (let curr = field.
|
|
568
|
-
const
|
|
569
|
-
|
|
671
|
+
for (let curr = field.infoPath?.prev; curr; curr = curr.prev) {
|
|
672
|
+
const fieldKey = rootCtx.config.collapse ? rootCtx.pathCache.get(curr) : curr
|
|
673
|
+
const innerField = rootCtx.fields.get(fieldKey)
|
|
674
|
+
if (innerField) {
|
|
675
|
+
if (curr.typename === undefined) {
|
|
676
|
+
if (rootCtx.hasFieldsByPath) {
|
|
677
|
+
const fieldByPath = rootCtx.fields.get(curr)
|
|
678
|
+
if (fieldByPath) return fieldByPath
|
|
679
|
+
}
|
|
680
|
+
return innerField
|
|
681
|
+
}
|
|
682
|
+
if (innerField.parentTypeName === curr.typename) return innerField
|
|
683
|
+
|
|
684
|
+
const parentTypeFields = innerField.parentTypeFields
|
|
685
|
+
if (parentTypeFields.parentTypeName === undefined) return parentTypeFields.get(curr.typename)
|
|
686
|
+
return parentTypeFields
|
|
687
|
+
}
|
|
570
688
|
}
|
|
571
689
|
|
|
572
690
|
return null
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
|
|
4
|
+
const { isApolloHealthCheckSource } = require('./utils')
|
|
4
5
|
|
|
5
6
|
const documentSources = new WeakMap()
|
|
6
7
|
|
|
8
|
+
// Documents produced by parsing an Apollo Gateway health-check poll. Populated
|
|
9
|
+
// here (parse owns the document lifecycle, like documentSources) and read by the
|
|
10
|
+
// validate plugin. Execute independently verifies the operation for cached docs.
|
|
11
|
+
const healthCheckDocuments = new WeakSet()
|
|
12
|
+
|
|
7
13
|
class GraphQLParsePlugin extends TracingPlugin {
|
|
8
14
|
static id = 'graphql'
|
|
9
15
|
static operation = 'parser'
|
|
@@ -12,6 +18,14 @@ class GraphQLParsePlugin extends TracingPlugin {
|
|
|
12
18
|
bindStart (ctx) {
|
|
13
19
|
const source = ctx.arguments?.[0]
|
|
14
20
|
|
|
21
|
+
// Apollo Gateway polls every subgraph with a fixed health-check query.
|
|
22
|
+
// Mark its document after parsing so validation can skip the same poll.
|
|
23
|
+
if (isApolloHealthCheckSource(source?.body ?? source)) {
|
|
24
|
+
ctx.ddHealthCheck = true
|
|
25
|
+
ctx.ddSkipped = true
|
|
26
|
+
return ctx.currentStore
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
this.startSpan('graphql.parse', {
|
|
16
30
|
service: this.config.service,
|
|
17
31
|
type: 'graphql',
|
|
@@ -24,8 +38,14 @@ class GraphQLParsePlugin extends TracingPlugin {
|
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
end (ctx) {
|
|
27
|
-
const source = ctx.ddSource
|
|
28
41
|
const document = ctx.result
|
|
42
|
+
|
|
43
|
+
if (ctx.ddHealthCheck) {
|
|
44
|
+
if (document) healthCheckDocuments.add(document)
|
|
45
|
+
return ctx.parentStore
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const source = ctx.ddSource
|
|
29
49
|
const span = ctx?.currentStore?.span || this.activeSpan
|
|
30
50
|
|
|
31
51
|
let docSource
|
|
@@ -51,5 +71,6 @@ class GraphQLParsePlugin extends TracingPlugin {
|
|
|
51
71
|
}
|
|
52
72
|
|
|
53
73
|
GraphQLParsePlugin.documentSources = documentSources
|
|
74
|
+
GraphQLParsePlugin.healthCheckDocuments = healthCheckDocuments
|
|
54
75
|
|
|
55
76
|
module.exports = GraphQLParsePlugin
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
|
|
4
|
-
const { extractErrorIntoSpanEvent, getCachedRequestOperation } = require('./utils')
|
|
4
|
+
const { extractErrorIntoSpanEvent, getCachedRequestOperation, isApolloHealthCheckSource } = require('./utils')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {object} GraphQLRequestStore
|
|
8
|
+
* @property {import('../../dd-trace/src/opentracing/span')} [span]
|
|
9
|
+
* @property {import('../../dd-trace/src/opentracing/span')} [graphqlRequestSpan]
|
|
10
|
+
* @property {string} [graphqlRequestOperationName]
|
|
11
|
+
* @property {unknown} [graphqlRequestSource]
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {object} GraphQLRequestContext
|
|
16
|
+
* @property {unknown[]} [arguments]
|
|
17
|
+
* @property {GraphQLRequestStore} [currentStore]
|
|
18
|
+
* @property {GraphQLRequestStore} [parentStore]
|
|
19
|
+
* @property {boolean} [ddSkipped]
|
|
20
|
+
* @property {{ errors?: import('graphql').GraphQLError[] }} [result]
|
|
21
|
+
*/
|
|
5
22
|
|
|
6
23
|
// Top-level GraphQL request span for drivers that funnel every operation
|
|
7
24
|
// through a single entry point but parse/validate/execute internally (mercurius
|
|
@@ -25,9 +42,18 @@ class GraphQLRequestPlugin extends TracingPlugin {
|
|
|
25
42
|
static kind = 'server'
|
|
26
43
|
static prefix = 'tracing:orchestrion:mercurius:apm:graphql:request'
|
|
27
44
|
|
|
45
|
+
/**
|
|
46
|
+
* @param {GraphQLRequestContext} ctx
|
|
47
|
+
*/
|
|
28
48
|
bindStart (ctx) {
|
|
29
49
|
// fastifyGraphQl(source, context, variables, operationName)
|
|
30
50
|
const source = ctx.arguments?.[0]
|
|
51
|
+
|
|
52
|
+
if (isApolloHealthCheckSource(source)) {
|
|
53
|
+
ctx.ddSkipped = true
|
|
54
|
+
return ctx.currentStore
|
|
55
|
+
}
|
|
56
|
+
|
|
31
57
|
const operationName = ctx.arguments?.[3]
|
|
32
58
|
|
|
33
59
|
// `source` is the request text on the common path, but mercurius also
|
|
@@ -72,7 +98,12 @@ class GraphQLRequestPlugin extends TracingPlugin {
|
|
|
72
98
|
return ctx.currentStore
|
|
73
99
|
}
|
|
74
100
|
|
|
101
|
+
/**
|
|
102
|
+
* @param {GraphQLRequestContext} ctx
|
|
103
|
+
*/
|
|
75
104
|
asyncEnd (ctx) {
|
|
105
|
+
if (ctx.ddSkipped) return ctx.parentStore
|
|
106
|
+
|
|
76
107
|
/* istanbul ignore next: currentStore is populated for the request lifecycle; activeSpan is base-plugin fallback. */
|
|
77
108
|
const span = ctx?.currentStore?.span || this.activeSpan
|
|
78
109
|
/* istanbul ignore if: startSpan always populates currentStore for the request lifecycle. */
|
|
@@ -228,6 +228,46 @@ function extractErrorIntoSpanEvent (config, span, exc) {
|
|
|
228
228
|
span.addEvent('dd.graphql.query.error', attributes, Date.now())
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
// Apollo Gateway's fixed subgraph health-check query, sent verbatim on every
|
|
232
|
+
// poll interval. See https://github.com/apollographql/federation
|
|
233
|
+
// `HEALTH_CHECK_QUERY`.
|
|
234
|
+
const HEALTH_CHECK_QUERY = 'query __ApolloServiceHealthCheck__ { __typename }'
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Matches the raw query string before it is parsed (the only input parse has).
|
|
238
|
+
*
|
|
239
|
+
* @param {unknown} source Raw query string or a graphql `Source` body.
|
|
240
|
+
* @returns {boolean}
|
|
241
|
+
*/
|
|
242
|
+
function isApolloHealthCheckSource (source) {
|
|
243
|
+
return source === HEALTH_CHECK_QUERY
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Matches Apollo's parsed health-check operation exactly for cached documents.
|
|
248
|
+
*
|
|
249
|
+
* @param {import('graphql').OperationDefinitionNode | undefined} operation
|
|
250
|
+
* @returns {boolean}
|
|
251
|
+
*/
|
|
252
|
+
function isApolloHealthCheck (operation) {
|
|
253
|
+
const selections = operation?.selectionSet?.selections
|
|
254
|
+
if (operation?.operation !== 'query' ||
|
|
255
|
+
operation.name?.value !== '__ApolloServiceHealthCheck__' ||
|
|
256
|
+
operation.variableDefinitions?.length ||
|
|
257
|
+
operation.directives?.length ||
|
|
258
|
+
selections?.length !== 1) {
|
|
259
|
+
return false
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const selection = selections[0]
|
|
263
|
+
return selection.kind === 'Field' &&
|
|
264
|
+
selection.name?.value === '__typename' &&
|
|
265
|
+
selection.alias === undefined &&
|
|
266
|
+
selection.selectionSet === undefined &&
|
|
267
|
+
selection.arguments?.length === 0 &&
|
|
268
|
+
selection.directives?.length === 0
|
|
269
|
+
}
|
|
270
|
+
|
|
231
271
|
let tools
|
|
232
272
|
|
|
233
273
|
function getSignature (document, operationName, operationType, calculate) {
|
|
@@ -261,5 +301,7 @@ module.exports = {
|
|
|
261
301
|
getCachedRequestOperation,
|
|
262
302
|
getOperation,
|
|
263
303
|
getSignature,
|
|
304
|
+
isApolloHealthCheck,
|
|
305
|
+
isApolloHealthCheckSource,
|
|
264
306
|
refineRequestSpan,
|
|
265
307
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const { storage } = require('../../datadog-core')
|
|
4
4
|
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
|
|
5
5
|
const GraphQLParsePlugin = require('./parse')
|
|
6
|
-
const { extractErrorIntoSpanEvent, refineRequestSpan } = require('./utils')
|
|
6
|
+
const { extractErrorIntoSpanEvent, isApolloHealthCheck, refineRequestSpan } = require('./utils')
|
|
7
7
|
|
|
8
8
|
const legacyStorage = storage('legacy')
|
|
9
9
|
|
|
@@ -15,6 +15,16 @@ class GraphQLValidatePlugin extends TracingPlugin {
|
|
|
15
15
|
bindStart (ctx) {
|
|
16
16
|
// validate(schema, documentAST, rules, options, typeInfo)
|
|
17
17
|
const document = ctx.arguments?.[1]
|
|
18
|
+
|
|
19
|
+
// Verify the marked document in case the caller transformed its AST after parsing.
|
|
20
|
+
if (document &&
|
|
21
|
+
GraphQLParsePlugin.healthCheckDocuments.has(document) &&
|
|
22
|
+
document.definitions?.length === 1 &&
|
|
23
|
+
isApolloHealthCheck(document.definitions[0])) {
|
|
24
|
+
ctx.ddSkipped = true
|
|
25
|
+
return ctx.currentStore
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
const docSource = document ? GraphQLParsePlugin.documentSources.get(document) : undefined
|
|
19
29
|
const source = this.config.source && document && docSource
|
|
20
30
|
|
|
@@ -52,12 +62,12 @@ class GraphQLValidatePlugin extends TracingPlugin {
|
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
end (ctx) {
|
|
65
|
+
if (ctx.ddSkipped) return ctx.parentStore
|
|
66
|
+
|
|
55
67
|
const document = ctx.ddDocument
|
|
56
68
|
const errors = ctx.result
|
|
57
69
|
const span = ctx?.currentStore?.span || this.activeSpan
|
|
58
70
|
|
|
59
|
-
this.config.hooks.validate(span, document, errors)
|
|
60
|
-
|
|
61
71
|
if (errors?.length) {
|
|
62
72
|
span.setTag('error', errors[0])
|
|
63
73
|
for (const err of errors) {
|
|
@@ -65,6 +75,8 @@ class GraphQLValidatePlugin extends TracingPlugin {
|
|
|
65
75
|
}
|
|
66
76
|
}
|
|
67
77
|
|
|
78
|
+
this.config.hooks.validate(span, document, errors)
|
|
79
|
+
|
|
68
80
|
span.finish()
|
|
69
81
|
|
|
70
82
|
return ctx.parentStore
|
|
@@ -32,7 +32,7 @@ class Http2ClientPlugin extends ClientPlugin {
|
|
|
32
32
|
const { authority, options, headers = {} } = message
|
|
33
33
|
const sessionDetails = extractSessionDetails(authority, options)
|
|
34
34
|
const path = headers[HTTP2_HEADER_PATH] || '/'
|
|
35
|
-
const pathname = path.split(/[?#]
|
|
35
|
+
const pathname = path.split(/[?#]/, 1)[0]
|
|
36
36
|
const method = headers[HTTP2_HEADER_METHOD] || HTTP2_METHOD_GET
|
|
37
37
|
const base = `${sessionDetails.protocol}//${sessionDetails.host}:${sessionDetails.port}`
|
|
38
38
|
const uri = `${base}${pathname}`
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
// Plugin temporarily disabled. See https://github.com/DataDog/dd-trace-js/issues/312
|
|
4
|
-
|
|
5
3
|
const ServerPlugin = require('../../dd-trace/src/plugins/server')
|
|
6
4
|
const web = require('../../dd-trace/src/plugins/util/web')
|
|
7
5
|
const { COMPONENT, SVC_SRC_KEY } = require('../../dd-trace/src/constants')
|
|
@@ -10,6 +8,7 @@ class Http2ServerPlugin extends ServerPlugin {
|
|
|
10
8
|
constructor (tracer, config) {
|
|
11
9
|
super(tracer, config)
|
|
12
10
|
this.addBind('apm:http2:server:response:emit', this.bindEmit)
|
|
11
|
+
this.addSub('apm:http2:server:request:adopt', this.adopt)
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
static id = 'http2'
|
|
@@ -47,15 +46,35 @@ class Http2ServerPlugin extends ServerPlugin {
|
|
|
47
46
|
|
|
48
47
|
const context = web.getContext(req)
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
49
|
+
// A mixed server adopts the real request off this stream later; key the
|
|
50
|
+
// context on the stream now so that lookup resolves. Skipped for the common
|
|
51
|
+
// single-listener request, which never adopts.
|
|
52
|
+
if (ctx.adoptable) web.linkContextToStream(req.stream, context)
|
|
53
|
+
|
|
54
|
+
instrumentWriteHead(context)
|
|
54
55
|
|
|
55
56
|
return ctx.currentStore
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
// A mixed server (raw-stream + 'request' listeners) creates the span from the
|
|
60
|
+
// 'stream' event with a throwaway adapter. When the compatibility layer then
|
|
61
|
+
// synthesizes the real request/response off the same stream, point the shared
|
|
62
|
+
// context at them so `web.setFramework`/`web.setRoute` from the user's
|
|
63
|
+
// 'request' handler resolve to this span and the finish `hooks.request`
|
|
64
|
+
// receives the real objects instead of the adapter.
|
|
65
|
+
adopt (ctx) {
|
|
66
|
+
const context = web.patch(ctx.req)
|
|
67
|
+
context.req = ctx.req
|
|
68
|
+
context.res = ctx.res
|
|
69
|
+
instrumentWriteHead(context)
|
|
70
|
+
}
|
|
71
|
+
|
|
58
72
|
bindEmit (ctx) {
|
|
73
|
+
// Both the compatibility response and the core-API stream emit 'close'
|
|
74
|
+
// exactly once, so the span is finished from a single source. `web.js`
|
|
75
|
+
// bypasses its `finished` idempotency guard for stream-backed requests
|
|
76
|
+
// (`!req.stream`); that bypass is harmless here only because of this
|
|
77
|
+
// single-finish property.
|
|
59
78
|
if (ctx.eventName !== 'close') return ctx.currentStore
|
|
60
79
|
|
|
61
80
|
const { req } = ctx
|
|
@@ -78,4 +97,17 @@ class Http2ServerPlugin extends ServerPlugin {
|
|
|
78
97
|
}
|
|
79
98
|
}
|
|
80
99
|
|
|
100
|
+
// The core stream API has no `res.writeHead`; CORS preflight tagging only
|
|
101
|
+
// applies to the compatibility response that exposes it. Runs once per context:
|
|
102
|
+
// the mixed path calls it again from `adopt` once the real response is in place.
|
|
103
|
+
/**
|
|
104
|
+
* @param {{ res: { writeHead?: Function }, instrumented?: boolean }} context
|
|
105
|
+
*/
|
|
106
|
+
function instrumentWriteHead (context) {
|
|
107
|
+
if (!context.instrumented && typeof context.res.writeHead === 'function') {
|
|
108
|
+
context.res.writeHead = web.wrapWriteHead(context)
|
|
109
|
+
context.instrumented = true
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
81
113
|
module.exports = Http2ServerPlugin
|