dd-trace 6.8.0 → 6.9.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 +27 -25
- package/ci/init.js +6 -6
- package/ci/runbook.md +114 -20
- package/ci/test-optimization-validation/approval-artifacts.js +36 -3
- package/ci/test-optimization-validation/approval.js +78 -22
- package/ci/test-optimization-validation/blocker-category.js +24 -0
- package/ci/test-optimization-validation/ci-discovery.js +23 -17
- package/ci/test-optimization-validation/ci-package-scripts.js +34 -6
- package/ci/test-optimization-validation/ci-remediation.js +1 -1
- package/ci/test-optimization-validation/cli.js +280 -52
- package/ci/test-optimization-validation/command-blocker.js +159 -20
- package/ci/test-optimization-validation/command-output-policy.js +9 -3
- package/ci/test-optimization-validation/command-runner.js +9 -7
- package/ci/test-optimization-validation/environment.js +4 -2
- package/ci/test-optimization-validation/executable.js +15 -13
- package/ci/test-optimization-validation/execution-lock.js +70 -0
- package/ci/test-optimization-validation/framework-adapters/cucumber-validation.json +3 -0
- package/ci/test-optimization-validation/framework-adapters/cucumber.js +657 -5
- package/ci/test-optimization-validation/framework-adapters/vitest.js +517 -0
- package/ci/test-optimization-validation/generated-files.js +38 -28
- package/ci/test-optimization-validation/generated-test-contract.js +8 -4
- package/ci/test-optimization-validation/generated-verifier.js +24 -17
- package/ci/test-optimization-validation/literal-glob.js +52 -0
- package/ci/test-optimization-validation/manifest-loader.js +3 -0
- package/ci/test-optimization-validation/manifest-scaffold.js +300 -50
- package/ci/test-optimization-validation/manifest-schema.js +80 -17
- package/ci/test-optimization-validation/offline-fixtures.js +13 -4
- package/ci/test-optimization-validation/package-check.js +94 -0
- package/ci/test-optimization-validation/plan-writer.js +183 -10
- package/ci/test-optimization-validation/preflight-runner.js +110 -31
- package/ci/test-optimization-validation/project-build-artifact.js +31 -0
- package/ci/test-optimization-validation/redaction.js +18 -27
- package/ci/test-optimization-validation/report-writer.js +182 -48
- package/ci/test-optimization-validation/result-semantics.js +20 -3
- package/ci/test-optimization-validation/runner-command.js +40 -20
- package/ci/test-optimization-validation/runner-contract.js +79 -43
- package/ci/test-optimization-validation/scenarios/basic-reporting.js +59 -5
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +141 -35
- package/ci/test-optimization-validation/scenarios/helpers.js +30 -6
- package/ci/test-optimization-validation/static-diagnosis.js +10 -3
- package/ci/test-optimization-validation/test-output.js +2 -1
- package/ci/test-optimization-validation/validation-blocker.js +21 -0
- package/ci/vitest-no-worker-init-setup.mjs +30 -34
- package/index.d.ts +98 -1
- package/init.js +1 -17
- package/initialize.mjs +11 -0
- package/loader-hook.mjs +6 -4
- package/package.json +5 -3
- package/packages/datadog-core/src/utils/src/parse-tags.js +0 -1
- package/packages/datadog-esbuild/src/utils.js +16 -1
- package/packages/datadog-instrumentations/src/ai.js +61 -34
- package/packages/datadog-instrumentations/src/bluebird.js +6 -6
- package/packages/datadog-instrumentations/src/claude-agent-sdk.js +81 -5
- package/packages/datadog-instrumentations/src/cucumber.js +124 -47
- package/packages/datadog-instrumentations/src/dns.js +21 -2
- package/packages/datadog-instrumentations/src/electron.js +1 -0
- package/packages/datadog-instrumentations/src/fastify.js +3 -1
- package/packages/datadog-instrumentations/src/grpc/client.js +4 -2
- package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +3 -1
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +33 -9
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +118 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +20 -2
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.js +32 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +61 -0
- package/packages/datadog-instrumentations/src/helpers/router-helper.js +37 -6
- package/packages/datadog-instrumentations/src/jest/coverage-backfill.js +1 -1
- package/packages/datadog-instrumentations/src/jest.js +979 -233
- package/packages/datadog-instrumentations/src/mocha/main.js +107 -9
- package/packages/datadog-instrumentations/src/mocha/utils.js +64 -42
- package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +25 -0
- package/packages/datadog-instrumentations/src/mocha/worker.js +12 -18
- package/packages/datadog-instrumentations/src/mysql.js +1 -1
- package/packages/datadog-instrumentations/src/next.js +155 -23
- package/packages/datadog-instrumentations/src/openai.js +14 -0
- package/packages/datadog-instrumentations/src/pg.js +180 -6
- package/packages/datadog-instrumentations/src/playwright.js +173 -79
- package/packages/datadog-instrumentations/src/promise-js.js +4 -4
- package/packages/datadog-instrumentations/src/promise.js +3 -3
- package/packages/datadog-instrumentations/src/router.js +195 -19
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +43 -10
- package/packages/datadog-instrumentations/src/vitest-main.js +360 -56
- package/packages/datadog-instrumentations/src/vitest-util.js +42 -7
- package/packages/datadog-instrumentations/src/vitest-worker.js +234 -17
- package/packages/datadog-instrumentations/src/webdriverio.js +194 -18
- package/packages/datadog-instrumentations/src/when.js +3 -3
- package/packages/datadog-instrumentations/src/ws.js +5 -1
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +59 -48
- package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +1 -3
- package/packages/datadog-plugin-cucumber/src/index.js +5 -1
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +50 -46
- package/packages/datadog-plugin-cypress/src/support.js +7 -4
- package/packages/datadog-plugin-electron/src/net.js +10 -4
- package/packages/datadog-plugin-http/src/client.js +1 -1
- package/packages/datadog-plugin-http2/src/client.js +1 -1
- package/packages/datadog-plugin-jest/src/index.js +1 -18
- package/packages/datadog-plugin-jest/src/util.js +2 -0
- package/packages/datadog-plugin-kafkajs/src/producer.js +2 -1
- package/packages/datadog-plugin-mocha/src/index.js +318 -1
- package/packages/datadog-plugin-next/src/index.js +57 -9
- package/packages/datadog-plugin-openai/src/tracing.js +4 -0
- package/packages/datadog-plugin-pg/src/index.js +65 -1
- package/packages/datadog-plugin-playwright/src/index.js +1 -1
- package/packages/datadog-plugin-vitest/src/index.js +95 -7
- package/packages/datadog-shimmer/src/shimmer.js +6 -2
- package/packages/dd-trace/src/aiguard/sdk.js +3 -0
- package/packages/dd-trace/src/appsec/{blocking.js → blocking/index.js} +26 -6
- package/packages/dd-trace/src/appsec/handlers/auth.js +62 -0
- package/packages/dd-trace/src/appsec/handlers/http-request.js +215 -0
- package/packages/dd-trace/src/appsec/handlers/http-response.js +78 -0
- package/packages/dd-trace/src/appsec/handlers/http-shared.js +24 -0
- package/packages/dd-trace/src/appsec/handlers/payments.js +104 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/hsts-header-missing-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
- package/packages/dd-trace/src/appsec/index.js +48 -476
- package/packages/dd-trace/src/appsec/sdk/track_event.js +9 -0
- package/packages/dd-trace/src/ci-visibility/coverage-report-discovery.js +46 -14
- package/packages/dd-trace/src/ci-visibility/efd-retry-policy.js +89 -0
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +27 -13
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +17 -0
- package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +21 -1
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -7
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -14
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +21 -5
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +1 -1
- package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +5 -1
- package/packages/dd-trace/src/config/generated-config-types.d.ts +2 -0
- package/packages/dd-trace/src/config/helper.js +2 -0
- package/packages/dd-trace/src/config/index.js +16 -5
- package/packages/dd-trace/src/config/remote_config.js +1 -1
- package/packages/dd-trace/src/config/supported-configurations.json +8 -0
- package/packages/dd-trace/src/debugger/devtools_client/status.js +1 -1
- package/packages/dd-trace/src/encode/coverage-ci-visibility.js +0 -1
- package/packages/dd-trace/src/guardrails/apply-pm2-env.js +16 -0
- package/packages/dd-trace/src/guardrails/log.js +1 -10
- package/packages/dd-trace/src/llmobs/eval-metric.js +249 -0
- package/packages/dd-trace/src/llmobs/experiments/index.js +33 -25
- package/packages/dd-trace/src/llmobs/experiments/util.js +4 -2
- package/packages/dd-trace/src/llmobs/noop.js +2 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +5 -5
- package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +1 -1
- package/packages/dd-trace/src/llmobs/sdk.js +149 -68
- package/packages/dd-trace/src/llmobs/telemetry.js +18 -0
- package/packages/dd-trace/src/llmobs/writers/base.js +3 -0
- package/packages/dd-trace/src/llmobs/writers/util.js +2 -0
- package/packages/dd-trace/src/log/channels.js +1 -9
- package/packages/dd-trace/src/log/levels.js +12 -0
- package/packages/dd-trace/src/openfeature/writers/exposures.js +1 -1
- package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
- package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +1 -1
- package/packages/dd-trace/src/opentelemetry/metrics/meter.js +8 -4
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +1 -1
- package/packages/dd-trace/src/opentelemetry/span.js +4 -0
- package/packages/dd-trace/src/plugins/ci_plugin.js +21 -5
- package/packages/dd-trace/src/plugins/index.js +1 -0
- package/packages/dd-trace/src/plugins/util/ci.js +6 -0
- package/packages/dd-trace/src/plugins/util/git.js +6 -4
- package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +2 -2
- package/packages/dd-trace/src/plugins/util/ip_extractor.js +1 -1
- package/packages/dd-trace/src/plugins/util/stacktrace.js +3 -0
- package/packages/dd-trace/src/plugins/util/test.js +319 -75
- package/packages/dd-trace/src/plugins/util/url.js +1 -1
- package/packages/dd-trace/src/plugins/util/web.js +23 -1
- package/packages/dd-trace/src/profiler.js +1 -1
- package/packages/dd-trace/src/profiling/exporters/event_serializer.js +1 -1
- package/packages/dd-trace/src/profiling/profilers/events.js +2 -2
- package/packages/dd-trace/src/proxy.js +1 -0
- package/packages/dd-trace/src/ritm.js +5 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +4 -2
- package/packages/dd-trace/src/sampling_rule.js +4 -2
- package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +1 -1
- package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +1 -1
- package/packages/dd-trace/src/telemetry/logs/index.js +3 -0
- package/packages/dd-trace/src/telemetry/telemetry.js +1 -1
- package/vendor/dist/pprof-format/index.js +1 -1
- package/version.js +10 -8
- /package/packages/dd-trace/src/appsec/{blocked_templates.js → blocking/templates.js} +0 -0
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
const { channel } = require('dc-polyfill')
|
|
4
4
|
|
|
5
5
|
const { isError } = require('../util')
|
|
6
|
-
const tracerVersion = require('../../../../package.json').version
|
|
7
6
|
const logger = require('../log')
|
|
8
7
|
const { getValueFromEnvSources } = require('../config/helper')
|
|
9
8
|
const Span = require('../opentracing/span')
|
|
@@ -13,6 +12,17 @@ const {
|
|
|
13
12
|
INPUT_VALUE,
|
|
14
13
|
TRACE_ID,
|
|
15
14
|
} = require('./constants/tags')
|
|
15
|
+
const {
|
|
16
|
+
FEEDBACK_METRIC_TYPES,
|
|
17
|
+
FEEDBACK_TARGET_KEYS,
|
|
18
|
+
buildMetricTags,
|
|
19
|
+
validateAssessment,
|
|
20
|
+
validateLabel,
|
|
21
|
+
validateMetricType,
|
|
22
|
+
validateMetricValue,
|
|
23
|
+
validateReasoning,
|
|
24
|
+
validateTimestamp,
|
|
25
|
+
} = require('./eval-metric')
|
|
16
26
|
const {
|
|
17
27
|
getFunctionArguments,
|
|
18
28
|
validateKind,
|
|
@@ -392,92 +402,35 @@ class LLMObs extends NoopLLMObs {
|
|
|
392
402
|
}
|
|
393
403
|
|
|
394
404
|
const timestampMs = options.timestampMs || Date.now()
|
|
395
|
-
|
|
396
|
-
err = 'invalid_timestamp'
|
|
397
|
-
throw new Error('timestampMs must be a non-negative integer. Evaluation metric data will not be sent')
|
|
398
|
-
}
|
|
405
|
+
validateTimestamp(timestampMs, 'evaluation')
|
|
399
406
|
|
|
400
407
|
const { label, value, tags, reasoning, assessment, metadata } = options
|
|
401
408
|
const metricType = options.metricType?.toLowerCase()
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
err = 'invalid_metric_type'
|
|
408
|
-
throw new Error('metricType must be one of "categorical", "score", "boolean" or "json"')
|
|
409
|
-
}
|
|
410
|
-
if (metricType === 'categorical' && typeof value !== 'string') {
|
|
411
|
-
err = 'invalid_metric_value'
|
|
412
|
-
throw new Error('value must be a string for a categorical metric.')
|
|
413
|
-
}
|
|
414
|
-
if (metricType === 'score' && typeof value !== 'number') {
|
|
415
|
-
err = 'invalid_metric_value'
|
|
416
|
-
throw new Error('value must be a number for a score metric.')
|
|
417
|
-
}
|
|
418
|
-
if (metricType === 'boolean' && typeof value !== 'boolean') {
|
|
419
|
-
err = 'invalid_metric_value'
|
|
420
|
-
throw new Error('value must be a boolean for a boolean metric')
|
|
421
|
-
}
|
|
422
|
-
if (metricType === 'json' && (typeof value !== 'object' || value == null || Array.isArray(value))) {
|
|
423
|
-
err = 'invalid_metric_value'
|
|
424
|
-
throw new Error('value must be a JSON object for a json metric')
|
|
425
|
-
}
|
|
426
|
-
if (assessment != null && assessment !== 'pass' && assessment !== 'fail') {
|
|
427
|
-
err = 'invalid_assessment'
|
|
428
|
-
throw new Error('assessment must be pass or fail')
|
|
429
|
-
}
|
|
430
|
-
if (reasoning != null && typeof reasoning !== 'string') {
|
|
431
|
-
err = 'invalid_reasoning'
|
|
432
|
-
throw new Error('reasoning must be a string')
|
|
433
|
-
}
|
|
409
|
+
const labelValue = validateLabel(label, 'evaluation')
|
|
410
|
+
validateMetricType(metricType, 'evaluation')
|
|
411
|
+
validateMetricValue(metricType, value)
|
|
412
|
+
validateAssessment(assessment)
|
|
413
|
+
validateReasoning(reasoning)
|
|
434
414
|
if (metadata != null && (typeof metadata !== 'object' || Array.isArray(metadata))) {
|
|
435
415
|
err = 'invalid_metadata'
|
|
436
416
|
throw new Error('metadata must be a JSON object')
|
|
437
417
|
}
|
|
438
418
|
|
|
439
|
-
const evaluationTags = {
|
|
440
|
-
'ddtrace.version': tracerVersion,
|
|
441
|
-
ml_app: mlApp,
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (tags) {
|
|
445
|
-
for (const key in tags) {
|
|
446
|
-
const tag = tags[key]
|
|
447
|
-
if (typeof tag === 'string') {
|
|
448
|
-
evaluationTags[key] = tag
|
|
449
|
-
} else if (typeof tag.toString === 'function') {
|
|
450
|
-
evaluationTags[key] = tag.toString()
|
|
451
|
-
} else if (tag == null) {
|
|
452
|
-
evaluationTags[key] = Object.prototype.toString.call(tag)
|
|
453
|
-
} else {
|
|
454
|
-
// should be a rare case
|
|
455
|
-
// every object in JS has a toString, otherwise every primitive has its own toString
|
|
456
|
-
// null and undefined are handled above
|
|
457
|
-
err = 'invalid_tags'
|
|
458
|
-
throw new Error('Failed to parse tags. Tags for evaluation metrics must be strings')
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// When OTel tracing is enabled, add source:otel tag to allow backend to wait for OTel span conversion
|
|
464
|
-
if (this._config.DD_TRACE_OTEL_ENABLED) {
|
|
465
|
-
evaluationTags.source = 'otel'
|
|
466
|
-
}
|
|
467
|
-
|
|
468
419
|
const payload = {
|
|
420
|
+
event_kind: 'evaluation',
|
|
469
421
|
join_on: {
|
|
470
422
|
span: {
|
|
471
423
|
span_id: spanId,
|
|
472
424
|
trace_id: traceId,
|
|
473
425
|
},
|
|
474
426
|
},
|
|
475
|
-
label,
|
|
427
|
+
label: labelValue,
|
|
476
428
|
metric_type: metricType,
|
|
477
429
|
ml_app: mlApp,
|
|
478
430
|
[`${metricType}_value`]: value,
|
|
479
431
|
timestamp_ms: timestampMs,
|
|
480
|
-
|
|
432
|
+
// When OTel tracing is enabled, `source:otel` lets the backend wait for OTel span conversion
|
|
433
|
+
tags: buildMetricTags(tags, mlApp, 'evaluation', this._config.DD_TRACE_OTEL_ENABLED),
|
|
481
434
|
}
|
|
482
435
|
if (reasoning != null) {
|
|
483
436
|
payload.reasoning = reasoning
|
|
@@ -491,11 +444,139 @@ class LLMObs extends NoopLLMObs {
|
|
|
491
444
|
const currentStore = storage.getStore()
|
|
492
445
|
const routing = currentStore?.routingContext
|
|
493
446
|
evalMetricAppendCh.publish({ payload, routing })
|
|
447
|
+
} catch (e) {
|
|
448
|
+
if (e.ddErrorTag) err = e.ddErrorTag
|
|
449
|
+
throw e
|
|
494
450
|
} finally {
|
|
495
451
|
telemetry.recordSubmitEvaluation(options, err)
|
|
496
452
|
}
|
|
497
453
|
}
|
|
498
454
|
|
|
455
|
+
/**
|
|
456
|
+
* Submits end-user feedback for a span, trace, session, or customer-defined entity.
|
|
457
|
+
*
|
|
458
|
+
* Exactly one target must be provided: `span` (as returned by `llmobs.exportSpan()`) or
|
|
459
|
+
* `spanId` to target a span, `traceId` a trace, `sessionId` a session, or `feedbackJoinKey`
|
|
460
|
+
* a customer-defined entity.
|
|
461
|
+
* `label`, `metricType`, `value` and `submitter` are required, and are validated at call time.
|
|
462
|
+
* @param {object} [options] - The feedback options.
|
|
463
|
+
* @param {string} [options.label] - The name of the feedback metric.
|
|
464
|
+
* @param {'categorical' | 'score' | 'boolean' | 'json' | 'text'} [options.metricType] - The value type.
|
|
465
|
+
* @param {string | number | boolean | Record<string, unknown>} [options.value] - The feedback value,
|
|
466
|
+
* matching `metricType`.
|
|
467
|
+
* @param {{ id: string, type?: string }} [options.submitter] - Who submitted the feedback.
|
|
468
|
+
* @param {{ traceId: string, spanId: string }} [options.span] - Span context to attach the feedback to.
|
|
469
|
+
* @param {string} [options.spanId] - ID of the span to attach the feedback to.
|
|
470
|
+
* @param {string} [options.traceId] - ID of the trace to attach the feedback to.
|
|
471
|
+
* @param {string} [options.sessionId] - ID of the session to attach the feedback to.
|
|
472
|
+
* @param {string} [options.feedbackJoinKey] - Customer-defined key to attach the feedback to.
|
|
473
|
+
* @param {Record<string, string>} [options.tags] - Tags to attach to the feedback.
|
|
474
|
+
* @param {string} [options.mlApp] - The ML app name. Defaults to the configured one.
|
|
475
|
+
* @param {number} [options.timestampMs] - When the feedback was generated. Defaults to now.
|
|
476
|
+
* @param {'pass' | 'fail'} [options.assessment] - Assessment of the feedback.
|
|
477
|
+
* @param {string} [options.reasoning] - Explanation of the feedback.
|
|
478
|
+
* @returns {void}
|
|
479
|
+
*/
|
|
480
|
+
submitFeedback (options = {}) {
|
|
481
|
+
if (!this.enabled) return
|
|
482
|
+
|
|
483
|
+
let err = ''
|
|
484
|
+
let targetType = 'other'
|
|
485
|
+
let metricTypeTag = 'other'
|
|
486
|
+
try {
|
|
487
|
+
const { span, spanId, traceId, sessionId, feedbackJoinKey, submitter } = options
|
|
488
|
+
|
|
489
|
+
// Resolved before any validation so telemetry still reports the metric type of a
|
|
490
|
+
// submission that fails on an unrelated field.
|
|
491
|
+
const metricType = options.metricType?.toLowerCase()
|
|
492
|
+
if (FEEDBACK_METRIC_TYPES.includes(metricType)) metricTypeTag = metricType
|
|
493
|
+
|
|
494
|
+
// The intake keys feedback off a single top-level identifier, so more than one target
|
|
495
|
+
// would be ambiguous and none would leave the feedback unattached. `span` also carries a
|
|
496
|
+
// traceId, but passing it is wire-equivalent to passing its `spanId` directly.
|
|
497
|
+
let targetName, targetValue
|
|
498
|
+
let targetCount = 0
|
|
499
|
+
if (span != null) { targetName = 'span'; targetValue = span.spanId; targetCount++ }
|
|
500
|
+
if (spanId != null) { targetName = 'spanId'; targetValue = spanId; targetCount++ }
|
|
501
|
+
if (traceId != null) { targetName = 'traceId'; targetValue = traceId; targetCount++ }
|
|
502
|
+
if (sessionId != null) { targetName = 'sessionId'; targetValue = sessionId; targetCount++ }
|
|
503
|
+
if (feedbackJoinKey != null) { targetName = 'feedbackJoinKey'; targetValue = feedbackJoinKey; targetCount++ }
|
|
504
|
+
|
|
505
|
+
if (targetCount !== 1) {
|
|
506
|
+
err = 'invalid_target_count'
|
|
507
|
+
throw new Error(
|
|
508
|
+
'Exactly one of `span`, `spanId`, `traceId`, `sessionId` or `feedbackJoinKey` ' +
|
|
509
|
+
'must be specified to submit feedback.'
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
targetType = FEEDBACK_TARGET_KEYS[targetName]
|
|
514
|
+
if (typeof targetValue !== 'string' || !targetValue) {
|
|
515
|
+
if (targetName === 'span') {
|
|
516
|
+
err = 'invalid_span'
|
|
517
|
+
throw new TypeError(
|
|
518
|
+
'`span` must be an object containing a non-empty string spanId. ' +
|
|
519
|
+
'`llmobs.exportSpan()` can be used to generate this object from a given span.'
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
err = `invalid_${targetType}`
|
|
523
|
+
throw new TypeError(`\`${targetName}\` must be a non-empty string`)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (typeof submitter?.id !== 'string' || !submitter.id) {
|
|
527
|
+
err = 'invalid_submitter'
|
|
528
|
+
throw new TypeError('submitter must be an object containing a non-empty string id')
|
|
529
|
+
}
|
|
530
|
+
if (submitter.type != null && typeof submitter.type !== 'string') {
|
|
531
|
+
err = 'invalid_submitter'
|
|
532
|
+
throw new TypeError('submitter.type must be a string')
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const mlApp = options.mlApp || this._config.llmobs.mlApp
|
|
536
|
+
if (!mlApp) {
|
|
537
|
+
err = 'missing_ml_app'
|
|
538
|
+
throw new Error('ML App name is required for sending feedback. Feedback data will not be sent.')
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const timestampMs = options.timestampMs || Date.now()
|
|
542
|
+
validateTimestamp(timestampMs, 'feedback')
|
|
543
|
+
|
|
544
|
+
const { label, value, tags, reasoning, assessment } = options
|
|
545
|
+
const labelValue = validateLabel(label, 'feedback')
|
|
546
|
+
validateMetricType(metricType, 'feedback')
|
|
547
|
+
validateMetricValue(metricType, value)
|
|
548
|
+
validateAssessment(assessment)
|
|
549
|
+
validateReasoning(reasoning)
|
|
550
|
+
|
|
551
|
+
const payload = {
|
|
552
|
+
event_kind: 'feedback',
|
|
553
|
+
[targetType]: targetValue,
|
|
554
|
+
label: labelValue,
|
|
555
|
+
metric_type: metricType,
|
|
556
|
+
ml_app: mlApp,
|
|
557
|
+
[`${metricType}_value`]: value,
|
|
558
|
+
timestamp_ms: timestampMs,
|
|
559
|
+
tags: buildMetricTags(tags, mlApp, 'feedback'),
|
|
560
|
+
submitter: submitter.type == null ? { id: submitter.id } : { id: submitter.id, type: submitter.type },
|
|
561
|
+
}
|
|
562
|
+
if (reasoning != null) {
|
|
563
|
+
payload.reasoning = reasoning
|
|
564
|
+
}
|
|
565
|
+
if (assessment != null) {
|
|
566
|
+
payload.assessment = assessment
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const currentStore = storage.getStore()
|
|
570
|
+
const routing = currentStore?.routingContext
|
|
571
|
+
evalMetricAppendCh.publish({ payload, routing })
|
|
572
|
+
} catch (e) {
|
|
573
|
+
if (e.ddErrorTag) err = e.ddErrorTag
|
|
574
|
+
throw e
|
|
575
|
+
} finally {
|
|
576
|
+
telemetry.recordSubmitFeedback(metricTypeTag, targetType, err)
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
499
580
|
annotationContext (options, fn) {
|
|
500
581
|
if (!this.enabled) return fn()
|
|
501
582
|
|
|
@@ -183,6 +183,23 @@ function recordSubmitEvaluation (options, err, value = 1) {
|
|
|
183
183
|
llmobsMetrics.count('evals_submitted', tags).inc(value)
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
/**
|
|
187
|
+
* @param {string} metricType - The submitted metric type, `'other'` if unrecognized.
|
|
188
|
+
* @param {string} targetType - The payload key the feedback was attached to, `'other'` if unresolved.
|
|
189
|
+
* @param {string} err - The telemetry error tag, empty when the submission succeeded.
|
|
190
|
+
* @param {number} [value]
|
|
191
|
+
* @returns {void}
|
|
192
|
+
*/
|
|
193
|
+
function recordSubmitFeedback (metricType, targetType, err, value = 1) {
|
|
194
|
+
const tags = {
|
|
195
|
+
error: Number(!!err),
|
|
196
|
+
metric_type: metricType,
|
|
197
|
+
target_type: targetType,
|
|
198
|
+
}
|
|
199
|
+
if (err) tags.error_type = err
|
|
200
|
+
llmobsMetrics.count('feedback_submitted', tags).inc(value)
|
|
201
|
+
}
|
|
202
|
+
|
|
186
203
|
function recordLLMObsUserProcessorCalled (error, value = 1) {
|
|
187
204
|
const tags = { error: error ? 1 : 0 }
|
|
188
205
|
llmobsMetrics.count('user_processor_called', tags).inc(value)
|
|
@@ -201,5 +218,6 @@ module.exports = {
|
|
|
201
218
|
recordUserFlush,
|
|
202
219
|
recordExportSpan,
|
|
203
220
|
recordSubmitEvaluation,
|
|
221
|
+
recordSubmitFeedback,
|
|
204
222
|
recordLLMObsUserProcessorCalled,
|
|
205
223
|
}
|
|
@@ -88,6 +88,9 @@ class BaseLLMObsWriter {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
/**
|
|
91
|
+
* @param {Record<string, unknown>} event
|
|
92
|
+
* @param {{ apiKey?: string, site?: string }} [routing]
|
|
93
|
+
* @param {number} [byteLength]
|
|
91
94
|
* @returns {boolean} `true` if the event was buffered, `false` if it was dropped
|
|
92
95
|
* (e.g. the per-routing buffer was full). Callers that depend on the event
|
|
93
96
|
* actually being submitted should check this value.
|
|
@@ -7,6 +7,8 @@ const { fetchAgentInfo } = require('../../agent/info')
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @param {import('../../config/config-base')} config
|
|
10
|
+
* @param {(agentless: boolean) => void} setWritersAgentlessValue
|
|
11
|
+
* @returns {void}
|
|
10
12
|
*/
|
|
11
13
|
function setAgentStrategy (config, setWritersAgentlessValue) {
|
|
12
14
|
const agentlessEnabled = config.llmobs.agentlessEnabled
|
|
@@ -2,15 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { channel } = require('dc-polyfill')
|
|
4
4
|
|
|
5
|
-
const Level =
|
|
6
|
-
trace: 10,
|
|
7
|
-
debug: 20,
|
|
8
|
-
info: 30,
|
|
9
|
-
warn: 40,
|
|
10
|
-
error: 50,
|
|
11
|
-
critical: 50,
|
|
12
|
-
off: 100,
|
|
13
|
-
}
|
|
5
|
+
const Level = require('./levels')
|
|
14
6
|
|
|
15
7
|
const traceChannel = channel('datadog:log:trace')
|
|
16
8
|
const debugChannel = channel('datadog:log:debug')
|
|
@@ -63,7 +63,7 @@ class ExposuresWriter extends BaseFFEWriter {
|
|
|
63
63
|
* @param {import('../../config/config-base')} config - Tracer configuration object
|
|
64
64
|
*/
|
|
65
65
|
constructor (config) {
|
|
66
|
-
const basePath = EVP_PROXY_AGENT_BASE_PATH.replace(
|
|
66
|
+
const basePath = EVP_PROXY_AGENT_BASE_PATH.replace(/\/$/, '')
|
|
67
67
|
const endpoint = EXPOSURES_ENDPOINT.replace(/^\/+/, '')
|
|
68
68
|
const fullEndpoint = `${basePath}/${endpoint}`
|
|
69
69
|
|
|
@@ -18,7 +18,7 @@ function setAgentStrategy (config, setWriterEnabledValue) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const endpoints = agentInfo.endpoints
|
|
21
|
-
const normalizedPath = EVP_PROXY_AGENT_BASE_PATH.replace(
|
|
21
|
+
const normalizedPath = EVP_PROXY_AGENT_BASE_PATH.replace(/\/$/, '')
|
|
22
22
|
const hasEndpoint = Array.isArray(endpoints) &&
|
|
23
23
|
endpoints.includes(normalizedPath) || endpoints.includes(normalizedPath + '/')
|
|
24
24
|
|
|
@@ -22,7 +22,7 @@ class BatchLogRecordProcessor {
|
|
|
22
22
|
/**
|
|
23
23
|
* Creates a new BatchLogRecordProcessor instance.
|
|
24
24
|
*
|
|
25
|
-
* @param {
|
|
25
|
+
* @param {import('./otlp_http_log_exporter')} exporter - Log processor for exporting batches to Datadog Agent
|
|
26
26
|
* @param {number} batchTimeout - Timeout in milliseconds for batch processing
|
|
27
27
|
* @param {number} maxExportBatchSize - Maximum number of log records per batch
|
|
28
28
|
*/
|
|
@@ -9,6 +9,8 @@ const { METRIC_TYPES } = require('./constants')
|
|
|
9
9
|
/**
|
|
10
10
|
* @typedef {import('@opentelemetry/api').MetricOptions} MetricOptions
|
|
11
11
|
* @typedef {import('@opentelemetry/core').InstrumentationScope} InstrumentationScope
|
|
12
|
+
* @typedef {Counter | UpDownCounter | Histogram | Gauge |
|
|
13
|
+
* ObservableGauge | ObservableCounter | ObservableUpDownCounter} Instrument
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
/**
|
|
@@ -25,7 +27,7 @@ class Meter {
|
|
|
25
27
|
/**
|
|
26
28
|
* Creates a new Meter instance.
|
|
27
29
|
*
|
|
28
|
-
* @param {
|
|
30
|
+
* @param {import('./meter_provider')} meterProvider - Parent meter provider
|
|
29
31
|
* @param {InstrumentationScope} instrumentationScope - Instrumentation scope information
|
|
30
32
|
* @param {string} [instrumentationScope.name] - Meter name (defaults to 'dd-trace-js')
|
|
31
33
|
* @param {string} [instrumentationScope.version] - Meter version (defaults to tracer version)
|
|
@@ -50,11 +52,13 @@ class Meter {
|
|
|
50
52
|
* Instruments are cached by type and normalized (lowercase) name.
|
|
51
53
|
*
|
|
52
54
|
*
|
|
55
|
+
* @template {Instrument} T
|
|
53
56
|
* @param {string} name - Instrument name (will be normalized to lowercase)
|
|
54
57
|
* @param {string} type - Instrument type (e.g., 'counter', 'histogram', 'gauge')
|
|
55
|
-
* @param {
|
|
56
|
-
*
|
|
57
|
-
* @
|
|
58
|
+
* @param {new (name: string, options: object, instrumentationScope: InstrumentationScope,
|
|
59
|
+
* reader: object) => T} InstrumentClass - Constructor for the instrument type
|
|
60
|
+
* @param {MetricOptions} options - Instrument options (description, unit, etc.)
|
|
61
|
+
* @returns {T} The instrument instance (new or cached)
|
|
58
62
|
*/
|
|
59
63
|
#getOrCreateInstrument (name, type, InstrumentClass, options) {
|
|
60
64
|
const normalizedName = name.toLowerCase()
|
|
@@ -109,7 +109,7 @@ class PeriodicMetricReader {
|
|
|
109
109
|
/**
|
|
110
110
|
* Creates a new PeriodicMetricReader instance.
|
|
111
111
|
*
|
|
112
|
-
* @param {
|
|
112
|
+
* @param {import('./otlp_http_metric_exporter')} exporter - Metric exporter for sending to Datadog Agent
|
|
113
113
|
* @param {number} exportInterval - Export interval in milliseconds
|
|
114
114
|
* @param {string} temporalityPreference - Temporality preference: DELTA, CUMULATIVE, or LOWMEMORY
|
|
115
115
|
* @param {number} maxBatchedQueueSize - Maximum number of measurements to queue before dropping
|
|
@@ -38,6 +38,10 @@ function hrTimeToMilliseconds (hrTime) {
|
|
|
38
38
|
/**
|
|
39
39
|
* Several of these attributes are not yet supported by the Node.js OTel API.
|
|
40
40
|
* We check for old equivalents where we can, but not all had equivalents.
|
|
41
|
+
*
|
|
42
|
+
* @param {string | undefined} spanName
|
|
43
|
+
* @param {import('@opentelemetry/api').SpanKind} kind
|
|
44
|
+
* @param {import('@opentelemetry/api').Attributes} attributes
|
|
41
45
|
*/
|
|
42
46
|
function spanNameMapper (spanName, kind, attributes) {
|
|
43
47
|
if (spanName) return spanName
|
|
@@ -169,7 +169,7 @@ function getTestSuiteLevelVisibilityTags (testSuiteSpan, testFramework) {
|
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
/**
|
|
172
|
-
* Keeps non-TIA settings while disabling suite skipping and coverage collection.
|
|
172
|
+
* Keeps non-TIA settings while disabling suite skipping and per-suite coverage collection.
|
|
173
173
|
*
|
|
174
174
|
* @param {object} libraryConfig
|
|
175
175
|
* @returns {object}
|
|
@@ -178,7 +178,6 @@ function disableTestImpactAnalysis (libraryConfig) {
|
|
|
178
178
|
return Object.freeze({
|
|
179
179
|
...libraryConfig,
|
|
180
180
|
isCodeCoverageEnabled: false,
|
|
181
|
-
isCoverageReportUploadEnabled: false,
|
|
182
181
|
isItrEnabled: false,
|
|
183
182
|
isSuitesSkippingEnabled: false,
|
|
184
183
|
})
|
|
@@ -260,6 +259,7 @@ module.exports = class CiPlugin extends Plugin {
|
|
|
260
259
|
{
|
|
261
260
|
...this.testConfiguration,
|
|
262
261
|
isCoverageReportUploadEnabled: this.libraryConfig?.isCoverageReportUploadEnabled,
|
|
262
|
+
isLineCoverageSupported: this.constructor.id !== 'vitest',
|
|
263
263
|
},
|
|
264
264
|
(err, skippableSuites, itrCorrelationId, skippableSuitesCoverage) => {
|
|
265
265
|
if (err) {
|
|
@@ -461,12 +461,28 @@ module.exports = class CiPlugin extends Plugin {
|
|
|
461
461
|
this.tracer._exporter.exportDiLogs(this.testEnvironmentMetadata, logMessage)
|
|
462
462
|
}
|
|
463
463
|
})
|
|
464
|
+
|
|
465
|
+
this.addSub(`ci:${this.constructor.id}:worker-report:telemetry`, data => {
|
|
466
|
+
const telemetryEvents = JSON.parse(data)
|
|
467
|
+
for (const event of telemetryEvents) {
|
|
468
|
+
if (event.type === 'ciVisEvent') {
|
|
469
|
+
this.telemetry.ciVisEvent(event.name, event.testLevel, {
|
|
470
|
+
...event.tags,
|
|
471
|
+
testFramework: event.testFramework,
|
|
472
|
+
isUnsupportedCIProvider: event.isUnsupportedCIProvider,
|
|
473
|
+
})
|
|
474
|
+
} else if (event.type === 'count') {
|
|
475
|
+
this.telemetry.count(event.name, event.tags, event.value)
|
|
476
|
+
} else if (event.type === 'distribution') {
|
|
477
|
+
this.telemetry.distribution(event.name, event.tags, event.measure)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
})
|
|
464
481
|
}
|
|
465
482
|
|
|
466
483
|
get telemetry () {
|
|
467
484
|
const testFramework = this.testFramework || this.constructor.id
|
|
468
485
|
const exporter = this.tracer?._exporter
|
|
469
|
-
// TODO: only jest worker supported yet
|
|
470
486
|
const isSupportedWorker = exporter && typeof exporter.exportTelemetry === 'function'
|
|
471
487
|
const ciProviderName = this.ciProviderName
|
|
472
488
|
|
|
@@ -1157,11 +1173,11 @@ module.exports = class CiPlugin extends Plugin {
|
|
|
1157
1173
|
return
|
|
1158
1174
|
}
|
|
1159
1175
|
|
|
1160
|
-
const { filePath, format } = coverageReports[reportIndex]
|
|
1176
|
+
const { filePath, fileDevice, fileInode, format } = coverageReports[reportIndex]
|
|
1161
1177
|
reportIndex++
|
|
1162
1178
|
|
|
1163
1179
|
this.tracer._exporter.uploadCoverageReport(
|
|
1164
|
-
{ filePath, format, testEnvironmentMetadata: this.testEnvironmentMetadata },
|
|
1180
|
+
{ filePath, fileDevice, fileInode, format, testEnvironmentMetadata: this.testEnvironmentMetadata },
|
|
1165
1181
|
(err) => {
|
|
1166
1182
|
if (err) {
|
|
1167
1183
|
failedCount++
|
|
@@ -36,6 +36,7 @@ const plugins = {
|
|
|
36
36
|
get '@smithy/core' () { return require('../../../datadog-plugin-aws-sdk/src') },
|
|
37
37
|
get '@smithy/smithy-client' () { return require('../../../datadog-plugin-aws-sdk/src') },
|
|
38
38
|
get '@vitest/runner' () { return require('../../../datadog-plugin-vitest/src') },
|
|
39
|
+
get '@wdio/jasmine-framework' () { return require('../../../datadog-plugin-mocha/src') },
|
|
39
40
|
get '@wdio/local-runner' () { return require('../../../datadog-plugin-mocha/src') },
|
|
40
41
|
get '@langchain/langgraph' () { return require('../../../datadog-plugin-langgraph/src') },
|
|
41
42
|
get '@openai/agents' () { return require('../../../datadog-plugin-openai-agents/src') },
|
|
@@ -114,6 +114,9 @@ const uniq = (items) => [...new Set(items)]
|
|
|
114
114
|
*
|
|
115
115
|
* This is much more robust than relying on hardcoded paths, especially on self-hosted runners
|
|
116
116
|
* and GHES environments where the runner may be installed under arbitrary directories/users.
|
|
117
|
+
*
|
|
118
|
+
* @param {string | undefined} runnerTemp value of the `RUNNER_TEMP` environment variable
|
|
119
|
+
* @returns {string[]}
|
|
117
120
|
*/
|
|
118
121
|
function getGithubDiagnosticDirsFromEnv (runnerTemp) {
|
|
119
122
|
const dirs = []
|
|
@@ -182,6 +185,9 @@ function expandGlobPattern (pattern) {
|
|
|
182
185
|
/**
|
|
183
186
|
* Expands a mixed list of literal directories and glob patterns into concrete
|
|
184
187
|
* directories. Literals pass through unchanged (existence is checked later).
|
|
188
|
+
*
|
|
189
|
+
* @param {string[]} candidates
|
|
190
|
+
* @returns {string[]}
|
|
185
191
|
*/
|
|
186
192
|
function expandDiagnosticDirCandidates (candidates) {
|
|
187
193
|
const expanded = []
|
|
@@ -38,6 +38,8 @@ const {
|
|
|
38
38
|
const { filterSensitiveInfoFromRepository } = require('./url')
|
|
39
39
|
const { cachedExec } = require('./git-cache')
|
|
40
40
|
|
|
41
|
+
/** @typedef {NonNullable<ReturnType<string['match']>>} RegExpMatch */
|
|
42
|
+
|
|
41
43
|
const legacyStorage = storage('legacy')
|
|
42
44
|
|
|
43
45
|
const GIT_REV_LIST_MAX_BUFFER = 12 * 1024 * 1024 // 12MB
|
|
@@ -116,12 +118,12 @@ function isShallowRepository () {
|
|
|
116
118
|
|
|
117
119
|
function getGitVersion () {
|
|
118
120
|
const gitVersionString = sanitizedExec('git', ['version'])
|
|
119
|
-
const gitVersionMatches = /** @type {
|
|
121
|
+
const gitVersionMatches = /** @type {RegExpMatch} */ (gitVersionString.match(/git version (\d+)\.(\d+)\.(\d+)/))
|
|
120
122
|
try {
|
|
121
123
|
return {
|
|
122
|
-
major: Number.parseInt(gitVersionMatches[1]),
|
|
123
|
-
minor: Number.parseInt(gitVersionMatches[2]),
|
|
124
|
-
patch: Number.parseInt(gitVersionMatches[3]),
|
|
124
|
+
major: Number.parseInt(gitVersionMatches[1], 10),
|
|
125
|
+
minor: Number.parseInt(gitVersionMatches[2], 10),
|
|
126
|
+
patch: Number.parseInt(gitVersionMatches[3], 10),
|
|
125
127
|
}
|
|
126
128
|
} catch {
|
|
127
129
|
return null
|
|
@@ -71,7 +71,7 @@ function decomposeServerUrl (rawUrl, obfuscatedUrl) {
|
|
|
71
71
|
address = stripIpv6Brackets(hostname)
|
|
72
72
|
}
|
|
73
73
|
if (parsed.port) {
|
|
74
|
-
const parsedPort = Number.parseInt(parsed.port)
|
|
74
|
+
const parsedPort = Number.parseInt(parsed.port, 10)
|
|
75
75
|
if (parsedPort > 0) port = parsedPort
|
|
76
76
|
}
|
|
77
77
|
path = parsed.pathname || '/'
|
|
@@ -210,7 +210,7 @@ function applyHttpOtelSemantics (formattedSpan) {
|
|
|
210
210
|
// metric (the OTLP exporter serializes meta as stringValue but metrics as
|
|
211
211
|
// intValue) — mirroring how server.port is handled below. Guard against a
|
|
212
212
|
// non-numeric status, which would otherwise write a NaN metric.
|
|
213
|
-
statusCode = Number.parseInt(status)
|
|
213
|
+
statusCode = Number.parseInt(status, 10)
|
|
214
214
|
if (Number.isFinite(statusCode)) newMetrics[HTTP_RESPONSE_STATUS_CODE] = statusCode
|
|
215
215
|
}
|
|
216
216
|
|
|
@@ -36,7 +36,7 @@ const privateIPMatcher = new net.BlockList()
|
|
|
36
36
|
for (const cidr of privateCIDRs) {
|
|
37
37
|
const [address, prefix] = cidr.split('/')
|
|
38
38
|
|
|
39
|
-
privateIPMatcher.addSubnet(address, Number.parseInt(prefix), net.isIPv6(address) ? 'ipv6' : 'ipv4')
|
|
39
|
+
privateIPMatcher.addSubnet(address, Number.parseInt(prefix, 10), net.isIPv6(address) ? 'ipv6' : 'ipv4')
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function extractIp (config, req) {
|
|
@@ -184,6 +184,9 @@ function isDDInstrumentationFile (fileName) {
|
|
|
184
184
|
* This function extracts the `myscript.js:10:3` part, passes it, returns the file name, line number, and column
|
|
185
185
|
* number and sets the `index` to the start of the whole location string.
|
|
186
186
|
*
|
|
187
|
+
* @param {string} stack the stack trace, or the nested location string when recursing
|
|
188
|
+
* @param {number} start index of the first character of the frame in `stack`
|
|
189
|
+
* @param {number} index index to start parsing backwards from
|
|
187
190
|
* @returns {[string, string, string, number]|undefined}
|
|
188
191
|
*/
|
|
189
192
|
function parseLocation (stack, start, index) {
|