dd-trace 6.13.0 → 6.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-3rdparty.csv +3 -1
- package/ci/init.js +8 -3
- package/ci/test-optimization-validation/ci-package-scripts.js +2 -2
- package/ci/test-optimization-validation/ci-remediation.js +6 -6
- package/ci/test-optimization-validation/command-blocker.js +7 -5
- package/ci/test-optimization-validation/framework-adapters/cucumber.js +1 -1
- package/ci/test-optimization-validation/framework-adapters/vitest.js +3 -3
- package/ci/test-optimization-validation/package-check.js +1 -1
- package/ci/test-optimization-validation/report-writer.js +7 -5
- package/ci/vitest-no-worker-init-runner-setup.mjs +8 -0
- package/ci/vitest-no-worker-init-setup.mjs +13 -13
- package/ci/vitest-no-worker-init-v5-setup.mjs +4 -0
- package/package.json +13 -10
- package/packages/datadog-esbuild/index.js +3 -1
- package/packages/datadog-instrumentations/src/cypress-config.js +19 -17
- package/packages/datadog-instrumentations/src/fs.js +24 -20
- package/packages/datadog-instrumentations/src/google-cloud-vertexai.js +2 -2
- package/packages/datadog-instrumentations/src/graphql.js +16 -16
- package/packages/datadog-instrumentations/src/helpers/graphql-jit-runtime.js +504 -0
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +265 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +1 -2
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +95 -30
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +0 -25
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +83 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +85 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +30 -12
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +13 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +251 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +11 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +562 -27
- package/packages/datadog-instrumentations/src/jest.js +25 -20
- package/packages/datadog-instrumentations/src/knex.js +76 -0
- package/packages/datadog-instrumentations/src/mariadb.js +6 -0
- package/packages/datadog-instrumentations/src/mocha/common.js +59 -2
- package/packages/datadog-instrumentations/src/mocha/main.js +25 -15
- package/packages/datadog-instrumentations/src/mocha/utils.js +2 -1
- package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +98 -0
- package/packages/datadog-instrumentations/src/mocha/worker.js +12 -8
- package/packages/datadog-instrumentations/src/net.js +14 -17
- package/packages/datadog-instrumentations/src/next.js +212 -62
- package/packages/datadog-instrumentations/src/openai.js +42 -40
- package/packages/datadog-instrumentations/src/pg.js +15 -0
- package/packages/datadog-instrumentations/src/playwright.js +109 -13
- package/packages/datadog-instrumentations/src/rum-browser-scripts.js +40 -0
- package/packages/datadog-instrumentations/src/sequelize.js +94 -2
- package/packages/datadog-instrumentations/src/undici.js +53 -4
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +37 -3
- package/packages/datadog-instrumentations/src/vitest-main.js +92 -9
- package/packages/datadog-instrumentations/src/vitest-worker.js +71 -14
- package/packages/datadog-instrumentations/src/webdriverio.js +965 -6
- package/packages/datadog-plugin-amqp10/src/consumer.js +3 -1
- package/packages/datadog-plugin-amqp10/src/producer.js +3 -1
- package/packages/datadog-plugin-amqp10/src/util.js +2 -1
- package/packages/datadog-plugin-avsc/src/schema_iterator.js +6 -1
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +16 -5
- package/packages/datadog-plugin-cassandra-driver/src/index.js +8 -3
- package/packages/datadog-plugin-cucumber/src/index.js +0 -2
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +262 -48
- package/packages/datadog-plugin-google-cloud-pubsub/src/client.js +4 -1
- package/packages/datadog-plugin-graphql/src/execute.js +998 -196
- package/packages/datadog-plugin-graphql/src/index.js +27 -7
- package/packages/datadog-plugin-graphql/src/jit.js +136 -0
- package/packages/datadog-plugin-graphql/src/request.js +18 -27
- package/packages/datadog-plugin-graphql/src/resolve-error.js +108 -0
- package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -2
- package/packages/datadog-plugin-graphql/src/tools/transforms.js +45 -3
- package/packages/datadog-plugin-graphql/src/utils.js +208 -120
- package/packages/datadog-plugin-graphql/src/validate.js +10 -23
- package/packages/datadog-plugin-jest/src/index.js +0 -6
- package/packages/datadog-plugin-langchain/src/tracing.js +7 -0
- package/packages/datadog-plugin-mocha/src/index.js +403 -75
- package/packages/datadog-plugin-mongodb-core/src/query.js +6 -0
- package/packages/datadog-plugin-mysql/src/index.js +41 -15
- package/packages/datadog-plugin-net/src/tcp.js +3 -1
- package/packages/datadog-plugin-next/src/index.js +63 -29
- package/packages/datadog-plugin-playwright/src/index.js +167 -27
- package/packages/datadog-plugin-undici/src/index.js +27 -41
- package/packages/datadog-plugin-vitest/src/index.js +6 -12
- package/packages/dd-trace/src/aiguard/evaluation.js +2 -1
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +8 -1
- package/packages/dd-trace/src/aiguard/messages/openai.js +8 -1
- package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +20 -6
- package/packages/dd-trace/src/appsec/api_security/index.js +23 -6
- package/packages/dd-trace/src/appsec/api_security/sampler.js +99 -40
- package/packages/dd-trace/src/appsec/graphql.js +6 -2
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +3 -3
- package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +2 -1
- package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +8 -2
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +11 -36
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/lambda.js +44 -26
- package/packages/dd-trace/src/ci-visibility/encode/json-encoder.js +32 -2
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +8 -8
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +6 -4
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +20 -4
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +5 -173
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +7 -6
- package/packages/dd-trace/src/ci-visibility/exporters/agents.js +54 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +1 -2
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +394 -225
- package/packages/dd-trace/src/ci-visibility/exporters/limits.js +9 -0
- package/packages/dd-trace/src/ci-visibility/exporters/request.js +323 -31
- package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +42 -0
- package/packages/dd-trace/src/ci-visibility/final-flush.js +2 -1
- package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +57 -18
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +0 -3
- package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +164 -71
- package/packages/dd-trace/src/ci-visibility/requests/video-request.js +176 -0
- package/packages/dd-trace/src/ci-visibility/telemetry.js +10 -7
- package/packages/dd-trace/src/ci-visibility/test-video.js +61 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
- package/packages/dd-trace/src/config/index.js +73 -13
- package/packages/dd-trace/src/config/parsers.js +11 -1
- package/packages/dd-trace/src/config/stable.js +3 -3
- package/packages/dd-trace/src/config/supported-configurations.json +20 -2
- package/packages/dd-trace/src/crashtracking/crashtracker.js +82 -18
- package/packages/dd-trace/src/debugger/config.js +12 -1
- package/packages/dd-trace/src/debugger/constants.js +1 -0
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -1
- package/packages/dd-trace/src/debugger/devtools_client/request-options.js +33 -0
- package/packages/dd-trace/src/debugger/devtools_client/send.js +7 -40
- package/packages/dd-trace/src/debugger/devtools_client/status.js +11 -6
- package/packages/dd-trace/src/debugger/devtools_client/tags.js +41 -0
- package/packages/dd-trace/src/debugger/index.js +23 -3
- package/packages/dd-trace/src/dogstatsd.js +510 -76
- package/packages/dd-trace/src/encode/0.4-cross-payload.js +170 -0
- package/packages/dd-trace/src/encode/0.4.js +5 -5
- package/packages/dd-trace/src/encode/agentless-ci-visibility.js +9 -8
- package/packages/dd-trace/src/evp_proxy/direct.js +27 -23
- package/packages/dd-trace/src/exporters/agent/index.js +2 -1
- package/packages/dd-trace/src/exporters/agent/writer.js +24 -7
- package/packages/dd-trace/src/exporters/agentless/index.js +7 -9
- package/packages/dd-trace/src/exporters/agentless/writer.js +100 -118
- package/packages/dd-trace/src/exporters/common/agents.js +28 -6
- package/packages/dd-trace/src/exporters/common/buffering-exporter.js +2 -2
- package/packages/dd-trace/src/exporters/common/final-flush-request-tracker.js +191 -0
- package/packages/dd-trace/src/exporters/common/request.js +31 -11
- package/packages/dd-trace/src/exporters/common/url.js +40 -1
- package/packages/dd-trace/src/exporters/common/writer.js +9 -7
- package/packages/dd-trace/src/flush.js +6 -3
- package/packages/dd-trace/src/id.js +14 -0
- package/packages/dd-trace/src/index.js +5 -2
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +2 -1
- package/packages/dd-trace/src/llmobs/index.js +32 -32
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +22 -71
- package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +23 -13
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +7 -0
- package/packages/dd-trace/src/llmobs/util.js +0 -33
- package/packages/dd-trace/src/llmobs/writers/base.js +49 -32
- package/packages/dd-trace/src/log/index.js +7 -1
- package/packages/dd-trace/src/{ci-visibility/log-submission → log-submission}/log-submission-plugin.js +48 -19
- package/packages/dd-trace/src/openfeature/writers/base.js +14 -12
- package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
- package/packages/dd-trace/src/opentelemetry/logs/index.js +5 -24
- package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +14 -4
- package/packages/dd-trace/src/opentelemetry/metrics/index.js +15 -22
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +14 -4
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +10 -0
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +19 -4
- package/packages/dd-trace/src/opentelemetry/resource-attributes.js +55 -0
- package/packages/dd-trace/src/opentelemetry/trace/index.js +6 -1
- package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +10 -0
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +93 -18
- package/packages/dd-trace/src/payload-tagging/index.js +2 -1
- package/packages/dd-trace/src/plugins/ci_plugin.js +1 -6
- package/packages/dd-trace/src/plugins/tracing.js +12 -10
- package/packages/dd-trace/src/plugins/util/ci.js +3 -1
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +16 -15
- package/packages/dd-trace/src/plugins/util/test.js +83 -20
- package/packages/dd-trace/src/plugins/util/url.js +2 -2
- package/packages/dd-trace/src/plugins/util/web.js +4 -9
- package/packages/dd-trace/src/profiling/config.js +1 -0
- package/packages/dd-trace/src/profiling/oom.js +7 -2
- package/packages/dd-trace/src/profiling/profiler.js +23 -13
- package/packages/dd-trace/src/profiling/profilers/space.js +38 -11
- package/packages/dd-trace/src/profiling/webspan-utils.js +7 -4
- package/packages/dd-trace/src/proxy.js +75 -5
- package/packages/dd-trace/src/remote_config/index.js +319 -101
- package/packages/dd-trace/src/remote_config/scheduler.js +37 -12
- package/packages/dd-trace/src/runtime_metrics/client.js +37 -8
- package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -1
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +5 -1
- package/packages/dd-trace/src/sampling_rule.js +36 -9
- package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +4 -8
- package/packages/dd-trace/src/serverless/vercel.js +53 -20
- package/packages/dd-trace/src/serverless.js +10 -3
- package/packages/dd-trace/src/service-naming/schemas/util.js +1 -1
- package/packages/dd-trace/src/span_stats.js +24 -28
- package/packages/dd-trace/src/startup-log.js +3 -2
- package/packages/dd-trace/src/telemetry/agentless-url.js +28 -0
- package/packages/dd-trace/src/telemetry/send-data.js +18 -16
- package/packages/dd-trace/src/telemetry/session-propagation.js +3 -2
- package/packages/dd-trace/src/tracer.js +4 -5
- package/packages/dd-trace/src/tracer_metadata.js +11 -3
- 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/packages/datadog-instrumentations/src/playwright-browser-scripts.js +0 -27
- package/packages/dd-trace/src/encode/agentless-json.js +0 -210
|
@@ -16,6 +16,8 @@ const {
|
|
|
16
16
|
HTTP_RESPONSE_HEADERS,
|
|
17
17
|
} = tags
|
|
18
18
|
|
|
19
|
+
const UPGRADE_CHANNEL = 'apm:undici:request:upgrade'
|
|
20
|
+
|
|
19
21
|
// WeakMap to store span context for native undici request objects
|
|
20
22
|
const requestContexts = new WeakMap()
|
|
21
23
|
|
|
@@ -33,6 +35,7 @@ class UndiciPlugin extends HttpClientPlugin {
|
|
|
33
35
|
this.addSub('undici:request:headers', this.#onNativeRequestHeaders.bind(this))
|
|
34
36
|
this.addSub('undici:request:trailers', this.#onNativeRequestTrailers.bind(this))
|
|
35
37
|
this.addSub('undici:request:error', this.#onNativeRequestError.bind(this))
|
|
38
|
+
this.addSub(UPGRADE_CHANNEL, this.#onNativeRequestUpgrade.bind(this))
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
// ===========================================
|
|
@@ -112,30 +115,15 @@ class UndiciPlugin extends HttpClientPlugin {
|
|
|
112
115
|
// Store span context for request for later retrieval
|
|
113
116
|
requestContexts.set(request, {
|
|
114
117
|
span,
|
|
115
|
-
store,
|
|
116
|
-
uri,
|
|
117
118
|
method,
|
|
118
119
|
})
|
|
119
|
-
|
|
120
|
-
// Enter the span context
|
|
121
|
-
storage('legacy').enterWith({ ...store, span })
|
|
122
120
|
}
|
|
123
121
|
|
|
124
122
|
#onNativeRequestBodySent ({ request }) {
|
|
125
123
|
const ctx = requestContexts.get(request)
|
|
126
124
|
if (!ctx || ctx.method !== 'CONNECT') return
|
|
127
125
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
this.config.hooks.request(span, null, null)
|
|
131
|
-
|
|
132
|
-
span.finish()
|
|
133
|
-
|
|
134
|
-
requestContexts.delete(request)
|
|
135
|
-
|
|
136
|
-
if (store) {
|
|
137
|
-
storage('legacy').enterWith(store)
|
|
138
|
-
}
|
|
126
|
+
this.#finishNativeRequest(request)
|
|
139
127
|
}
|
|
140
128
|
|
|
141
129
|
#onNativeRequestHeaders ({ request, response }) {
|
|
@@ -160,50 +148,48 @@ class UndiciPlugin extends HttpClientPlugin {
|
|
|
160
148
|
}
|
|
161
149
|
|
|
162
150
|
#onNativeRequestTrailers ({ request }) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const { span, store } = ctx
|
|
167
|
-
|
|
168
|
-
// Call the request hook if configured
|
|
169
|
-
this.config.hooks.request(span, null, null)
|
|
170
|
-
|
|
171
|
-
// Finish the span
|
|
172
|
-
span.finish()
|
|
151
|
+
this.#finishNativeRequest(request)
|
|
152
|
+
}
|
|
173
153
|
|
|
174
|
-
|
|
175
|
-
|
|
154
|
+
#onNativeRequestError ({ request, error }) {
|
|
155
|
+
this.#finishNativeRequest(request, error)
|
|
156
|
+
}
|
|
176
157
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
158
|
+
/**
|
|
159
|
+
* @param {{ error?: Error, headers: unknown, request: object, statusCode: number }} context
|
|
160
|
+
*/
|
|
161
|
+
#onNativeRequestUpgrade ({ error, headers, request, statusCode }) {
|
|
162
|
+
this.#onNativeRequestHeaders({ request, response: { headers, statusCode } })
|
|
163
|
+
this.#finishNativeRequest(request, error)
|
|
181
164
|
}
|
|
182
165
|
|
|
183
|
-
|
|
166
|
+
/**
|
|
167
|
+
* @param {object} request
|
|
168
|
+
* @param {Error} [error]
|
|
169
|
+
*/
|
|
170
|
+
#finishNativeRequest (request, error) {
|
|
184
171
|
const ctx = requestContexts.get(request)
|
|
185
172
|
if (!ctx) return
|
|
186
173
|
|
|
187
|
-
const { span
|
|
174
|
+
const { span } = ctx
|
|
188
175
|
|
|
189
176
|
// Don't record AbortError as an error - it's user-initiated cancellation
|
|
190
177
|
if (error && error.name !== 'AbortError') {
|
|
191
178
|
span.setTag('error', error)
|
|
192
179
|
}
|
|
193
180
|
|
|
194
|
-
|
|
195
|
-
|
|
181
|
+
const requestHook = this.config.hooks.request
|
|
182
|
+
if (requestHook !== noop) {
|
|
183
|
+
this.tracer.scope().activate(span, () => {
|
|
184
|
+
requestHook(span, null, null)
|
|
185
|
+
})
|
|
186
|
+
}
|
|
196
187
|
|
|
197
188
|
// Finish the span
|
|
198
189
|
span.finish()
|
|
199
190
|
|
|
200
191
|
// Clean up
|
|
201
192
|
requestContexts.delete(request)
|
|
202
|
-
|
|
203
|
-
// Restore parent store
|
|
204
|
-
if (store) {
|
|
205
|
-
storage('legacy').enterWith(store)
|
|
206
|
-
}
|
|
207
193
|
}
|
|
208
194
|
|
|
209
195
|
// ===========================================
|
|
@@ -361,13 +361,13 @@ class VitestPlugin extends CiPlugin {
|
|
|
361
361
|
[TEST_SOURCE_START]: testStartLine || 1,
|
|
362
362
|
[TEST_STATUS]: 'skip',
|
|
363
363
|
[TEST_FINAL_STATUS]: 'skip',
|
|
364
|
-
...(isAttemptToFix ? { [TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX]: 'true' } : {}),
|
|
365
|
-
...(isDisabled ? { [TEST_MANAGEMENT_IS_DISABLED]: 'true' } : {}),
|
|
366
|
-
...(isQuarantined ? { [TEST_MANAGEMENT_IS_QUARANTINED]: 'true' } : {}),
|
|
367
|
-
...(isNew ? { [TEST_IS_NEW]: 'true' } : {}),
|
|
368
|
-
...(isRumActive ? { [TEST_IS_RUM_ACTIVE]: 'true' } : {}),
|
|
369
|
-
...(isTestFrameworkWorker ? { [TEST_IS_TEST_FRAMEWORK_WORKER]: 'true' } : {}),
|
|
370
364
|
}
|
|
365
|
+
if (isAttemptToFix) extraTags[TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX] = 'true'
|
|
366
|
+
if (isDisabled) extraTags[TEST_MANAGEMENT_IS_DISABLED] = 'true'
|
|
367
|
+
if (isQuarantined) extraTags[TEST_MANAGEMENT_IS_QUARANTINED] = 'true'
|
|
368
|
+
if (isNew) extraTags[TEST_IS_NEW] = 'true'
|
|
369
|
+
if (isRumActive) extraTags[TEST_IS_RUM_ACTIVE] = 'true'
|
|
370
|
+
if (isTestFrameworkWorker) extraTags[TEST_IS_TEST_FRAMEWORK_WORKER] = 'true'
|
|
371
371
|
setBrowserTags(extraTags, {
|
|
372
372
|
browserDriver,
|
|
373
373
|
browserName,
|
|
@@ -523,7 +523,6 @@ class VitestPlugin extends CiPlugin {
|
|
|
523
523
|
this.telemetry.ciVisEvent(TELEMETRY_CODE_COVERAGE_FINISHED, 'suite', { library: coverageLibrary })
|
|
524
524
|
this.telemetry.distribution(TELEMETRY_CODE_COVERAGE_NUM_FILES, {}, relativeFiles.length)
|
|
525
525
|
}
|
|
526
|
-
this.tracer._exporter.deferTestSuiteSpan?.(testSuiteSpan)
|
|
527
526
|
testSuiteSpan.finish()
|
|
528
527
|
finishAllTraceSpans(testSuiteSpan)
|
|
529
528
|
}
|
|
@@ -567,7 +566,6 @@ class VitestPlugin extends CiPlugin {
|
|
|
567
566
|
this.addSub('ci:vitest:session:finish', ({
|
|
568
567
|
status,
|
|
569
568
|
error,
|
|
570
|
-
isTestSessionFinalizationError,
|
|
571
569
|
testCodeCoverageLinesTotal,
|
|
572
570
|
isEarlyFlakeDetectionEnabled,
|
|
573
571
|
isEarlyFlakeDetectionFaulty,
|
|
@@ -590,9 +588,6 @@ class VitestPlugin extends CiPlugin {
|
|
|
590
588
|
this.testSessionSpan.setTag(TEST_STATUS, status)
|
|
591
589
|
this.testModuleSpan.setTag(TEST_STATUS, status)
|
|
592
590
|
if (error) {
|
|
593
|
-
if (isTestSessionFinalizationError) {
|
|
594
|
-
this.tracer._exporter.setDeferredTestSuiteError?.(error)
|
|
595
|
-
}
|
|
596
591
|
this.testModuleSpan.setTag('error', error)
|
|
597
592
|
this.testSessionSpan.setTag('error', error)
|
|
598
593
|
}
|
|
@@ -622,7 +617,6 @@ class VitestPlugin extends CiPlugin {
|
|
|
622
617
|
if (vitestPool) {
|
|
623
618
|
this.testSessionSpan.setTag(VITEST_POOL, vitestPool)
|
|
624
619
|
}
|
|
625
|
-
this.tracer._exporter.exportDeferredTestSuiteSpans?.()
|
|
626
620
|
this.testModuleSpan.finish()
|
|
627
621
|
this.telemetry.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
|
|
628
622
|
this.testSessionSpan.finish()
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { HTTP_CLIENT_IP, HTTP_USERAGENT, NETWORK_CLIENT_IP } = require('../../../../ext/tags')
|
|
4
|
-
const
|
|
4
|
+
const createRfdc = require('../../../../vendor/dist/rfdc')
|
|
5
|
+
const clone = createRfdc({ proto: false, circles: false })
|
|
5
6
|
const { USER_ID, USER_SESSION_ID } = require('../appsec/addresses')
|
|
6
7
|
const { getActiveRequest } = require('../appsec/store')
|
|
7
8
|
const { keepTrace } = require('../priority_sampler')
|
|
@@ -208,7 +208,14 @@ function walkContentBlocks (blocks) {
|
|
|
208
208
|
function partsToContent (parts, hasImages) {
|
|
209
209
|
if (!parts.length) return
|
|
210
210
|
if (hasImages) return parts
|
|
211
|
-
|
|
211
|
+
let content = ''
|
|
212
|
+
let isFirstPart = true
|
|
213
|
+
for (const part of parts) {
|
|
214
|
+
if (!isFirstPart) content += '\n'
|
|
215
|
+
content += part.text
|
|
216
|
+
isFirstPart = false
|
|
217
|
+
}
|
|
218
|
+
return content
|
|
212
219
|
}
|
|
213
220
|
|
|
214
221
|
/**
|
|
@@ -322,7 +322,14 @@ function openAIResponseContentToMessageContent (content) {
|
|
|
322
322
|
|
|
323
323
|
if (!parts.length) return
|
|
324
324
|
if (hasImages) return parts
|
|
325
|
-
|
|
325
|
+
let textContent = ''
|
|
326
|
+
let isFirstPart = true
|
|
327
|
+
for (const part of parts) {
|
|
328
|
+
if (!isFirstPart) textContent += '\n'
|
|
329
|
+
textContent += part.text
|
|
330
|
+
isFirstPart = false
|
|
331
|
+
}
|
|
332
|
+
return textContent
|
|
326
333
|
}
|
|
327
334
|
|
|
328
335
|
/**
|
|
@@ -62,19 +62,26 @@ function convertVercelPromptToMessages (prompt) {
|
|
|
62
62
|
if (hasImages) {
|
|
63
63
|
messages.push({ role: 'user', content: contentParts })
|
|
64
64
|
} else {
|
|
65
|
-
|
|
65
|
+
let content = ''
|
|
66
|
+
let isFirstPart = true
|
|
67
|
+
for (const part of contentParts) {
|
|
68
|
+
if (!isFirstPart) content += '\n'
|
|
69
|
+
content += part.text
|
|
70
|
+
isFirstPart = false
|
|
71
|
+
}
|
|
72
|
+
messages.push({ role: 'user', content })
|
|
66
73
|
}
|
|
67
74
|
break
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
case 'assistant': {
|
|
71
|
-
|
|
78
|
+
let text
|
|
72
79
|
const toolCalls = []
|
|
73
80
|
if (!Array.isArray(msg.content)) break
|
|
74
81
|
|
|
75
82
|
for (const part of msg.content) {
|
|
76
83
|
if (part.type === 'text') {
|
|
77
|
-
|
|
84
|
+
text = text === undefined ? part.text : `${text}\n${part.text}`
|
|
78
85
|
} else if (part.type === 'tool-call') {
|
|
79
86
|
toolCalls.push({
|
|
80
87
|
id: part.toolCallId,
|
|
@@ -88,8 +95,8 @@ function convertVercelPromptToMessages (prompt) {
|
|
|
88
95
|
|
|
89
96
|
if (toolCalls.length > 0) {
|
|
90
97
|
messages.push({ role: 'assistant', tool_calls: toolCalls })
|
|
91
|
-
} else if (
|
|
92
|
-
messages.push({ role: 'assistant', content:
|
|
98
|
+
} else if (text !== undefined) {
|
|
99
|
+
messages.push({ role: 'assistant', content: text })
|
|
93
100
|
}
|
|
94
101
|
break
|
|
95
102
|
}
|
|
@@ -161,7 +168,14 @@ function buildTextOutputMessages (inputMessages, text) {
|
|
|
161
168
|
function buildOutputMessages (inputMessages, content) {
|
|
162
169
|
const toolCalls = content.filter(c => c.type === 'tool-call')
|
|
163
170
|
if (toolCalls.length) return buildToolCallOutputMessages(inputMessages, toolCalls)
|
|
164
|
-
|
|
171
|
+
let text = ''
|
|
172
|
+
let isFirstTextPart = true
|
|
173
|
+
for (const part of content) {
|
|
174
|
+
if (part.type !== 'text') continue
|
|
175
|
+
if (!isFirstTextPart) text += '\n'
|
|
176
|
+
text += part.text
|
|
177
|
+
isFirstTextPart = false
|
|
178
|
+
}
|
|
165
179
|
if (text) return buildTextOutputMessages(inputMessages, text)
|
|
166
180
|
return []
|
|
167
181
|
}
|
|
@@ -5,6 +5,8 @@ const { isSchemaAttribute } = require('../reporter')
|
|
|
5
5
|
const appsecTelemetry = require('../telemetry')
|
|
6
6
|
const sampler = require('./sampler')
|
|
7
7
|
|
|
8
|
+
/** @typedef {import('../../opentracing/span')} DatadogSpan */
|
|
9
|
+
|
|
8
10
|
/**
|
|
9
11
|
* Map a sampling decision into the corresponding API Security telemetry metric.
|
|
10
12
|
*
|
|
@@ -13,14 +15,14 @@ const sampler = require('./sampler')
|
|
|
13
15
|
* - MISSING_ROUTE: missing_route
|
|
14
16
|
* - SKIP: no metric emitted
|
|
15
17
|
*
|
|
16
|
-
* @param {
|
|
18
|
+
* @param {DatadogSpan} rootSpan Span the sampling decision was attached to
|
|
17
19
|
* @param {'sample' | 'missing_route' | 'skip'} samplingDecision Sampler decision
|
|
18
20
|
* @param {{ attributes?: Record<string, unknown> } | undefined} wafResult WAF run result
|
|
19
21
|
*/
|
|
20
|
-
function
|
|
22
|
+
function reportRootSpanRequest (rootSpan, samplingDecision, wafResult) {
|
|
21
23
|
switch (samplingDecision) {
|
|
22
24
|
case sampler.SamplingDecision.SAMPLE: {
|
|
23
|
-
const framework = getFramework(
|
|
25
|
+
const framework = getFramework(rootSpan)
|
|
24
26
|
if (hasSchemaAttributes(wafResult?.attributes)) {
|
|
25
27
|
appsecTelemetry.incrementApiSecRequestSchemaMetric(framework)
|
|
26
28
|
} else {
|
|
@@ -29,13 +31,26 @@ function reportRequest (req, samplingDecision, wafResult) {
|
|
|
29
31
|
break
|
|
30
32
|
}
|
|
31
33
|
case sampler.SamplingDecision.MISSING_ROUTE:
|
|
32
|
-
appsecTelemetry.incrementApiSecMissingRouteMetric(getFramework(
|
|
34
|
+
appsecTelemetry.incrementApiSecMissingRouteMetric(getFramework(rootSpan))
|
|
33
35
|
break
|
|
34
36
|
}
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Node HTTP adapter over {@link reportRootSpanRequest}.
|
|
41
|
+
*
|
|
42
|
+
* @param {import('http').IncomingMessage} req
|
|
43
|
+
* @param {'sample' | 'missing_route' | 'skip'} samplingDecision Sampler decision
|
|
44
|
+
* @param {{ attributes?: Record<string, unknown> } | undefined} wafResult WAF run result
|
|
45
|
+
*/
|
|
46
|
+
function reportRequest (req, samplingDecision, wafResult) {
|
|
47
|
+
if (samplingDecision === sampler.SamplingDecision.SKIP) return
|
|
48
|
+
|
|
49
|
+
reportRootSpanRequest(web.root(req), samplingDecision, wafResult)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getFramework (rootSpan) {
|
|
53
|
+
return rootSpan?.context?.()?.getTag?.('component')
|
|
39
54
|
}
|
|
40
55
|
|
|
41
56
|
function hasSchemaAttributes (attributes) {
|
|
@@ -50,6 +65,8 @@ module.exports = {
|
|
|
50
65
|
configure: sampler.configure,
|
|
51
66
|
disable: sampler.disable,
|
|
52
67
|
sampleRequest: sampler.sampleRequest,
|
|
68
|
+
sampleRootSpanRequest: sampler.sampleRootSpanRequest,
|
|
53
69
|
reportRequest,
|
|
70
|
+
reportRootSpanRequest,
|
|
54
71
|
SamplingDecision: sampler.SamplingDecision,
|
|
55
72
|
}
|
|
@@ -8,6 +8,8 @@ const { keepTrace } = require('../../priority_sampler')
|
|
|
8
8
|
const { ASM } = require('../../standalone/product')
|
|
9
9
|
const { isBlocked } = require('../blocking')
|
|
10
10
|
|
|
11
|
+
/** @typedef {import('../../opentracing/span')} DatadogSpan */
|
|
12
|
+
|
|
11
13
|
const MAX_SIZE = 4096
|
|
12
14
|
|
|
13
15
|
const SamplingDecision = Object.freeze({
|
|
@@ -44,77 +46,113 @@ function disable () {
|
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
|
-
* @param {
|
|
48
|
-
* @param {
|
|
49
|
+
* @param {DatadogSpan} rootSpan Span the sampling decision is attached to
|
|
50
|
+
* @param {object} request
|
|
51
|
+
* @param {string} request.method
|
|
52
|
+
* @param {number|string} request.statusCode
|
|
53
|
+
* @param {string|undefined} request.route Tri-state: a route string, an empty string (still a
|
|
54
|
+
* valid route — dd-trace-js represents the express root path '/' as an empty path segment), or
|
|
55
|
+
* `undefined` meaning no route information at all.
|
|
56
|
+
* @param {boolean} [request.blocked] Whether the response was blocked by ASM
|
|
49
57
|
* @param {boolean} record When true and the decision is SAMPLE, records the endpoint in the TTL cache
|
|
50
58
|
* @returns {'sample' | 'missing_route' | 'skip'}
|
|
51
59
|
*/
|
|
52
|
-
function
|
|
60
|
+
function sampleRootSpanRequest (rootSpan, request, record = false) {
|
|
53
61
|
if (!enabled) return SamplingDecision.SKIP
|
|
54
62
|
|
|
55
|
-
const rootSpan = web.root(req)
|
|
56
63
|
if (!rootSpan) return SamplingDecision.SKIP
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
let priority = getSpanPriority(rootSpan)
|
|
60
|
-
if (!priority) {
|
|
61
|
-
rootSpan._prioritySampler?.sample(rootSpan)
|
|
62
|
-
priority = getSpanPriority(rootSpan)
|
|
63
|
-
}
|
|
65
|
+
const { method, statusCode, route, blocked = false } = request ?? {}
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
if (!method || !statusCode) {
|
|
68
|
+
log.warn('[ASM] Unsupported groupkey for API security')
|
|
69
|
+
return SamplingDecision.SKIP
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
if (!resolved) return SamplingDecision.SKIP
|
|
72
|
+
if (isRejected(rootSpan)) return SamplingDecision.SKIP
|
|
72
73
|
|
|
73
|
-
if (
|
|
74
|
-
if (
|
|
74
|
+
if (isRoutelessRecord(route, record)) {
|
|
75
|
+
if (isNotFound(statusCode) || blocked) return SamplingDecision.SKIP
|
|
75
76
|
return SamplingDecision.MISSING_ROUTE
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
const key = buildSamplingKey(method, route, statusCode)
|
|
80
|
+
if (sampledRequests.has(key)) return SamplingDecision.SKIP
|
|
79
81
|
|
|
80
82
|
if (asmStandaloneEnabled) {
|
|
81
83
|
keepTrace(rootSpan, ASM)
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
if (record) {
|
|
85
|
-
sampledRequests.set(
|
|
87
|
+
sampledRequests.set(key, undefined)
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
return SamplingDecision.SAMPLE
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
/**
|
|
94
|
+
* Node HTTP adapter over {@link sampleRootSpanRequest}.
|
|
95
|
+
*
|
|
92
96
|
* @param {import('http').IncomingMessage} req
|
|
93
97
|
* @param {import('http').ServerResponse} res
|
|
94
|
-
* @
|
|
98
|
+
* @param {boolean} record When true and the decision is SAMPLE, records the endpoint in the TTL cache
|
|
99
|
+
* @returns {'sample' | 'missing_route' | 'skip'}
|
|
95
100
|
*/
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
return resolved !== null && sampledRequests.has(resolved.key)
|
|
99
|
-
}
|
|
101
|
+
function sampleRequest (req, res, record = false) {
|
|
102
|
+
if (!enabled) return SamplingDecision.SKIP
|
|
100
103
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const status = res.statusCode
|
|
104
|
+
const rootSpan = web.root(req)
|
|
105
|
+
if (!rootSpan) return SamplingDecision.SKIP
|
|
104
106
|
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
if (isRejected(rootSpan)) return SamplingDecision.SKIP
|
|
108
|
+
|
|
109
|
+
const statusCode = res.statusCode
|
|
110
|
+
const route = getRouteOrEndpoint(web.getContext(req), statusCode)
|
|
111
|
+
|
|
112
|
+
return sampleRootSpanRequest(rootSpan, {
|
|
113
|
+
method: req.method,
|
|
114
|
+
statusCode,
|
|
115
|
+
route,
|
|
116
|
+
blocked: isRoutelessRecord(route, record) && !isNotFound(statusCode) ? isBlocked(res) : false,
|
|
117
|
+
}, record)
|
|
118
|
+
}
|
|
109
119
|
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Whether this is a request with no route information
|
|
122
|
+
*
|
|
123
|
+
* @param {string|undefined} route
|
|
124
|
+
* @param {boolean} record
|
|
125
|
+
* @returns {boolean}
|
|
126
|
+
*/
|
|
127
|
+
function isRoutelessRecord (route, record) {
|
|
128
|
+
return record && route === undefined
|
|
129
|
+
}
|
|
112
130
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
131
|
+
/**
|
|
132
|
+
* @param {number|string} statusCode
|
|
133
|
+
* @returns {boolean}
|
|
134
|
+
*/
|
|
135
|
+
function isNotFound (statusCode) {
|
|
136
|
+
return Number(statusCode) === 404
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {string} method
|
|
141
|
+
* @param {string|undefined} route A route string, an empty string (still a valid route), or
|
|
142
|
+
* `undefined`
|
|
143
|
+
* @param {number|string} statusCode
|
|
144
|
+
* @returns {string}
|
|
145
|
+
*/
|
|
146
|
+
function buildSamplingKey (method, route, statusCode) {
|
|
147
|
+
return method + (route ?? '') + statusCode
|
|
116
148
|
}
|
|
117
149
|
|
|
150
|
+
/**
|
|
151
|
+
* @param {{ paths?: string[], span?: object }} [context] Web context of the request
|
|
152
|
+
* @param {number|string} statusCode
|
|
153
|
+
* @returns {string|undefined} A route string, an empty string (still a valid route), or
|
|
154
|
+
* `undefined` when no route information is available.
|
|
155
|
+
*/
|
|
118
156
|
function getRouteOrEndpoint (context, statusCode) {
|
|
119
157
|
// The router plugin populates `context.paths` whenever the framework matched something.
|
|
120
158
|
// For express's root '/' route the matched path is normalized to '' (see datadog-plugin-router),
|
|
@@ -125,23 +163,44 @@ function getRouteOrEndpoint (context, statusCode) {
|
|
|
125
163
|
return paths.join('')
|
|
126
164
|
}
|
|
127
165
|
|
|
128
|
-
if (statusCode
|
|
166
|
+
if (isNotFound(statusCode)) return
|
|
129
167
|
|
|
130
168
|
const endpoint = context?.span?.context()?.getTag?.('http.endpoint')
|
|
131
169
|
if (endpoint) return endpoint
|
|
170
|
+
}
|
|
132
171
|
|
|
133
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Whether the span's trace is dropped, forcing the sampling decision if none was made yet.
|
|
174
|
+
*
|
|
175
|
+
* Always false under ASM standalone: there the trace is kept via `keepTrace(rootSpan, ASM)`
|
|
176
|
+
* regardless of the APM sampling decision.
|
|
177
|
+
*
|
|
178
|
+
* @param {DatadogSpan} rootSpan
|
|
179
|
+
* @returns {boolean}
|
|
180
|
+
*/
|
|
181
|
+
function isRejected (rootSpan) {
|
|
182
|
+
if (asmStandaloneEnabled) return false
|
|
183
|
+
|
|
184
|
+
let priority = getSpanPriority(rootSpan)
|
|
185
|
+
if (priority == null) {
|
|
186
|
+
rootSpan._prioritySampler?.sample(rootSpan)
|
|
187
|
+
priority = getSpanPriority(rootSpan)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return priority === AUTO_REJECT || priority === USER_REJECT
|
|
134
191
|
}
|
|
135
192
|
|
|
136
193
|
function getSpanPriority (span) {
|
|
137
194
|
const spanContext = span.context?.()
|
|
138
|
-
|
|
195
|
+
const priority = spanContext?._sampling?.priority
|
|
196
|
+
|
|
197
|
+
return priority == null ? priority : Number(priority)
|
|
139
198
|
}
|
|
140
199
|
|
|
141
200
|
module.exports = {
|
|
142
201
|
configure,
|
|
143
202
|
disable,
|
|
144
203
|
sampleRequest,
|
|
145
|
-
|
|
204
|
+
sampleRootSpanRequest,
|
|
146
205
|
SamplingDecision,
|
|
147
206
|
}
|
|
@@ -32,11 +32,15 @@ function disable () {
|
|
|
32
32
|
disableGraphql()
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
/**
|
|
36
|
+
* @param {{ createResolverInfo?: (data: object) => object | null }} data
|
|
37
|
+
*/
|
|
38
|
+
function onGraphqlStartResolver (data) {
|
|
36
39
|
const req = getActiveRequest()
|
|
37
40
|
|
|
38
41
|
if (!req) return
|
|
39
42
|
|
|
43
|
+
const resolverInfo = data.createResolverInfo?.(data)
|
|
40
44
|
if (!resolverInfo || typeof resolverInfo !== 'object') return
|
|
41
45
|
|
|
42
46
|
const result = waf.run({ ephemeral: { [addresses.HTTP_INCOMING_GRAPHQL_RESOLVER]: resolverInfo } }, req)
|
|
@@ -46,7 +50,7 @@ function onGraphqlStartResolver ({ abortController, resolverInfo }) {
|
|
|
46
50
|
if (requestData?.isInGraphqlRequest) {
|
|
47
51
|
requestData.blocked = true
|
|
48
52
|
requestData.wafAction = blockingAction
|
|
49
|
-
abortController?.abort()
|
|
53
|
+
data.abortController?.abort()
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
}
|
|
@@ -178,12 +178,12 @@ class TaintTrackingPlugin extends SourceIastPlugin {
|
|
|
178
178
|
addGraphQLSubscriptions () {
|
|
179
179
|
this.addSub(
|
|
180
180
|
{ channelName: 'apm:graphql:resolve:start', tag: HTTP_REQUEST_BODY },
|
|
181
|
-
(
|
|
181
|
+
({ rootCtx, args }) => {
|
|
182
182
|
const iastContext = getIastContext(storage('legacy').getStore())
|
|
183
|
-
const source =
|
|
183
|
+
const source = rootCtx?.source
|
|
184
184
|
const ranges = source && getRanges(iastContext, source)
|
|
185
185
|
if (ranges?.length) {
|
|
186
|
-
this._taintTrackingHandler(ranges[0].iinfo.type,
|
|
186
|
+
this._taintTrackingHandler(ranges[0].iinfo.type, args, null, iastContext)
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
)
|
|
@@ -242,7 +242,8 @@ function lodashTaintTrackingHandler (message) {
|
|
|
242
242
|
const context = getContextDefault()
|
|
243
243
|
const transactionId = getTransactionId(context)
|
|
244
244
|
if (transactionId) {
|
|
245
|
-
|
|
245
|
+
const taintResult = getLodashTaintedUtilFn(message.operation)
|
|
246
|
+
message.result = taintResult(transactionId, message.result, ...message.arguments)
|
|
246
247
|
}
|
|
247
248
|
} catch (e) {
|
|
248
249
|
log.error('[ASM] Error invoking CSI lodash %s', message.operation, e)
|
|
@@ -46,8 +46,14 @@ function filterTags (tags) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function processTagValue (tags) {
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
let processedTags = ''
|
|
50
|
+
let isFirstTag = true
|
|
51
|
+
for (const tag of tags) {
|
|
52
|
+
if (!isFirstTag) processedTags += '_'
|
|
53
|
+
processedTags += tag.includes(':') ? getSegment(tag, ':', 1) : tag
|
|
54
|
+
isFirstTag = false
|
|
55
|
+
}
|
|
56
|
+
return processedTags.replaceAll('.', '_')
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
module.exports = {
|