dd-trace 6.16.0 → 6.17.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/diagnose.js +0 -23
- package/ci/test-optimization-validation/approval-artifacts.js +0 -3
- package/ci/test-optimization-validation/approval.js +0 -6
- package/ci/test-optimization-validation/artifact-id.js +0 -1
- package/ci/test-optimization-validation/bounded-json.js +0 -1
- package/ci/test-optimization-validation/cli.js +0 -12
- package/ci/test-optimization-validation/command-output-policy.js +0 -1
- package/ci/test-optimization-validation/command-runner.js +0 -3
- package/ci/test-optimization-validation/environment.js +0 -4
- package/ci/test-optimization-validation/executable-approval.js +0 -1
- package/ci/test-optimization-validation/executable.js +0 -5
- package/ci/test-optimization-validation/framework-adapters/cucumber.js +0 -6
- package/ci/test-optimization-validation/framework-adapters/cypress.js +0 -3
- package/ci/test-optimization-validation/framework-adapters/playwright.js +0 -6
- package/ci/test-optimization-validation/framework-adapters/vitest.js +0 -4
- package/ci/test-optimization-validation/generated-test-contract.js +0 -5
- package/ci/test-optimization-validation/generated-verifier.js +0 -1
- package/ci/test-optimization-validation/literal-environment.js +0 -1
- package/ci/test-optimization-validation/manifest-scaffold.js +0 -17
- package/ci/test-optimization-validation/manifest-schema.js +0 -21
- package/ci/test-optimization-validation/offline-fixtures.js +0 -4
- package/ci/test-optimization-validation/plan-writer.js +0 -11
- package/ci/test-optimization-validation/preflight-runner.js +0 -1
- package/ci/test-optimization-validation/redaction.js +0 -4
- package/ci/test-optimization-validation/report-writer.js +0 -16
- package/ci/test-optimization-validation/runner-command.js +0 -3
- package/ci/test-optimization-validation/runner-contract.js +0 -4
- package/ci/test-optimization-validation/safe-files.js +0 -1
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +0 -1
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +0 -1
- package/ci/test-optimization-validation/source-text.js +0 -3
- package/ci/vitest-no-worker-init-setup.mjs +41 -16
- package/index.d.ts +165 -0
- package/package.json +17 -16
- package/packages/datadog-esbuild/src/utils.js +0 -2
- package/packages/datadog-instrumentations/src/aerospike.js +28 -6
- package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +0 -2
- package/packages/datadog-instrumentations/src/aws-sdk.js +0 -2
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +0 -2
- package/packages/datadog-instrumentations/src/console.js +97 -0
- package/packages/datadog-instrumentations/src/cucumber.js +72 -12
- package/packages/datadog-instrumentations/src/cypress-config.js +0 -23
- package/packages/datadog-instrumentations/src/helpers/channel.js +0 -2
- package/packages/datadog-instrumentations/src/helpers/graphql-jit-runtime.js +0 -5
- package/packages/datadog-instrumentations/src/helpers/hooks.js +8 -0
- package/packages/datadog-instrumentations/src/helpers/instrumentation-utils.js +0 -3
- package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +0 -6
- package/packages/datadog-instrumentations/src/helpers/register.js +4 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +72 -24
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentation-registry.js +39 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +3 -17
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +12 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/postgres.js +36 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/supabase.js +128 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +16 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms/postgres.js +388 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +0 -5
- package/packages/datadog-instrumentations/src/helpers/router-helper.js +0 -1
- package/packages/datadog-instrumentations/src/jest.js +141 -51
- package/packages/datadog-instrumentations/src/knex.js +0 -1
- package/packages/datadog-instrumentations/src/mariadb-bundle.js +0 -24
- package/packages/datadog-instrumentations/src/mariadb.js +0 -2
- package/packages/datadog-instrumentations/src/mocha/common.js +0 -1
- package/packages/datadog-instrumentations/src/mocha/main.js +22 -25
- package/packages/datadog-instrumentations/src/mocha/utils.js +119 -49
- package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +0 -6
- package/packages/datadog-instrumentations/src/mocha/worker.js +14 -17
- package/packages/datadog-instrumentations/src/mysql2.js +0 -2
- package/packages/datadog-instrumentations/src/openai-realtime/audio-accumulator.js +241 -0
- package/packages/datadog-instrumentations/src/openai-realtime/events.js +76 -0
- package/packages/datadog-instrumentations/src/openai-realtime/index.js +301 -0
- package/packages/datadog-instrumentations/src/openai-realtime/session.js +998 -0
- package/packages/datadog-instrumentations/src/openai-realtime/turns.js +211 -0
- package/packages/datadog-instrumentations/src/openai.js +51 -12
- package/packages/datadog-instrumentations/src/oracledb.js +9 -3
- package/packages/datadog-instrumentations/src/playwright-reporter.js +3 -16
- package/packages/datadog-instrumentations/src/playwright.js +238 -55
- package/packages/datadog-instrumentations/src/postgres.js +7 -0
- package/packages/datadog-instrumentations/src/router.js +0 -2
- package/packages/datadog-instrumentations/src/rum-browser-scripts.js +0 -2
- package/packages/datadog-instrumentations/src/sequelize.js +0 -1
- package/packages/datadog-instrumentations/src/supabase.js +15 -0
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +18 -18
- package/packages/datadog-instrumentations/src/vitest-main.js +37 -17
- package/packages/datadog-instrumentations/src/vitest-util.js +7 -1
- package/packages/datadog-instrumentations/src/vitest-worker.js +49 -13
- package/packages/datadog-instrumentations/src/webdriverio.js +23 -42
- package/packages/datadog-instrumentations/src/ws.js +0 -1
- package/packages/datadog-plugin-ai/src/utils.js +0 -2
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +0 -1
- package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +0 -2
- package/packages/datadog-plugin-aws-sdk/src/base.js +0 -1
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +0 -1
- package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +0 -1
- package/packages/datadog-plugin-aws-sdk/src/util.js +0 -1
- package/packages/datadog-plugin-azure-durable-functions/src/index.js +0 -1
- package/packages/datadog-plugin-cucumber/src/index.js +10 -0
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +171 -75
- package/packages/datadog-plugin-cypress/src/finalization.js +0 -3
- package/packages/datadog-plugin-cypress/src/source-map-utils.js +0 -4
- package/packages/datadog-plugin-cypress/src/support.js +52 -7
- package/packages/datadog-plugin-graphql/src/resolve-error.js +1 -2
- package/packages/datadog-plugin-graphql/src/utils.js +0 -5
- package/packages/datadog-plugin-jest/src/index.js +13 -0
- package/packages/datadog-plugin-kafkajs/src/producer.js +0 -1
- package/packages/datadog-plugin-mocha/src/index.js +39 -11
- package/packages/datadog-plugin-mongodb-core/src/query.js +0 -1
- package/packages/datadog-plugin-openai/src/index.js +16 -1
- package/packages/datadog-plugin-openai/src/realtime.js +144 -0
- package/packages/datadog-plugin-openai-agents/src/integration.js +0 -4
- package/packages/datadog-plugin-openai-agents/src/util.js +0 -1
- package/packages/datadog-plugin-playwright/src/index.js +76 -9
- package/packages/datadog-plugin-postgres/src/index.js +167 -0
- package/packages/datadog-plugin-prisma/src/datadog-tracing-helper.js +0 -1
- package/packages/datadog-plugin-redis/src/index.js +15 -1
- package/packages/datadog-plugin-supabase/src/error.js +18 -0
- package/packages/datadog-plugin-supabase/src/index.js +21 -0
- package/packages/datadog-plugin-supabase/src/supabase-auth-js-gotrueclient-getuser.js +69 -0
- package/packages/datadog-plugin-supabase/src/supabase-functions-js-functionsclient-invoke.js +70 -0
- package/packages/datadog-plugin-supabase/src/supabase-postgrest-js-postgrestbuilder-then.js +122 -0
- package/packages/datadog-plugin-supabase/src/supabase-realtime-js-realtimechannel-send.js +66 -0
- package/packages/datadog-plugin-supabase/src/supabase-storage-js-handle-request.js +93 -0
- package/packages/datadog-plugin-supabase/src/url.js +17 -0
- package/packages/datadog-plugin-vitest/src/index.js +10 -0
- package/packages/datadog-plugin-ws/src/util.js +0 -4
- package/packages/datadog-turbopack/index.js +0 -6
- package/packages/datadog-turbopack/src/loader.js +0 -16
- package/packages/datadog-webpack/src/loader.js +0 -1
- package/packages/dd-trace/src/aiguard/client.js +2 -3
- package/packages/dd-trace/src/aiguard/evaluation.js +6 -12
- package/packages/dd-trace/src/aiguard/index.js +2 -2
- package/packages/dd-trace/src/aiguard/integrations/index.js +1 -1
- package/packages/dd-trace/src/aiguard/integrations/openai.js +81 -7
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +0 -2
- package/packages/dd-trace/src/aiguard/messages/openai.js +64 -1
- package/packages/dd-trace/src/aiguard/messages/utils.js +0 -1
- package/packages/dd-trace/src/aiguard/redaction.js +0 -1
- package/packages/dd-trace/src/aiguard/sdk.js +1 -1
- package/packages/dd-trace/src/appsec/activation.js +1 -1
- package/packages/dd-trace/src/appsec/api_security/normalized-route.js +0 -5
- package/packages/dd-trace/src/appsec/api_security/sampler.js +0 -4
- package/packages/dd-trace/src/appsec/blocking/index.js +3 -3
- package/packages/dd-trace/src/appsec/downstream_requests.js +1 -4
- package/packages/dd-trace/src/appsec/iast/index.js +1 -1
- package/packages/dd-trace/src/appsec/iast/overhead-controller.js +5 -5
- package/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +1 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +1 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +3 -3
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +8 -10
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +2 -2
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +4 -1
- package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +7 -7
- package/packages/dd-trace/src/appsec/index.js +2 -2
- package/packages/dd-trace/src/appsec/lambda.js +71 -2
- package/packages/dd-trace/src/appsec/rasp/utils.js +5 -1
- package/packages/dd-trace/src/appsec/remote_config.js +8 -6
- package/packages/dd-trace/src/appsec/reporter.js +10 -6
- package/packages/dd-trace/src/appsec/rule_manager.js +3 -3
- package/packages/dd-trace/src/appsec/sdk/track_event.js +0 -2
- package/packages/dd-trace/src/appsec/waf/index.js +2 -2
- package/packages/dd-trace/src/appsec/waf/waf_manager.js +8 -5
- package/packages/dd-trace/src/carrier.js +0 -5
- package/packages/dd-trace/src/ci-visibility/dynamic-atr-retries.js +104 -0
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +0 -2
- package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +34 -3
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +0 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +0 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +0 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +0 -4
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +0 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +0 -2
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +37 -15
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +0 -3
- package/packages/dd-trace/src/ci-visibility/exporters/request.js +0 -11
- package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +0 -1
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +0 -1
- package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +0 -6
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +2 -2
- package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +0 -1
- package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +0 -5
- package/packages/dd-trace/src/ci-visibility/requests/video-request.js +0 -1
- package/packages/dd-trace/src/ci-visibility/telemetry.js +13 -0
- package/packages/dd-trace/src/ci-visibility/test-optimization-cache.js +0 -1
- package/packages/dd-trace/src/ci-visibility/test-screenshot.js +0 -2
- package/packages/dd-trace/src/ci-visibility/test-video.js +0 -1
- package/packages/dd-trace/src/config/defaults.js +3 -2
- package/packages/dd-trace/src/config/generated-config-types.d.ts +73 -67
- package/packages/dd-trace/src/config/helper.js +0 -1
- package/packages/dd-trace/src/config/index.js +24 -24
- package/packages/dd-trace/src/config/parsers.js +34 -6
- package/packages/dd-trace/src/config/remote_config.js +25 -109
- package/packages/dd-trace/src/config/supported-configurations.json +139 -10
- package/packages/dd-trace/src/datastreams/encoding.js +0 -1
- package/packages/dd-trace/src/datastreams/pathway.js +0 -1
- package/packages/dd-trace/src/debugger/constants.js +10 -0
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +62 -10
- package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
- package/packages/dd-trace/src/debugger/devtools_client/guardrail-metrics.js +15 -0
- package/packages/dd-trace/src/debugger/devtools_client/index.js +103 -27
- package/packages/dd-trace/src/debugger/devtools_client/json-buffer.js +50 -7
- package/packages/dd-trace/src/debugger/devtools_client/log.js +3 -1
- package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +21 -8
- package/packages/dd-trace/src/debugger/devtools_client/send.js +40 -14
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +53 -13
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +118 -71
- package/packages/dd-trace/src/debugger/devtools_client/snapshot-pruner.js +0 -3
- package/packages/dd-trace/src/debugger/devtools_client/status.js +20 -6
- package/packages/dd-trace/src/debugger/guardrail-metrics.js +167 -0
- package/packages/dd-trace/src/debugger/index.js +50 -2
- package/packages/dd-trace/src/debugger/inspect-segment.js +0 -3
- package/packages/dd-trace/src/debugger/probe_sampler.js +133 -9
- package/packages/dd-trace/src/debugger/probe_sampler_constants.js +31 -1
- package/packages/dd-trace/src/dogstatsd.js +0 -23
- package/packages/dd-trace/src/encode/0.4-cross-payload.js +0 -3
- package/packages/dd-trace/src/encode/0.4.js +0 -7
- package/packages/dd-trace/src/encode/0.5.js +0 -2
- package/packages/dd-trace/src/encode/tags-processors.js +0 -1
- package/packages/dd-trace/src/evp_proxy/discovery.js +0 -1
- package/packages/dd-trace/src/evp_proxy/path.js +0 -2
- package/packages/dd-trace/src/exporters/agent/writer.js +0 -1
- package/packages/dd-trace/src/exporters/agentless/index.js +0 -1
- package/packages/dd-trace/src/exporters/agentless/intake.js +0 -1
- package/packages/dd-trace/src/exporters/agentless/writer.js +0 -3
- package/packages/dd-trace/src/exporters/common/final-flush-request-tracker.js +0 -6
- package/packages/dd-trace/src/exporters/common/request.js +1 -0
- package/packages/dd-trace/src/exporters/common/url.js +0 -2
- package/packages/dd-trace/src/exporters/common/writer.js +0 -2
- package/packages/dd-trace/src/id.js +0 -11
- package/packages/dd-trace/src/llmobs/audio-codec.js +192 -0
- package/packages/dd-trace/src/llmobs/audio-utils.js +121 -0
- package/packages/dd-trace/src/llmobs/constants/audio.js +35 -0
- package/packages/dd-trace/src/llmobs/constants/prompts.js +14 -0
- package/packages/dd-trace/src/llmobs/constants/tags.js +17 -0
- package/packages/dd-trace/src/llmobs/eval-metric.js +0 -6
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +0 -4
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +0 -3
- package/packages/dd-trace/src/llmobs/experiments/index.js +7 -6
- package/packages/dd-trace/src/llmobs/experiments/util.js +1 -11
- package/packages/dd-trace/src/llmobs/index.js +1 -3
- package/packages/dd-trace/src/llmobs/noop.js +10 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +0 -4
- package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +0 -1
- package/packages/dd-trace/src/llmobs/plugins/genai/util.js +0 -4
- package/packages/dd-trace/src/llmobs/plugins/modelcontextprotocol-sdk/utils.js +0 -2
- package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +0 -2
- package/packages/dd-trace/src/llmobs/plugins/openai/index.js +4 -6
- package/packages/dd-trace/src/llmobs/plugins/openai/realtime.js +285 -0
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +16 -3
- package/packages/dd-trace/src/llmobs/prompts/cache.js +272 -0
- package/packages/dd-trace/src/llmobs/prompts/manager.js +639 -0
- package/packages/dd-trace/src/llmobs/prompts/noop.js +29 -0
- package/packages/dd-trace/src/llmobs/prompts/prompt.js +92 -0
- package/packages/dd-trace/src/llmobs/sdk.js +21 -6
- package/packages/dd-trace/src/llmobs/span_processor.js +81 -2
- package/packages/dd-trace/src/llmobs/tagger.js +30 -7
- package/packages/dd-trace/src/llmobs/telemetry.js +36 -4
- package/packages/dd-trace/src/llmobs/util.js +0 -34
- package/packages/dd-trace/src/llmobs/writers/base.js +0 -1
- package/packages/dd-trace/src/llmobs/writers/util.js +1 -2
- package/packages/dd-trace/src/log/channels.js +3 -0
- package/packages/dd-trace/src/log/index.js +23 -5
- package/packages/dd-trace/src/log-submission/log-submission-plugin.js +74 -6
- package/packages/dd-trace/src/msgpack/chunk.js +0 -1
- package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +0 -12
- package/packages/dd-trace/src/openfeature/encoding.js +0 -2
- package/packages/dd-trace/src/openfeature/eval-metrics-hook.js +0 -1
- package/packages/dd-trace/src/openfeature/index.js +0 -4
- package/packages/dd-trace/src/openfeature/span-enrichment-hook.js +0 -1
- package/packages/dd-trace/src/openfeature/span-enrichment.js +0 -4
- package/packages/dd-trace/src/openfeature/writers/base.js +0 -6
- package/packages/dd-trace/src/openfeature/writers/exposures.js +0 -5
- package/packages/dd-trace/src/openfeature/writers/util.js +0 -4
- package/packages/dd-trace/src/opentelemetry/logs/logger.js +0 -1
- package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +0 -1
- package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +0 -1
- package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +0 -2
- package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +0 -1
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +0 -1
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_transformer.js +0 -1
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -14
- package/packages/dd-trace/src/opentelemetry/metrics/time.js +0 -1
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +30 -13
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +0 -1
- package/packages/dd-trace/src/opentelemetry/span-helpers.js +0 -1
- package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +0 -1
- package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +0 -3
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +0 -6
- package/packages/dd-trace/src/opentracing/span_context.js +0 -1
- package/packages/dd-trace/src/otel-thread-ctx.js +38 -11
- package/packages/dd-trace/src/payload-tagging/tagging.js +15 -3
- package/packages/dd-trace/src/plugins/ci_plugin.js +0 -1
- package/packages/dd-trace/src/plugins/database.js +2 -6
- package/packages/dd-trace/src/plugins/index.js +7 -0
- package/packages/dd-trace/src/plugins/plugin.js +0 -4
- package/packages/dd-trace/src/plugins/tracing.js +1 -1
- package/packages/dd-trace/src/plugins/util/git.js +1 -3
- package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +0 -1
- package/packages/dd-trace/src/plugins/util/llm.js +0 -2
- package/packages/dd-trace/src/plugins/util/status-validator.js +0 -1
- package/packages/dd-trace/src/plugins/util/test.js +21 -20
- package/packages/dd-trace/src/plugins/util/url.js +0 -5
- package/packages/dd-trace/src/priority_sampler.js +0 -6
- package/packages/dd-trace/src/process-tags/index.js +0 -1
- package/packages/dd-trace/src/profiler.js +132 -35
- package/packages/dd-trace/src/profiling/exporters/event_serializer.js +0 -1
- package/packages/dd-trace/src/profiling/profiler.js +101 -41
- package/packages/dd-trace/src/profiling/ssi-heuristics.js +29 -15
- package/packages/dd-trace/src/propagation-hash/index.js +0 -1
- package/packages/dd-trace/src/proxy.js +21 -45
- package/packages/dd-trace/src/random_sampler.js +0 -4
- package/packages/dd-trace/src/rate_limiter.js +0 -4
- package/packages/dd-trace/src/remote_config/capabilities.js +1 -9
- package/packages/dd-trace/src/remote_config/index.js +0 -1
- package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +0 -3
- package/packages/dd-trace/src/sampler.js +0 -4
- package/packages/dd-trace/src/sampling_rule.js +0 -7
- package/packages/dd-trace/src/serverless/vercel.js +0 -1
- package/packages/dd-trace/src/serverless.js +0 -1
- package/packages/dd-trace/src/service-naming/index.js +0 -1
- package/packages/dd-trace/src/service-naming/schemas/util.js +0 -1
- package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +7 -0
- package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +5 -0
- package/packages/dd-trace/src/span_processor.js +2 -2
- package/packages/dd-trace/src/span_sampler.js +0 -1
- package/packages/dd-trace/src/standalone/product.js +2 -2
- package/packages/dd-trace/src/standalone/tracesource.js +0 -1
- package/packages/dd-trace/src/startup-log.js +1 -1
- package/packages/dd-trace/src/telemetry/endpoints.js +0 -8
- package/packages/dd-trace/src/telemetry/logs/index.js +16 -5
- package/packages/dd-trace/src/telemetry/logs/log-collector.js +16 -6
- package/packages/dd-trace/src/telemetry/metrics.js +0 -2
- package/packages/dd-trace/src/telemetry/telemetry.js +4 -1
- package/packages/dd-trace/src/util.js +13 -1
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
LLMOBS_AUDIO_ACCUMULATE_MAX_BYTES,
|
|
5
|
+
} = require('../../../dd-trace/src/llmobs/constants/audio')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Collects one side of a realtime turn's audio, decoding each base64 frame to a `Buffer` as it
|
|
9
|
+
* arrives.
|
|
10
|
+
*
|
|
11
|
+
* Decoding eagerly rather than retaining base64 costs 1x memory instead of 1.33x, and — because the
|
|
12
|
+
* segment is then just bytes — lets `trimLeading` and `capTo` cut at an exact byte offset with a
|
|
13
|
+
* `subarray`, where operating on base64 would have to decode and re-encode the straddling frame.
|
|
14
|
+
*
|
|
15
|
+
* `present` records that audio was seen at all, so a turn can still surface an `[audio]` marker when
|
|
16
|
+
* the bytes were dropped; `oversize` marks that the retention cap is currently spent, which stops
|
|
17
|
+
* `append` retaining anything further. It is a statement about the bytes held right now, not a
|
|
18
|
+
* permanent mark on the segment: `trimLeading` re-derives it from what survived, so shedding a
|
|
19
|
+
* lead-in that spent the cap reopens the segment for the speech that follows.
|
|
20
|
+
*
|
|
21
|
+
* Constructed with `retain = false` when nothing consumes the audio, which is the default APM-only
|
|
22
|
+
* configuration: the byte *count* still drives the speech windows, but no frame is decoded and no
|
|
23
|
+
* segment is held.
|
|
24
|
+
*/
|
|
25
|
+
class AudioAccumulator {
|
|
26
|
+
/** @type {Buffer[]} */
|
|
27
|
+
chunks = []
|
|
28
|
+
|
|
29
|
+
present = false
|
|
30
|
+
|
|
31
|
+
oversize = false
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Wall clock (epoch ms) when the first frame of this segment was observed, anchoring it on the
|
|
35
|
+
* session timeline. Set even when the bytes are later dropped, since `present` still surfaces a
|
|
36
|
+
* marker.
|
|
37
|
+
*
|
|
38
|
+
* @type {number | undefined}
|
|
39
|
+
*/
|
|
40
|
+
startTime = undefined
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The audio format in force when this segment's first frame arrived. A segment keeps the format it
|
|
44
|
+
* was captured under, so a later `session.update` cannot retime bytes that arrived before it.
|
|
45
|
+
*
|
|
46
|
+
* @type {string}
|
|
47
|
+
*/
|
|
48
|
+
mimeType = ''
|
|
49
|
+
|
|
50
|
+
/** @type {number} */
|
|
51
|
+
sampleRate = 0
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Total bytes seen for this segment, never reduced by the retention cap. Playback duration — and
|
|
55
|
+
* therefore the speaking window — is derived from this, so it stays accurate even when `chunks`
|
|
56
|
+
* was dropped.
|
|
57
|
+
*/
|
|
58
|
+
totalDecodedBytes = 0
|
|
59
|
+
|
|
60
|
+
/** Bytes currently retained across `chunks`. */
|
|
61
|
+
#retainedBytes = 0
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether anything consumes the bytes. When false only `totalDecodedBytes` is maintained.
|
|
65
|
+
*
|
|
66
|
+
* Resolved from `#shouldRetain` on the frame that opens the segment, not at construction: a
|
|
67
|
+
* pending input segment is created as soon as the previous turn starts and then sits open across
|
|
68
|
+
* the whole agent response, so construction time can be many seconds before any audio arrives and
|
|
69
|
+
* whatever was true then may not be true now. Deciding on the opening frame is as late as
|
|
70
|
+
* possible while still being once per segment, so retention can never flip mid-clip and leave a
|
|
71
|
+
* segment holding only part of itself.
|
|
72
|
+
*
|
|
73
|
+
* @type {boolean}
|
|
74
|
+
*/
|
|
75
|
+
#retain = true
|
|
76
|
+
|
|
77
|
+
/** @type {() => boolean} */
|
|
78
|
+
#shouldRetain
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {(() => boolean) | boolean} [shouldRetain]
|
|
82
|
+
*/
|
|
83
|
+
constructor (shouldRetain = true) {
|
|
84
|
+
this.#shouldRetain = typeof shouldRetain === 'function' ? shouldRetain : () => shouldRetain
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Add one base64 audio frame, reporting its decoded byte count for the input-buffer clock.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} base64
|
|
91
|
+
* @param {number} now - Epoch ms at which this frame was observed.
|
|
92
|
+
* @param {string} [mimeType] - The session's audio format, recorded on the first frame only.
|
|
93
|
+
* @param {number} [sampleRate]
|
|
94
|
+
*/
|
|
95
|
+
append (base64, now, mimeType = '', sampleRate = 0) {
|
|
96
|
+
if (typeof base64 !== 'string' || base64.length === 0) return 0
|
|
97
|
+
|
|
98
|
+
if (this.startTime === undefined) {
|
|
99
|
+
this.startTime = now
|
|
100
|
+
this.mimeType = mimeType
|
|
101
|
+
this.sampleRate = sampleRate
|
|
102
|
+
this.#retain = this.#shouldRetain()
|
|
103
|
+
}
|
|
104
|
+
this.present = true
|
|
105
|
+
|
|
106
|
+
// Size the frame without allocating — `byteLength` is exactly what `Buffer.from(base64,
|
|
107
|
+
// 'base64').length` would be — so every path that ends up discarding the frame pays nothing for
|
|
108
|
+
// it. Decoding first would make the retention cap a bound on what is *kept* rather than on peak
|
|
109
|
+
// allocation, and an oversize live stream would keep paying for discarded decodes.
|
|
110
|
+
const decodedLength = Buffer.byteLength(base64, 'base64')
|
|
111
|
+
this.totalDecodedBytes += decodedLength
|
|
112
|
+
|
|
113
|
+
if (!this.#retain || this.oversize) return decodedLength
|
|
114
|
+
|
|
115
|
+
if (this.#retainedBytes + decodedLength > LLMOBS_AUDIO_ACCUMULATE_MAX_BYTES) {
|
|
116
|
+
// Free what we had: the size guard would drop the whole segment anyway. `#retainedBytes` goes
|
|
117
|
+
// with it, so it never describes more bytes than `chunks` holds.
|
|
118
|
+
this.oversize = true
|
|
119
|
+
this.chunks = []
|
|
120
|
+
this.#retainedBytes = 0
|
|
121
|
+
return decodedLength
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
this.#retainedBytes += decodedLength
|
|
125
|
+
this.chunks.push(Buffer.from(base64, 'base64'))
|
|
126
|
+
return decodedLength
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Drop this segment's first `decodedBytes` bytes.
|
|
131
|
+
*
|
|
132
|
+
* Cuts the pre-speech audio a continuously-streaming client appends — the microphone keeps sending
|
|
133
|
+
* while the agent talks — off the front of a user turn, so the captured audio covers the same
|
|
134
|
+
* window the span reports.
|
|
135
|
+
*
|
|
136
|
+
* @param {number} decodedBytes
|
|
137
|
+
*/
|
|
138
|
+
trimLeading (decodedBytes) {
|
|
139
|
+
if (!(decodedBytes > 0)) return
|
|
140
|
+
|
|
141
|
+
if (decodedBytes >= this.totalDecodedBytes) {
|
|
142
|
+
// Everything seen so far precedes the onset, so the segment starts empty. Reset outright,
|
|
143
|
+
// retention cap included: otherwise a long silent lead-in — the buffer stays open across the
|
|
144
|
+
// whole previous agent response — could spend the cap before the speech even starts and leave
|
|
145
|
+
// the turn with nothing but an `[audio]` marker.
|
|
146
|
+
this.clear()
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let remaining = decodedBytes
|
|
151
|
+
while (this.chunks.length > 0 && remaining > 0) {
|
|
152
|
+
const chunk = this.chunks[0]
|
|
153
|
+
if (chunk.length > remaining) {
|
|
154
|
+
this.chunks[0] = chunk.subarray(remaining)
|
|
155
|
+
break
|
|
156
|
+
}
|
|
157
|
+
this.chunks.shift()
|
|
158
|
+
remaining -= chunk.length
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.#retainedBytes = 0
|
|
162
|
+
for (const chunk of this.chunks) this.#retainedBytes += chunk.length
|
|
163
|
+
|
|
164
|
+
// Reopen retention around whatever survived. This is what the trim is for: on a continuously
|
|
165
|
+
// streaming client the lead-in is what spends the cap — the buffer stays open across the whole
|
|
166
|
+
// previous agent response — and once `append` trips, it drops every later frame. Leaving the
|
|
167
|
+
// segment closed here would discard the user's actual speech and leave the turn with nothing but
|
|
168
|
+
// an `[audio]` marker, which is the outcome trimming exists to prevent.
|
|
169
|
+
//
|
|
170
|
+
// A segment that had already tripped the cap holds nothing, so it reopens empty and the clip
|
|
171
|
+
// starts at the next frame. `audio_start_ms` carries the session's `prefix_padding_ms`, so the
|
|
172
|
+
// bytes missing from the front are that padding rather than speech — `speech_started` fires at
|
|
173
|
+
// the onset, before there is speech to lose. The duration derived from `totalDecodedBytes`
|
|
174
|
+
// therefore runs a padding's-width longer than the clip it describes, which is the price of
|
|
175
|
+
// keeping the speech at all.
|
|
176
|
+
this.oversize = this.#retainedBytes > LLMOBS_AUDIO_ACCUMULATE_MAX_BYTES
|
|
177
|
+
|
|
178
|
+
this.totalDecodedBytes -= decodedBytes
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Shrink this segment to its first `decodedBytes` bytes.
|
|
183
|
+
*
|
|
184
|
+
* The mirror of `trimLeading`, for audio that was delivered but never heard: when the listener cuts
|
|
185
|
+
* the agent off, everything past that point is generated-but-unplayed. Absolute and shrink-only, so
|
|
186
|
+
* a client truncation and the server's acknowledgement of it apply the cap once between them.
|
|
187
|
+
*
|
|
188
|
+
* @param {number} decodedBytes
|
|
189
|
+
*/
|
|
190
|
+
capTo (decodedBytes) {
|
|
191
|
+
if (decodedBytes >= this.totalDecodedBytes) return
|
|
192
|
+
|
|
193
|
+
if (decodedBytes <= 0) {
|
|
194
|
+
// Nothing was heard at all, so the segment is empty rather than zero-length-but-present.
|
|
195
|
+
this.clear()
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let kept = 0
|
|
200
|
+
const chunks = []
|
|
201
|
+
for (const chunk of this.chunks) {
|
|
202
|
+
if (kept + chunk.length <= decodedBytes) {
|
|
203
|
+
chunks.push(chunk)
|
|
204
|
+
kept += chunk.length
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
const partial = chunk.subarray(0, decodedBytes - kept)
|
|
208
|
+
if (partial.length > 0) {
|
|
209
|
+
chunks.push(partial)
|
|
210
|
+
kept += partial.length
|
|
211
|
+
}
|
|
212
|
+
break
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
this.chunks = chunks
|
|
216
|
+
this.#retainedBytes = kept
|
|
217
|
+
this.totalDecodedBytes = decodedBytes
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* The retained audio as one buffer. Empty when the segment was dropped or never had bytes.
|
|
222
|
+
*
|
|
223
|
+
* @returns {Buffer}
|
|
224
|
+
*/
|
|
225
|
+
toBuffer () {
|
|
226
|
+
return this.chunks.length === 1 ? this.chunks[0] : Buffer.concat(this.chunks, this.#retainedBytes)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
clear () {
|
|
230
|
+
this.chunks = []
|
|
231
|
+
this.present = false
|
|
232
|
+
this.oversize = false
|
|
233
|
+
this.startTime = undefined
|
|
234
|
+
this.mimeType = ''
|
|
235
|
+
this.sampleRate = 0
|
|
236
|
+
this.totalDecodedBytes = 0
|
|
237
|
+
this.#retainedBytes = 0
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = AudioAccumulator
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('./turns').ToolCall} ToolCall
|
|
5
|
+
* @typedef {import('./turns').ToolResult} ToolResult
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Collapse SDK naming drift so `response.output_audio.*` / `response.output_text.*` match their
|
|
10
|
+
* older `response.audio.*` / `response.text.*` equivalents.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} eventType
|
|
13
|
+
*/
|
|
14
|
+
function normalizeResponseEventType (eventType) {
|
|
15
|
+
// Only `response.*` events carry the renamed segments, and they are a small fraction of the
|
|
16
|
+
// stream, so skip the three replacements entirely for everything else.
|
|
17
|
+
if (!eventType.startsWith('response.')) return eventType
|
|
18
|
+
|
|
19
|
+
return eventType
|
|
20
|
+
.replace('.output_audio_transcript', '.audio_transcript')
|
|
21
|
+
.replace('.output_audio', '.audio')
|
|
22
|
+
.replace('.output_text', '.text')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Pull function and MCP tool usage out of a `response.done`'s output items.
|
|
27
|
+
*
|
|
28
|
+
* Function calls become tool calls — the app returns their result later via `function_call_output`,
|
|
29
|
+
* captured on the next turn's input. MCP calls run server-side, so their result is inline on the
|
|
30
|
+
* item and is captured as a tool result alongside the call.
|
|
31
|
+
*
|
|
32
|
+
* `arguments` is left as the raw JSON string the API sent; the LLM Observability plugin parses it,
|
|
33
|
+
* so this stays a provider-native extraction.
|
|
34
|
+
*
|
|
35
|
+
* @param {{ output?: Array<Record<string, unknown>> }} response
|
|
36
|
+
* @returns {{ toolCalls: ToolCall[], toolResults: ToolResult[] }}
|
|
37
|
+
*/
|
|
38
|
+
function extractResponseTools (response) {
|
|
39
|
+
/** @type {ToolCall[]} */
|
|
40
|
+
const toolCalls = []
|
|
41
|
+
/** @type {ToolResult[]} */
|
|
42
|
+
const toolResults = []
|
|
43
|
+
|
|
44
|
+
const output = response?.output
|
|
45
|
+
if (!output) return { toolCalls, toolResults }
|
|
46
|
+
|
|
47
|
+
for (const item of output) {
|
|
48
|
+
if (item?.type === 'function_call') {
|
|
49
|
+
toolCalls.push({
|
|
50
|
+
name: String(item.name ?? ''),
|
|
51
|
+
arguments: item.arguments,
|
|
52
|
+
toolId: String(item.call_id ?? item.id ?? ''),
|
|
53
|
+
type: 'function',
|
|
54
|
+
})
|
|
55
|
+
} else if (item?.type === 'mcp_call') {
|
|
56
|
+
const toolId = String(item.id ?? '')
|
|
57
|
+
const name = String(item.name ?? '')
|
|
58
|
+
|
|
59
|
+
toolCalls.push({ name, arguments: item.arguments, toolId, type: 'mcp_call' })
|
|
60
|
+
|
|
61
|
+
const { output, error } = item
|
|
62
|
+
if (output != null || error != null) {
|
|
63
|
+
toolResults.push({
|
|
64
|
+
name,
|
|
65
|
+
result: String(output ?? error),
|
|
66
|
+
toolId,
|
|
67
|
+
type: 'mcp_tool_result',
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { toolCalls, toolResults }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { extractResponseTools, normalizeResponseEventType }
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const dc = require('dc-polyfill')
|
|
4
|
+
|
|
5
|
+
const { getValueFromEnvSources } = require('../../../dd-trace/src/config/helper')
|
|
6
|
+
const log = require('../../../dd-trace/src/log')
|
|
7
|
+
const shimmer = require('../../../datadog-shimmer')
|
|
8
|
+
const RealtimeSession = require('./session')
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('./session').TurnDescriptor} TurnDescriptor
|
|
12
|
+
* @typedef {import('./turns').ResponseTurn} ResponseTurn
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// One span per phase of a turn. `user speech` and `agent speech` share a channel: they differ only
|
|
16
|
+
// by name and resource, which is data, not a type.
|
|
17
|
+
const turnChannel = dc.tracingChannel('apm:openai:realtime:turn')
|
|
18
|
+
const responseChannel = dc.tracingChannel('apm:openai:realtime:response')
|
|
19
|
+
const speechChannel = dc.tracingChannel('apm:openai:realtime:speech')
|
|
20
|
+
|
|
21
|
+
// Published once per turn at `response.created`. The tracing and LLM Observability plugins each
|
|
22
|
+
// compose a `runInContext` onto the turn, so the tree replayed at finalize nests under whatever
|
|
23
|
+
// context the caller had active — the active context at finalize is the socket's, not theirs.
|
|
24
|
+
const captureContextChannel = dc.channel('dd-trace:openai:realtime:capture-context')
|
|
25
|
+
|
|
26
|
+
// Never published on — subscribed to as a capability signal, the way `captureContextChannel` is
|
|
27
|
+
// probed before publishing. Only LLM Observability consumes a turn's audio bytes (the tracing
|
|
28
|
+
// plugins read the model and the phase boundaries), and a disabled plugin unsubscribes its channels,
|
|
29
|
+
// so `hasSubscribers` here is exactly "someone will read the audio". Without it the default
|
|
30
|
+
// APM-only configuration decodes and buffers every frame of every turn for no consumer.
|
|
31
|
+
//
|
|
32
|
+
// A dedicated channel is what makes the distinction possible: the two plugin families share the
|
|
33
|
+
// `apm:openai:realtime:*` prefixes, so the turn channel cannot tell them apart.
|
|
34
|
+
const audioChannel = dc.channel('dd-trace:openai:realtime:audio')
|
|
35
|
+
|
|
36
|
+
const USER_SPEECH = { operation: 'createRealtimeUserSpeech', llmobsName: 'user speech' }
|
|
37
|
+
const AGENT_SPEECH = { operation: 'createRealtimeAgentSpeech', llmobsName: 'agent speech' }
|
|
38
|
+
|
|
39
|
+
/** @type {WeakMap<object, { session: RealtimeSession, finalize: () => void }>} */
|
|
40
|
+
const connections = new WeakMap()
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Live sessions, weakly, so a process exiting without closing its connections still reports the
|
|
44
|
+
* turns they hold. Weak because a realtime connection can outlive many turns and we must not be the
|
|
45
|
+
* reason one stays reachable.
|
|
46
|
+
*
|
|
47
|
+
* @type {Set<WeakRef<RealtimeSession>>}
|
|
48
|
+
*/
|
|
49
|
+
const liveSessions = new Set()
|
|
50
|
+
|
|
51
|
+
function noop () {}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Replay a finished turn as a back-dated span tree. Nesting falls out of the synchronous scoping of
|
|
55
|
+
* `traceSync`: the phase spans are created inside the turn root's callback, so each picks the root
|
|
56
|
+
* up as its parent from the active store without any explicit parent stamping.
|
|
57
|
+
*
|
|
58
|
+
* @param {TurnDescriptor} turn
|
|
59
|
+
*/
|
|
60
|
+
function emitTurn (turn) {
|
|
61
|
+
const run = turn.runInContext ?? (fn => fn())
|
|
62
|
+
|
|
63
|
+
run(() => {
|
|
64
|
+
turnChannel.traceSync(() => {
|
|
65
|
+
const { userSpeech, agentSpeech } = turn
|
|
66
|
+
|
|
67
|
+
if (userSpeech !== undefined) {
|
|
68
|
+
speechChannel.traceSync(noop, { turn, phase: USER_SPEECH, ...userSpeech })
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
responseChannel.traceSync(noop, {
|
|
72
|
+
turn,
|
|
73
|
+
startTime: turn.llm.startTime,
|
|
74
|
+
finishTime: turn.llm.finishTime,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
if (agentSpeech !== undefined) {
|
|
78
|
+
speechChannel.traceSync(noop, { turn, phase: AGENT_SPEECH, ...agentSpeech })
|
|
79
|
+
}
|
|
80
|
+
}, { turn, startTime: turn.root.startTime, finishTime: turn.root.finishTime })
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {ResponseTurn} turn
|
|
86
|
+
*/
|
|
87
|
+
function captureContext (turn) {
|
|
88
|
+
if (captureContextChannel.hasSubscribers) captureContextChannel.publish(turn)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Start tracking a connection, reading the model and base path off the URL the SDK already built.
|
|
93
|
+
*
|
|
94
|
+
* @param {object} emitter
|
|
95
|
+
* @returns {{ session: RealtimeSession, finalize: () => void }}
|
|
96
|
+
*/
|
|
97
|
+
function createConnection (emitter) {
|
|
98
|
+
let model
|
|
99
|
+
let basePath = ''
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const { url } = emitter
|
|
103
|
+
if (url) {
|
|
104
|
+
// Azure names it `deployment`; a sideband `call_id` connection has neither, and picks the
|
|
105
|
+
// model up from `session.created` instead.
|
|
106
|
+
model = url.searchParams.get('model') ?? url.searchParams.get('deployment') ?? undefined
|
|
107
|
+
// Origin and path only. The query string carries `Authorization` / `api-key` on the Azure
|
|
108
|
+
// path, before the SDK redacts them.
|
|
109
|
+
basePath = url.origin + url.pathname
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// The URL shape is not guaranteed across SDK versions; the session works without it.
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Asked per segment rather than snapshotted here: a plugin can be reconfigured while a connection
|
|
116
|
+
// is open, and a session that outlived its consumer would otherwise keep buffering audio nothing
|
|
117
|
+
// reads.
|
|
118
|
+
const session = new RealtimeSession({
|
|
119
|
+
emitTurn,
|
|
120
|
+
captureContext,
|
|
121
|
+
model,
|
|
122
|
+
basePath,
|
|
123
|
+
shouldRetainAudio: () => audioChannel.hasSubscribers,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const reference = new WeakRef(session)
|
|
127
|
+
liveSessions.add(reference)
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* @param {boolean} [failed] - Whether the connection ended abnormally, so a response still in
|
|
131
|
+
* flight is reported as failed rather than as a clean finish.
|
|
132
|
+
*/
|
|
133
|
+
const finalize = (failed = false) => {
|
|
134
|
+
liveSessions.delete(reference)
|
|
135
|
+
session.finishSession(Date.now(), failed)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const connection = { session, finalize }
|
|
139
|
+
attachSocketClose(emitter, finalize)
|
|
140
|
+
return connection
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Close codes that mean the conversation ended on purpose: a normal close, the peer going away, and
|
|
144
|
+
// the "no status received" code `ws` reports for a close frame that carried none. Anything else — a
|
|
145
|
+
// dropped connection (1006), an internal error (1011) — cut a response short, so a turn still in
|
|
146
|
+
// flight is a failure rather than a clean finish.
|
|
147
|
+
const NORMAL_CLOSE_CODES = new Set([1000, 1001, 1005])
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {unknown} code
|
|
151
|
+
*/
|
|
152
|
+
function isAbnormalClose (code) {
|
|
153
|
+
return typeof code === 'number' && !NORMAL_CLOSE_CODES.has(code)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Finalize when the server closes the connection, so a caller that never calls `close()` — or whose
|
|
158
|
+
* connection drops — still reports its turns. `close` alone covers both transports: `ws` emits
|
|
159
|
+
* `error` then always `close`, and a global `WebSocket` that fails to connect does the same.
|
|
160
|
+
*
|
|
161
|
+
* @param {object} emitter
|
|
162
|
+
* @param {(failed?: boolean) => void} finalize
|
|
163
|
+
*/
|
|
164
|
+
function attachSocketClose (emitter, finalize) {
|
|
165
|
+
try {
|
|
166
|
+
const { socket } = emitter
|
|
167
|
+
if (socket == null) return
|
|
168
|
+
|
|
169
|
+
// `ws` sockets are Node EventEmitters and also expose `addEventListener`, so check `on` first.
|
|
170
|
+
// The close code arrives as an argument on the `ws` surface and on the `CloseEvent` on the DOM
|
|
171
|
+
// one; an app-initiated `close()` goes through the shim below instead and is never flagged.
|
|
172
|
+
if (typeof socket.on === 'function') {
|
|
173
|
+
socket.once('close', code => finalize(isAbnormalClose(code)))
|
|
174
|
+
} else if (typeof socket.addEventListener === 'function') {
|
|
175
|
+
socket.addEventListener('close', event => finalize(isAbnormalClose(event?.code)), { once: true })
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
log.debug('Error attaching OpenAI realtime close listener: %s', error?.message)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @param {object} emitter
|
|
184
|
+
* @returns {{ session: RealtimeSession, finalize: () => void } | undefined}
|
|
185
|
+
*/
|
|
186
|
+
function getConnection (emitter) {
|
|
187
|
+
const connection = connections.get(emitter)
|
|
188
|
+
if (connection !== undefined) return connection
|
|
189
|
+
|
|
190
|
+
// Nothing is listening, so never allocate. Checked only on the miss path, so an established
|
|
191
|
+
// connection pays one WeakMap lookup per event.
|
|
192
|
+
if (!turnChannel.start.hasSubscribers) return
|
|
193
|
+
|
|
194
|
+
const created = createConnection(emitter)
|
|
195
|
+
connections.set(emitter, created)
|
|
196
|
+
return created
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Wrap the emitter's `_emit` — the single funnel every server event passes through, ahead of the
|
|
201
|
+
* app's own listeners.
|
|
202
|
+
*
|
|
203
|
+
* `_emit` is inherited from the SDK's generic `EventEmitter`, which its streaming helpers also use;
|
|
204
|
+
* `shimmer.wrap` defines an own property on the realtime prototype instead of touching that shared
|
|
205
|
+
* one.
|
|
206
|
+
*
|
|
207
|
+
* @param {object} prototype
|
|
208
|
+
*/
|
|
209
|
+
function patchRealtimeEmitter (prototype) {
|
|
210
|
+
if (prototype == null) return
|
|
211
|
+
|
|
212
|
+
shimmer.wrap(prototype, '_emit', _emit => function (name, event) {
|
|
213
|
+
// `_emit` fires twice per server event — once as `event`, once as the event's own type — so the
|
|
214
|
+
// second half of the traffic is eliminated by a pointer compare before any other work.
|
|
215
|
+
if (name === 'event') {
|
|
216
|
+
const connection = getConnection(this)
|
|
217
|
+
// Observed before the app's listeners run: this feature is timestamps, and the app's own
|
|
218
|
+
// audio-playback callbacks would otherwise land inside the measured window.
|
|
219
|
+
if (connection !== undefined) connection.session.onServerEvent(event, Date.now())
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return _emit.apply(this, arguments)
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Wrap a concrete realtime transport's `send` (every client event) and `close` (session end).
|
|
228
|
+
*
|
|
229
|
+
* @param {object} prototype
|
|
230
|
+
*/
|
|
231
|
+
function patchRealtimeTransport (prototype) {
|
|
232
|
+
if (prototype == null) return
|
|
233
|
+
|
|
234
|
+
shimmer.wrap(prototype, 'send', send => function (event) {
|
|
235
|
+
// Recorded only after the send succeeds, so a failed send doesn't attribute unsent audio to the
|
|
236
|
+
// next turn.
|
|
237
|
+
const result = send.apply(this, arguments)
|
|
238
|
+
|
|
239
|
+
const connection = getConnection(this)
|
|
240
|
+
if (connection !== undefined) connection.session.onClientEvent(event, Date.now())
|
|
241
|
+
|
|
242
|
+
return result
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
shimmer.wrap(prototype, 'close', close => function () {
|
|
246
|
+
const connection = connections.get(this)
|
|
247
|
+
try {
|
|
248
|
+
return close.apply(this, arguments)
|
|
249
|
+
} finally {
|
|
250
|
+
// In a `finally` deliberately. Every realtime transport wraps `socket.close()` in its own
|
|
251
|
+
// try/catch and routes a failure to `_onError` (which rejects a promise rather than
|
|
252
|
+
// rethrowing), so this call does not throw and the two placements are equivalent today —
|
|
253
|
+
// but if that ever changes, finalizing is what keeps the session from leaking. The socket's
|
|
254
|
+
// close event is the other half of that guarantee.
|
|
255
|
+
connection?.finalize()
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Realtime is a large wrapping surface that buffers audio in memory, so it can be turned off on its
|
|
262
|
+
* own without giving up the rest of the OpenAI integration.
|
|
263
|
+
*
|
|
264
|
+
* Read through every configuration source, not just `process.env`: this is an operator-facing kill
|
|
265
|
+
* switch, and an org that sets it through Fleet Automation or local stable config would otherwise
|
|
266
|
+
* see it reported as disabled in configuration telemetry while the patching stayed on. Resolving it
|
|
267
|
+
* here also applies the option's registered boolean parser and its `true` default.
|
|
268
|
+
*/
|
|
269
|
+
function realtimeEnabled () {
|
|
270
|
+
return getValueFromEnvSources('DD_OPENAI_REALTIME_ENABLED') !== false
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Submit whatever live sessions still hold, at process exit.
|
|
275
|
+
*
|
|
276
|
+
* This is the only backstop for a connection the app drops without closing. A `FinalizationRegistry`
|
|
277
|
+
* would not help: the socket's own `message` handler closes over the emitter, and libuv holds the
|
|
278
|
+
* socket for as long as the connection is open, so the emitter is never unreachable while there is
|
|
279
|
+
* anything left to report.
|
|
280
|
+
*/
|
|
281
|
+
function flushLiveSessions () {
|
|
282
|
+
for (const reference of liveSessions) {
|
|
283
|
+
const session = reference.deref()
|
|
284
|
+
if (session === undefined) {
|
|
285
|
+
liveSessions.delete(reference)
|
|
286
|
+
continue
|
|
287
|
+
}
|
|
288
|
+
// Idempotent, so a session already closed by `close()` or a socket close costs nothing here.
|
|
289
|
+
session.finishSession(Date.now())
|
|
290
|
+
}
|
|
291
|
+
liveSessions.clear()
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
globalThis[Symbol.for('dd-trace')]?.beforeExitHandlers?.add(flushLiveSessions)
|
|
295
|
+
|
|
296
|
+
module.exports = {
|
|
297
|
+
flushLiveSessions,
|
|
298
|
+
patchRealtimeEmitter,
|
|
299
|
+
patchRealtimeTransport,
|
|
300
|
+
realtimeEnabled,
|
|
301
|
+
}
|