dd-trace 5.108.0 → 5.110.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/ci/init.js +4 -3
- package/index.d.ts +88 -25
- package/loader-hook.mjs +88 -2
- package/package.json +19 -14
- package/packages/datadog-esbuild/index.js +26 -0
- package/packages/datadog-esbuild/src/utils.js +46 -3
- package/packages/datadog-instrumentations/src/ai.js +43 -48
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js-context-methods.js +18 -0
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +111 -0
- package/packages/datadog-instrumentations/src/aws-sdk.js +3 -1
- package/packages/datadog-instrumentations/src/connect.js +4 -3
- package/packages/datadog-instrumentations/src/electron.js +1 -1
- package/packages/datadog-instrumentations/src/fastify.js +4 -12
- package/packages/datadog-instrumentations/src/graphql.js +17 -6
- package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +81 -8
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/instrument.js +27 -2
- package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +65 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +6 -4
- package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +17 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/aws-durable-execution-sdk-js.js +31 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +18 -2
- package/packages/datadog-instrumentations/src/http/client.js +12 -2
- package/packages/datadog-instrumentations/src/ioredis.js +0 -1
- package/packages/datadog-instrumentations/src/iovalkey.js +1 -2
- package/packages/datadog-instrumentations/src/koa.js +3 -2
- package/packages/datadog-instrumentations/src/mariadb.js +4 -2
- package/packages/datadog-instrumentations/src/mocha/main.js +3 -3
- package/packages/datadog-instrumentations/src/next.js +44 -10
- package/packages/datadog-instrumentations/src/openai.js +77 -18
- package/packages/datadog-instrumentations/src/otel-sdk-trace.js +7 -6
- package/packages/datadog-instrumentations/src/playwright.js +1 -1
- package/packages/datadog-instrumentations/src/redis.js +0 -1
- package/packages/datadog-instrumentations/src/restify.js +4 -3
- package/packages/datadog-instrumentations/src/router.js +18 -5
- package/packages/datadog-instrumentations/src/selenium.js +1 -2
- package/packages/datadog-instrumentations/src/tedious.js +28 -0
- package/packages/datadog-instrumentations/src/vitest.js +323 -7
- package/packages/datadog-plugin-avsc/src/schema_iterator.js +2 -2
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/checkpoint.js +31 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +55 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +114 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +128 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/index.js +19 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +224 -0
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +43 -0
- package/packages/datadog-plugin-aws-sdk/src/base.js +14 -16
- package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +43 -6
- package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +100 -37
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +108 -99
- package/packages/datadog-plugin-bullmq/src/filter.js +35 -0
- package/packages/datadog-plugin-bullmq/src/producer.js +84 -4
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +9 -1
- package/packages/datadog-plugin-fs/src/index.js +1 -0
- package/packages/datadog-plugin-http/src/client.js +8 -3
- package/packages/datadog-plugin-http2/src/client.js +5 -2
- package/packages/datadog-plugin-mysql/src/index.js +7 -2
- package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +2 -2
- package/packages/datadog-plugin-redis/src/index.js +1 -2
- package/packages/datadog-plugin-undici/src/index.js +5 -2
- package/packages/datadog-plugin-vitest/src/index.js +11 -2
- package/packages/datadog-webpack/index.js +17 -1
- package/packages/datadog-webpack/src/optional-peer-loader.js +17 -0
- package/packages/dd-trace/src/aiguard/channels.js +0 -1
- package/packages/dd-trace/src/aiguard/index.js +11 -49
- package/packages/dd-trace/src/aiguard/integrations/evaluate.js +46 -0
- package/packages/dd-trace/src/aiguard/integrations/openai.js +66 -0
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +78 -0
- package/packages/{datadog-instrumentations/src/helpers/ai-messages.js → dd-trace/src/aiguard/messages/openai.js} +85 -193
- package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +185 -0
- package/packages/dd-trace/src/aiguard/sdk.js +13 -1
- package/packages/dd-trace/src/appsec/api_security/index.js +55 -0
- package/packages/dd-trace/src/appsec/api_security/sampler.js +147 -0
- package/packages/dd-trace/src/appsec/channels.js +1 -0
- package/packages/dd-trace/src/appsec/downstream_requests.js +111 -58
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +54 -12
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +5 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +29 -4
- package/packages/dd-trace/src/appsec/index.js +10 -6
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +19 -11
- package/packages/dd-trace/src/appsec/reporter.js +1 -0
- package/packages/dd-trace/src/appsec/telemetry/api_security.js +34 -0
- package/packages/dd-trace/src/appsec/telemetry/index.js +26 -0
- package/packages/dd-trace/src/ci-visibility/requests/request.js +1 -17
- package/packages/dd-trace/src/config/defaults.js +22 -3
- package/packages/dd-trace/src/config/generated-config-types.d.ts +538 -1
- package/packages/dd-trace/src/config/helper.js +133 -33
- package/packages/dd-trace/src/config/index.js +4 -1
- package/packages/dd-trace/src/config/parsers.js +19 -1
- package/packages/dd-trace/src/config/supported-configurations.json +45 -5
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +49 -35
- package/packages/dd-trace/src/debugger/devtools_client/index.js +31 -66
- package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +83 -0
- package/packages/dd-trace/src/debugger/devtools_client/state.js +1 -0
- package/packages/dd-trace/src/debugger/index.js +5 -0
- package/packages/dd-trace/src/debugger/probe_sampler.js +112 -0
- package/packages/dd-trace/src/debugger/probe_sampler_constants.js +19 -0
- package/packages/dd-trace/src/dogstatsd.js +15 -8
- package/packages/dd-trace/src/encode/agentless-ci-visibility.js +77 -26
- package/packages/dd-trace/src/exporters/agentless/index.js +7 -5
- package/packages/dd-trace/src/exporters/agentless/intake.js +43 -0
- package/packages/dd-trace/src/exporters/agentless/writer.js +5 -4
- package/packages/dd-trace/src/exporters/common/request.js +1 -20
- package/packages/dd-trace/src/exporters/common/url.js +34 -0
- package/packages/dd-trace/src/exporters/span-stats/writer.js +1 -3
- package/packages/dd-trace/src/guardrails/index.js +8 -5
- package/packages/dd-trace/src/index.js +6 -4
- package/packages/dd-trace/src/llmobs/constants/tags.js +6 -0
- package/packages/dd-trace/src/llmobs/index.js +16 -5
- package/packages/dd-trace/src/llmobs/plugins/genai/util.js +3 -3
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +5 -1
- package/packages/dd-trace/src/llmobs/sdk.js +5 -3
- package/packages/dd-trace/src/llmobs/span_processor.js +4 -0
- package/packages/dd-trace/src/llmobs/tagger.js +55 -0
- package/packages/dd-trace/src/llmobs/telemetry.js +1 -0
- package/packages/dd-trace/src/log/index.js +18 -20
- package/packages/dd-trace/src/openfeature/flagging_provider.js +3 -1
- package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +5 -4
- package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +2 -1
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +4 -3
- package/packages/dd-trace/src/opentelemetry/metrics/time.js +19 -0
- package/packages/dd-trace/src/opentelemetry/otlp/common.proto +2 -2
- package/packages/dd-trace/src/opentelemetry/otlp/logs.proto +1 -1
- package/packages/dd-trace/src/opentelemetry/otlp/logs_service.proto +1 -1
- package/packages/dd-trace/src/opentelemetry/otlp/metrics.proto +1 -1
- package/packages/dd-trace/src/opentelemetry/otlp/metrics_service.proto +1 -1
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +11 -4
- package/packages/dd-trace/src/opentelemetry/span-helpers.js +29 -13
- package/packages/dd-trace/src/opentelemetry/span.js +14 -3
- package/packages/dd-trace/src/opentelemetry/span_context.js +1 -0
- package/packages/dd-trace/src/opentelemetry/trace/index.js +2 -1
- package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +3 -2
- package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +26 -13
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -3
- package/packages/dd-trace/src/opentracing/span_context.js +17 -0
- package/packages/dd-trace/src/plugin_manager.js +7 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +36 -3
- package/packages/dd-trace/src/plugins/index.js +3 -0
- package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +276 -0
- package/packages/dd-trace/src/plugins/util/test.js +80 -0
- package/packages/dd-trace/src/plugins/util/url.js +68 -1
- package/packages/dd-trace/src/plugins/util/web.js +19 -36
- package/packages/dd-trace/src/priority_sampler.js +1 -13
- package/packages/dd-trace/src/profiling/config.js +129 -198
- package/packages/dd-trace/src/profiling/constants.js +4 -7
- package/packages/dd-trace/src/profiling/exporter_cli.js +10 -10
- package/packages/dd-trace/src/profiling/exporters/agent.js +23 -16
- package/packages/dd-trace/src/profiling/exporters/event_serializer.js +45 -14
- package/packages/dd-trace/src/profiling/exporters/file.js +16 -5
- package/packages/dd-trace/src/profiling/oom.js +71 -0
- package/packages/dd-trace/src/profiling/profiler.js +35 -23
- package/packages/dd-trace/src/profiling/profilers/events.js +14 -7
- package/packages/dd-trace/src/profiling/profilers/space.js +27 -18
- package/packages/dd-trace/src/profiling/profilers/wall.js +35 -25
- package/packages/dd-trace/src/ritm.js +4 -2
- package/packages/dd-trace/src/serverless.js +1 -2
- package/packages/dd-trace/src/service-naming/schemas/util.js +36 -5
- package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/web.js +5 -3
- package/packages/dd-trace/src/span_format.js +2 -1
- package/packages/dd-trace/src/span_processor.js +6 -0
- package/packages/dd-trace/src/telemetry/send-data.js +12 -10
- package/packages/dd-trace/src/tracer.js +15 -1
- package/packages/dd-trace/src/util.js +14 -0
- package/register.js +58 -1
- 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/pprof-format/index.js +1 -1
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +0 -284
- package/packages/dd-trace/src/appsec/api_security_sampler.js +0 -117
|
@@ -8,11 +8,18 @@ function recordDataAsString (data) {
|
|
|
8
8
|
return Buffer.isBuffer(data) ? data.toString('utf8') : Buffer.from(data).toString('utf8')
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// Caps the promise-path iterator→stream cache so abandoned shard iterators
|
|
12
|
+
// (AWS expires them after 5 minutes) can't grow it without bound. Polling loops
|
|
13
|
+
// delete on consume, so their working set is ~the active shard count.
|
|
14
|
+
const MAX_TRACKED_SHARD_ITERATORS = 1000
|
|
15
|
+
|
|
11
16
|
class Kinesis extends BaseAwsSdkPlugin {
|
|
12
17
|
static id = 'kinesis'
|
|
13
18
|
static peerServicePrecursors = ['streamname']
|
|
14
19
|
static isPayloadReporter = true
|
|
15
20
|
|
|
21
|
+
#shardIteratorStreams = new Map()
|
|
22
|
+
|
|
16
23
|
constructor (...args) {
|
|
17
24
|
super(...args)
|
|
18
25
|
|
|
@@ -20,51 +27,107 @@ class Kinesis extends BaseAwsSdkPlugin {
|
|
|
20
27
|
// in the base class
|
|
21
28
|
this.requestTags = new WeakMap()
|
|
22
29
|
|
|
23
|
-
this.addBind('apm:aws:response:start:kinesis', ctx =>
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
this.addBind('apm:aws:response:start:kinesis', ctx => this.#startResponseSpan(ctx))
|
|
31
|
+
|
|
32
|
+
// Promise / event-emitter calls never publish response:start, so create and finish the
|
|
33
|
+
// consumer span from request:complete instead. Callback calls handle it via the bind above.
|
|
34
|
+
this.addSub('apm:aws:request:complete:kinesis', ctx => {
|
|
35
|
+
if (ctx.cbExists) return
|
|
36
|
+
// v2 nests the SDK payload under response.data; v3 spreads the output onto response.
|
|
37
|
+
const response = ctx.response?.data ?? ctx.response
|
|
38
|
+
const responseCtx = { request: ctx.request, response }
|
|
39
|
+
this.#startResponseSpan(responseCtx)
|
|
40
|
+
if (responseCtx.needsFinish) this.finish(responseCtx)
|
|
41
|
+
// The async store that carries streamName to getRecords on the callback path is
|
|
42
|
+
// absent here, so map each shard iterator to its stream for the DSM topic tag.
|
|
43
|
+
if (this.config.dsmEnabled) this.#trackShardStream(ctx.request, response)
|
|
44
|
+
})
|
|
26
45
|
|
|
27
|
-
|
|
46
|
+
this.addSub('apm:aws:response:finish:kinesis', ctx => {
|
|
47
|
+
if (!ctx.needsFinish) return
|
|
48
|
+
this.finish(ctx)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
28
51
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
52
|
+
/**
|
|
53
|
+
* @param {object} ctx Completion context carrying the SDK request and response.
|
|
54
|
+
*/
|
|
55
|
+
#startResponseSpan (ctx) {
|
|
56
|
+
const { request, response } = ctx
|
|
34
57
|
|
|
35
|
-
|
|
36
|
-
let span
|
|
37
|
-
const responseExtraction = this.responseExtract(request.params, request.operation, response)
|
|
38
|
-
if (responseExtraction && responseExtraction.maybeChildOf) {
|
|
39
|
-
ctx.needsFinish = true
|
|
40
|
-
const options = {
|
|
41
|
-
childOf: responseExtraction.maybeChildOf,
|
|
42
|
-
meta: {
|
|
43
|
-
...this.requestTags.get(request),
|
|
44
|
-
'span.kind': 'server',
|
|
45
|
-
},
|
|
46
|
-
integrationName: 'aws-sdk',
|
|
47
|
-
}
|
|
48
|
-
span = plugin.startSpan('aws.response', options, ctx)
|
|
49
|
-
store = ctx.currentStore
|
|
50
|
-
}
|
|
58
|
+
let store = this._parentMap.get(request)
|
|
51
59
|
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
// if we have either of these operations, we want to store the streamName param
|
|
61
|
+
// since it is not typically available during get/put records requests
|
|
62
|
+
if (request.operation === 'getShardIterator' || request.operation === 'listShards') {
|
|
63
|
+
return this.storeStreamName(request.params, request.operation, store)
|
|
64
|
+
}
|
|
54
65
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
if (request.operation === 'getRecords') {
|
|
67
|
+
let span
|
|
68
|
+
const responseExtraction = this.responseExtract(request.params, request.operation, response)
|
|
69
|
+
if (responseExtraction && responseExtraction.maybeChildOf) {
|
|
70
|
+
ctx.needsFinish = true
|
|
71
|
+
const options = {
|
|
72
|
+
childOf: responseExtraction.maybeChildOf,
|
|
73
|
+
meta: {
|
|
74
|
+
...this.requestTags.get(request),
|
|
75
|
+
'span.kind': 'server',
|
|
76
|
+
},
|
|
77
|
+
integrationName: 'aws-sdk',
|
|
78
|
+
}
|
|
79
|
+
span = this.startSpan('aws.response', options, ctx)
|
|
80
|
+
store = ctx.currentStore
|
|
59
81
|
}
|
|
60
82
|
|
|
61
|
-
|
|
62
|
-
|
|
83
|
+
if (this.config.dsmEnabled) {
|
|
84
|
+
// streamName rides the async store on the callback path; the promise path has no
|
|
85
|
+
// such link, so fall back to the iterator the producer returned.
|
|
86
|
+
const streamName = store?.streamName ?? this.#shardIteratorStreams.get(request.params.ShardIterator)
|
|
87
|
+
this.responseExtractDSMContext(request.operation, request.params, response, span || null, { streamName })
|
|
88
|
+
}
|
|
89
|
+
}
|
|
63
90
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
return store
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param {object} request SDK request; reads `operation` and `params`.
|
|
96
|
+
* @param {object} response SDK output; reads `ShardIterator` / `NextShardIterator`.
|
|
97
|
+
*/
|
|
98
|
+
#trackShardStream (request, response) {
|
|
99
|
+
if (request.operation === 'getShardIterator') {
|
|
100
|
+
this.#rememberShardStream(response?.ShardIterator, request.params?.StreamName)
|
|
101
|
+
} else if (request.operation === 'getRecords') {
|
|
102
|
+
this.#advanceShardStream(request.params?.ShardIterator, response?.NextShardIterator)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {string} [iterator] Shard iterator the producer returned.
|
|
108
|
+
* @param {string} [streamName] Stream the iterator belongs to.
|
|
109
|
+
*/
|
|
110
|
+
#rememberShardStream (iterator, streamName) {
|
|
111
|
+
if (!iterator || streamName === undefined) return
|
|
112
|
+
// FIFO-evict the oldest entry (Map keeps insertion order) when the cap is hit; only
|
|
113
|
+
// abandoned iterators get here, so no realistic test drives the cap (eviction ignored).
|
|
114
|
+
/* istanbul ignore if */
|
|
115
|
+
if (this.#shardIteratorStreams.size >= MAX_TRACKED_SHARD_ITERATORS) {
|
|
116
|
+
this.#shardIteratorStreams.delete(this.#shardIteratorStreams.keys().next().value)
|
|
117
|
+
}
|
|
118
|
+
this.#shardIteratorStreams.set(iterator, streamName)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {string} [consumedIterator] Iterator just passed to getRecords.
|
|
123
|
+
* @param {string} [nextIterator] NextShardIterator for the following poll.
|
|
124
|
+
*/
|
|
125
|
+
#advanceShardStream (consumedIterator, nextIterator) {
|
|
126
|
+
const streamName = this.#shardIteratorStreams.get(consumedIterator)
|
|
127
|
+
if (streamName === undefined) return
|
|
128
|
+
this.#shardIteratorStreams.delete(consumedIterator)
|
|
129
|
+
// carry the stream onto the next iterator so the polling loop keeps its topic
|
|
130
|
+
if (nextIterator) this.#rememberShardStream(nextIterator, streamName)
|
|
68
131
|
}
|
|
69
132
|
|
|
70
133
|
generateTags (params, operation, response) {
|
|
@@ -54,41 +54,16 @@ class Sqs extends BaseAwsSdkPlugin {
|
|
|
54
54
|
// in the base class
|
|
55
55
|
this.requestTags = new WeakMap()
|
|
56
56
|
|
|
57
|
-
this.addBind('apm:aws:response:start:sqs', ctx =>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
if (
|
|
67
|
-
parsedFirstBody = contextExtraction.parsedBody
|
|
68
|
-
firstBodyChecked = contextExtraction.bodyChecked === true
|
|
69
|
-
if (contextExtraction.datadogContext !== undefined) {
|
|
70
|
-
ctx.needsFinish = true
|
|
71
|
-
const options = {
|
|
72
|
-
childOf: contextExtraction.datadogContext,
|
|
73
|
-
meta: {
|
|
74
|
-
...this.requestTags.get(request),
|
|
75
|
-
'span.kind': 'server',
|
|
76
|
-
},
|
|
77
|
-
integrationName: 'aws-sdk',
|
|
78
|
-
}
|
|
79
|
-
parsedMessageAttributes = contextExtraction.parsedAttributes
|
|
80
|
-
span = this.startSpan('aws.response', options, ctx)
|
|
81
|
-
store = ctx.currentStore
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Extract DSM context after, as we might not have a parent-child but may have a DSM context.
|
|
86
|
-
this.responseExtractDSMContext(
|
|
87
|
-
request.operation, request.params, response, span ?? null,
|
|
88
|
-
{ parsedAttributes: parsedMessageAttributes, parsedFirstBody, firstBodyChecked }
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
return store
|
|
57
|
+
this.addBind('apm:aws:response:start:sqs', ctx => this.#startResponseSpan(ctx))
|
|
58
|
+
|
|
59
|
+
// No-callback receives (promises, event emitters) never publish response:start, so link and
|
|
60
|
+
// finish the consumer span here instead. Callback paths reach the same logic via the bind above.
|
|
61
|
+
this.addSub('apm:aws:request:complete:sqs', ctx => {
|
|
62
|
+
if (ctx.cbExists) return
|
|
63
|
+
// v2 nests the SDK payload under response.data; v3 spreads the output onto response.
|
|
64
|
+
const responseCtx = { request: ctx.request, response: ctx.response?.data ?? ctx.response }
|
|
65
|
+
this.#startResponseSpan(responseCtx)
|
|
66
|
+
if (responseCtx.needsFinish) this.finish(responseCtx)
|
|
92
67
|
})
|
|
93
68
|
|
|
94
69
|
this.addSub('apm:aws:response:finish:sqs', ctx => {
|
|
@@ -97,6 +72,57 @@ class Sqs extends BaseAwsSdkPlugin {
|
|
|
97
72
|
})
|
|
98
73
|
}
|
|
99
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Start the consumer (`aws.response`) span for a receive. The first message carrying trace
|
|
77
|
+
* context becomes the parent; every additional one fans in as a span link.
|
|
78
|
+
*
|
|
79
|
+
* @param {{ request: object, response: object, needsFinish?: boolean, currentStore?: object }} ctx
|
|
80
|
+
* @returns {object | undefined} The store to activate for the consumer span, else the parent store.
|
|
81
|
+
*/
|
|
82
|
+
#startResponseSpan (ctx) {
|
|
83
|
+
const { request, response } = ctx
|
|
84
|
+
const carriers = this.responseExtract(request.params, request.operation, response)
|
|
85
|
+
|
|
86
|
+
let store = this._parentMap.get(request)
|
|
87
|
+
let span
|
|
88
|
+
|
|
89
|
+
if (carriers !== undefined) {
|
|
90
|
+
// request:start records requestTags only after the isEnabled gate, so an absent entry
|
|
91
|
+
// means this consumer is disabled — gate on it instead of paying isEnabled again here.
|
|
92
|
+
const requestTags = this.requestTags.get(request)
|
|
93
|
+
if (requestTags !== undefined) {
|
|
94
|
+
// A receive can return messages from many producers; fanning the extra ones in as span
|
|
95
|
+
// links is the shape dd-trace-java and dd-trace-py use for batch SQS receives.
|
|
96
|
+
for (const carrier of carriers) {
|
|
97
|
+
if (carrier === undefined) continue
|
|
98
|
+
const datadogContext = this.tracer.extract('text_map', carrier)
|
|
99
|
+
// A DSM-only carrier (a non-first sendMessageBatch entry when batchPropagationEnabled
|
|
100
|
+
// is off) extracts to null; span.addLink dereferences the context and would throw on it.
|
|
101
|
+
if (datadogContext === null) continue
|
|
102
|
+
if (span === undefined) {
|
|
103
|
+
ctx.needsFinish = true
|
|
104
|
+
span = this.startSpan('aws.response', {
|
|
105
|
+
childOf: datadogContext,
|
|
106
|
+
meta: {
|
|
107
|
+
...requestTags,
|
|
108
|
+
'span.kind': 'server',
|
|
109
|
+
},
|
|
110
|
+
integrationName: 'aws-sdk',
|
|
111
|
+
}, ctx)
|
|
112
|
+
store = ctx.currentStore
|
|
113
|
+
} else {
|
|
114
|
+
span.addLink({ context: datadogContext })
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Extract DSM context after, as we might not have a parent-child but may have a DSM context.
|
|
121
|
+
this.responseExtractDSMContext(request.operation, request.params, response, span ?? null, carriers)
|
|
122
|
+
|
|
123
|
+
return store
|
|
124
|
+
}
|
|
125
|
+
|
|
100
126
|
operationFromRequest (request) {
|
|
101
127
|
switch (request.operation) {
|
|
102
128
|
case 'receiveMessage':
|
|
@@ -166,19 +192,39 @@ class Sqs extends BaseAwsSdkPlugin {
|
|
|
166
192
|
return tags
|
|
167
193
|
}
|
|
168
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Parse the trace-context carrier of every received message, in message order.
|
|
197
|
+
* Entries are `undefined` for messages that carry no `_datadog` context.
|
|
198
|
+
*
|
|
199
|
+
* @param {{ MaxNumberOfMessages?: number }} params
|
|
200
|
+
* @param {string} operation
|
|
201
|
+
* @param {{ Messages?: object[] }} response
|
|
202
|
+
* @returns {Array<Record<string, string> | undefined> | undefined}
|
|
203
|
+
*/
|
|
169
204
|
responseExtract (params, operation, response) {
|
|
170
205
|
if (operation !== 'receiveMessage') return
|
|
171
|
-
if (
|
|
172
|
-
if (!response || !response.Messages || !response.Messages[0]) return
|
|
206
|
+
if (!response?.Messages?.length) return
|
|
173
207
|
|
|
174
|
-
|
|
175
|
-
|
|
208
|
+
return response.Messages.map(message => this.parseMessageCarrier(message))
|
|
209
|
+
}
|
|
176
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Resolve the trace-context carrier for a single received message. The
|
|
213
|
+
* `MessageAttributes._datadog` text map (direct SQS or SNS to SQS) wins;
|
|
214
|
+
* otherwise the EventBridge envelope, optionally wrapped in an SNS
|
|
215
|
+
* `Notification` (see getEventBridgeContext). Checking MessageAttributes first
|
|
216
|
+
* avoids parsing a large SNS `Message` just to rule out an EventBridge envelope.
|
|
217
|
+
*
|
|
218
|
+
* @param {object} message A single `response.Messages` entry.
|
|
219
|
+
* @returns {Record<string, string> | undefined}
|
|
220
|
+
*/
|
|
221
|
+
parseMessageCarrier (message) {
|
|
222
|
+
let parsedBody
|
|
177
223
|
if (message.Body) {
|
|
178
224
|
try {
|
|
179
225
|
parsedBody = JSON.parse(message.Body)
|
|
180
226
|
} catch {
|
|
181
|
-
// SQS to SQS
|
|
227
|
+
// Opaque, non-JSON body (SQS to SQS).
|
|
182
228
|
}
|
|
183
229
|
// SNS to SQS
|
|
184
230
|
if (parsedBody?.Type === 'Notification') {
|
|
@@ -186,34 +232,9 @@ class Sqs extends BaseAwsSdkPlugin {
|
|
|
186
232
|
}
|
|
187
233
|
}
|
|
188
234
|
|
|
189
|
-
// Check MessageAttributes first (common direct-SQS/SNS path): avoids parsing
|
|
190
|
-
// the body (e.g. a large SNS `Message`) just to rule out an EventBridge
|
|
191
|
-
// envelope. Precedence matches responseExtractDSMContext.
|
|
192
235
|
const datadogAttribute = message.MessageAttributes?._datadog
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (parsedAttributes) {
|
|
196
|
-
return {
|
|
197
|
-
datadogContext: this.tracer.extract('text_map', parsedAttributes),
|
|
198
|
-
parsedAttributes,
|
|
199
|
-
parsedBody,
|
|
200
|
-
bodyChecked: true,
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Then the EventBridge envelope (optionally via SNS); see getEventBridgeContext.
|
|
206
|
-
const eventBridgeContext = getEventBridgeContext(parsedBody)
|
|
207
|
-
if (eventBridgeContext) {
|
|
208
|
-
return {
|
|
209
|
-
datadogContext: this.tracer.extract('text_map', eventBridgeContext),
|
|
210
|
-
parsedAttributes: eventBridgeContext,
|
|
211
|
-
parsedBody,
|
|
212
|
-
bodyChecked: true,
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return { parsedBody, bodyChecked: true }
|
|
236
|
+
const carrier = datadogAttribute ? this.parseDatadogAttributes(datadogAttribute) : undefined
|
|
237
|
+
return carrier ?? getEventBridgeContext(parsedBody)
|
|
217
238
|
}
|
|
218
239
|
|
|
219
240
|
parseDatadogAttributes (attributes) {
|
|
@@ -230,51 +251,39 @@ class Sqs extends BaseAwsSdkPlugin {
|
|
|
230
251
|
}
|
|
231
252
|
}
|
|
232
253
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
254
|
+
/**
|
|
255
|
+
* @param {string} operation
|
|
256
|
+
* @param {{ QueueUrl: string }} params
|
|
257
|
+
* @param {{ Messages?: object[] }} response
|
|
258
|
+
* @param {import('../../../dd-trace/src/opentracing/span') | null} span
|
|
259
|
+
* @param {Array<Record<string, string> | undefined>} [carriers] Per-message carriers already
|
|
260
|
+
* parsed by `responseExtract`; reused so each message body is parsed once. When omitted, the
|
|
261
|
+
* carriers are parsed here.
|
|
262
|
+
*/
|
|
263
|
+
responseExtractDSMContext (operation, params, response, span, carriers) {
|
|
236
264
|
if (!this.config.dsmEnabled) return
|
|
237
265
|
if (operation !== 'receiveMessage') return
|
|
238
|
-
if (!response
|
|
266
|
+
if (!response?.Messages?.length) return
|
|
239
267
|
|
|
268
|
+
const messages = response.Messages
|
|
240
269
|
// Only attribute payloadSize to the span when there is a single message.
|
|
241
|
-
span =
|
|
270
|
+
span = messages.length > 1 ? null : span
|
|
242
271
|
|
|
243
272
|
// QueueUrl is the same for the whole receive batch.
|
|
244
273
|
const queue = params.QueueUrl.slice(params.QueueUrl.lastIndexOf('/') + 1)
|
|
245
274
|
|
|
246
|
-
for (let i = 0; i <
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
} else if (message.Body) {
|
|
254
|
-
try {
|
|
255
|
-
body = JSON.parse(message.Body)
|
|
256
|
-
} catch {
|
|
257
|
-
// SQS to SQS
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
// SNS to SQS
|
|
261
|
-
if (body?.Type === 'Notification') {
|
|
262
|
-
message = body
|
|
263
|
-
}
|
|
264
|
-
// MessageAttributes for direct SQS/SNS; else the EventBridge envelope.
|
|
265
|
-
parsedAttributes = message.MessageAttributes?._datadog
|
|
266
|
-
? this.parseDatadogAttributes(message.MessageAttributes._datadog)
|
|
267
|
-
: getEventBridgeContext(body)
|
|
275
|
+
for (let i = 0; i < messages.length; i++) {
|
|
276
|
+
const message = messages[i]
|
|
277
|
+
const carrier = carriers === undefined ? this.parseMessageCarrier(message) : carriers[i]
|
|
278
|
+
if (carrier) {
|
|
279
|
+
// Inert for EventBridge until its producer emits a pathway (separate
|
|
280
|
+
// change) — no `dd-pathway-ctx-base64` to decode yet; SQS/SNS decode now.
|
|
281
|
+
this.tracer.decodeDataStreamsContext(carrier)
|
|
268
282
|
}
|
|
269
283
|
const payloadSize = getHeadersSize({
|
|
270
284
|
Body: message.Body,
|
|
271
285
|
MessageAttributes: message.MessageAttributes,
|
|
272
286
|
})
|
|
273
|
-
if (parsedAttributes) {
|
|
274
|
-
// Inert for EventBridge until its producer emits a pathway (separate
|
|
275
|
-
// change) — no `dd-pathway-ctx-base64` to decode yet; SQS/SNS decode now.
|
|
276
|
-
this.tracer.decodeDataStreamsContext(parsedAttributes)
|
|
277
|
-
}
|
|
278
287
|
this.tracer
|
|
279
288
|
.setCheckpoint(['direction:in', `topic:${queue}`, 'type:sqs'], span, payloadSize)
|
|
280
289
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const log = require('../../dd-trace/src/log')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {object} BullmqJobShape
|
|
7
|
+
* @property {string} [name]
|
|
8
|
+
* @property {unknown} [data]
|
|
9
|
+
* @property {unknown} [opts]
|
|
10
|
+
* @property {string} [queueName]
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {(job: BullmqJobShape) => boolean} BullmqFilter
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a user-provided filter from plugin config. Returns `undefined` when
|
|
19
|
+
* no filter is configured so callers can short-circuit the filtering path on a
|
|
20
|
+
* cheap truthy check. If `producerFilter` is present but not a function, log
|
|
21
|
+
* an error and fall back to no filter.
|
|
22
|
+
*
|
|
23
|
+
* @param {{ producerFilter?: unknown }} config Plugin config that may carry a `producerFilter`.
|
|
24
|
+
* @returns {BullmqFilter | undefined}
|
|
25
|
+
*/
|
|
26
|
+
function getFilter (config) {
|
|
27
|
+
if (typeof config?.producerFilter === 'function') {
|
|
28
|
+
return /** @type {BullmqFilter} */ (config.producerFilter)
|
|
29
|
+
}
|
|
30
|
+
if (config?.producerFilter !== undefined) {
|
|
31
|
+
log.error('Expected `producerFilter` to be a function. Ignoring.')
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { getFilter }
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
const log = require('../../dd-trace/src/log')
|
|
4
4
|
const ProducerPlugin = require('../../dd-trace/src/plugins/producer')
|
|
5
5
|
const { DsmPathwayCodec, getMessageSize } = require('../../dd-trace/src/datastreams')
|
|
6
|
+
const { getFilter } = require('./filter')
|
|
7
|
+
|
|
8
|
+
const filteredJobs = Symbol('bullmq.filteredJobs')
|
|
6
9
|
|
|
7
10
|
// Customer-controlled metadata may be malformed JSON. Returning a fresh `{}`
|
|
8
11
|
// on parse failure keeps the publish path alive instead of throwing into
|
|
@@ -31,6 +34,19 @@ class BaseBullmqProducerPlugin extends ProducerPlugin {
|
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
bindStart (ctx) {
|
|
37
|
+
let instrument = true
|
|
38
|
+
if (this.config.producerFilter) {
|
|
39
|
+
try {
|
|
40
|
+
instrument = this.shouldInstrument(ctx)
|
|
41
|
+
} catch (error) {
|
|
42
|
+
log.error('bullmq: producerFilter threw, filtering is disabled: %s', error.message)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!instrument) {
|
|
47
|
+
return { noop: true }
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
const { resource, meta } = this.getSpanData(ctx)
|
|
35
51
|
const span = this.startSpan({
|
|
36
52
|
resource,
|
|
@@ -48,6 +64,17 @@ class BaseBullmqProducerPlugin extends ProducerPlugin {
|
|
|
48
64
|
return ctx.currentStore
|
|
49
65
|
}
|
|
50
66
|
|
|
67
|
+
configure (config) {
|
|
68
|
+
if (typeof config === 'boolean') {
|
|
69
|
+
return super.configure(config)
|
|
70
|
+
}
|
|
71
|
+
return super.configure({ ...config, producerFilter: getFilter(config) })
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
shouldInstrument (ctx) {
|
|
75
|
+
throw new Error('shouldInstrument must be implemented by subclass')
|
|
76
|
+
}
|
|
77
|
+
|
|
51
78
|
getSpanData (ctx) {
|
|
52
79
|
throw new Error('getSpanData must be implemented by subclass')
|
|
53
80
|
}
|
|
@@ -87,6 +114,15 @@ class BaseBullmqProducerPlugin extends ProducerPlugin {
|
|
|
87
114
|
class QueueAddPlugin extends BaseBullmqProducerPlugin {
|
|
88
115
|
static prefix = 'tracing:orchestrion:bullmq:Queue_add'
|
|
89
116
|
|
|
117
|
+
shouldInstrument (ctx) {
|
|
118
|
+
return this.config.producerFilter({
|
|
119
|
+
name: ctx.arguments?.[0],
|
|
120
|
+
data: ctx.arguments?.[1],
|
|
121
|
+
opts: ctx.arguments?.[2],
|
|
122
|
+
queueName: ctx.self?.name,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
90
126
|
getSpanData (ctx) {
|
|
91
127
|
const queueName = ctx.self?.name || 'bullmq'
|
|
92
128
|
return {
|
|
@@ -128,6 +164,40 @@ class QueueAddPlugin extends BaseBullmqProducerPlugin {
|
|
|
128
164
|
class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
|
|
129
165
|
static prefix = 'tracing:orchestrion:bullmq:Queue_addBulk'
|
|
130
166
|
|
|
167
|
+
shouldInstrument (ctx) {
|
|
168
|
+
const jobs = this.#getFilteredJobs(ctx)
|
|
169
|
+
// Empty bulk calls are publish attempts and should keep producing a span.
|
|
170
|
+
return jobs === undefined || jobs.length > 0 || ctx.arguments?.[0].length === 0
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#getFilteredJobs (ctx) {
|
|
174
|
+
if (Object.hasOwn(ctx, filteredJobs)) return ctx[filteredJobs]
|
|
175
|
+
|
|
176
|
+
const jobs = ctx.arguments?.[0]
|
|
177
|
+
if (!Array.isArray(jobs)) return
|
|
178
|
+
|
|
179
|
+
let allowedJobs
|
|
180
|
+
if (this.config.producerFilter) {
|
|
181
|
+
const queueName = ctx.self?.name
|
|
182
|
+
try {
|
|
183
|
+
allowedJobs = []
|
|
184
|
+
for (const job of jobs) {
|
|
185
|
+
if (job && this.config.producerFilter({ name: job.name, data: job.data, opts: job.opts, queueName })) {
|
|
186
|
+
allowedJobs.push(job)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
} catch (error) {
|
|
190
|
+
log.error('bullmq: producerFilter threw, filtering is disabled: %s', error.message)
|
|
191
|
+
allowedJobs = jobs.filter(Boolean)
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
allowedJobs = jobs
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
ctx[filteredJobs] = allowedJobs
|
|
198
|
+
return allowedJobs
|
|
199
|
+
}
|
|
200
|
+
|
|
131
201
|
operationName () {
|
|
132
202
|
return 'bullmq.addBulk'
|
|
133
203
|
}
|
|
@@ -139,14 +209,14 @@ class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
|
|
|
139
209
|
resource: queueName,
|
|
140
210
|
meta: {
|
|
141
211
|
'messaging.destination.name': ctx.self?.name,
|
|
142
|
-
'messaging.batch.message_count':
|
|
212
|
+
'messaging.batch.message_count': jobs?.length,
|
|
143
213
|
},
|
|
144
214
|
}
|
|
145
215
|
}
|
|
146
216
|
|
|
147
217
|
injectTraceContext (span, ctx) {
|
|
148
|
-
const jobs = ctx
|
|
149
|
-
if (!
|
|
218
|
+
const jobs = this.#getFilteredJobs(ctx)
|
|
219
|
+
if (!jobs) return
|
|
150
220
|
|
|
151
221
|
const cache = []
|
|
152
222
|
for (let i = 0; i < jobs.length; i++) {
|
|
@@ -159,7 +229,7 @@ class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
|
|
|
159
229
|
}
|
|
160
230
|
|
|
161
231
|
setProducerCheckpoint (span, ctx) {
|
|
162
|
-
const jobs = ctx
|
|
232
|
+
const jobs = this.#getFilteredJobs(ctx) || []
|
|
163
233
|
const queueName = ctx.self?.name || 'bullmq'
|
|
164
234
|
const edgeTags = ['direction:out', `topic:${queueName}`, 'type:bullmq']
|
|
165
235
|
const cache = ctx._ddMetadata
|
|
@@ -180,6 +250,16 @@ class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
|
|
|
180
250
|
class FlowProducerAddPlugin extends BaseBullmqProducerPlugin {
|
|
181
251
|
static prefix = 'tracing:orchestrion:bullmq:FlowProducer_add'
|
|
182
252
|
|
|
253
|
+
shouldInstrument (ctx) {
|
|
254
|
+
const flow = ctx.arguments?.[0]
|
|
255
|
+
return this.config.producerFilter({
|
|
256
|
+
name: flow?.name,
|
|
257
|
+
data: flow?.data,
|
|
258
|
+
opts: flow?.opts,
|
|
259
|
+
queueName: flow?.queueName,
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
183
263
|
getSpanData (ctx) {
|
|
184
264
|
const flow = ctx.arguments?.[0]
|
|
185
265
|
const queueName = flow?.queueName || 'bullmq'
|
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
TEST_IS_RUM_ACTIVE,
|
|
12
12
|
TEST_CODE_OWNERS,
|
|
13
13
|
getTestEnvironmentMetadata,
|
|
14
|
+
getTestLevelsMetadataTags,
|
|
14
15
|
CI_APP_ORIGIN,
|
|
15
16
|
getTestParentSpan,
|
|
16
17
|
getCodeOwnersFileEntries,
|
|
@@ -23,6 +24,7 @@ const {
|
|
|
23
24
|
TEST_MODULE_ID,
|
|
24
25
|
TEST_SESSION_ID,
|
|
25
26
|
TEST_COMMAND,
|
|
27
|
+
TEST_LEVELS_METADATA,
|
|
26
28
|
TEST_MODULE,
|
|
27
29
|
TEST_SOURCE_START,
|
|
28
30
|
finishAllTraceSpans,
|
|
@@ -1043,7 +1045,13 @@ class CypressPlugin {
|
|
|
1043
1045
|
)
|
|
1044
1046
|
|
|
1045
1047
|
if (this.tracer._tracer._exporter?.addMetadataTags) {
|
|
1046
|
-
const metadataTags = {
|
|
1048
|
+
const metadataTags = {
|
|
1049
|
+
[TEST_LEVELS_METADATA]: {
|
|
1050
|
+
[TEST_COMMAND]: this.command,
|
|
1051
|
+
[TEST_SESSION_NAME]: testSessionName,
|
|
1052
|
+
...getTestLevelsMetadataTags(this.testEnvironmentMetadata),
|
|
1053
|
+
},
|
|
1054
|
+
}
|
|
1047
1055
|
const libraryCapabilitiesTags = getLibraryCapabilitiesTags(this.constructor.id, this.frameworkVersion)
|
|
1048
1056
|
metadataTags.test = {
|
|
1049
1057
|
...libraryCapabilitiesTags,
|