Skip to content

feat(llmobs): add experiments dataset operations - #9458

Merged
mehulsonowal merged 20 commits into
masterfrom
feat/llmobs-node-experiments-p0
Jul 28, 2026
Merged

feat(llmobs): add experiments dataset operations#9458
mehulsonowal merged 20 commits into
masterfrom
feat/llmobs-node-experiments-p0

Conversation

@mehulsonowal

@mehulsonowal mehulsonowal commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds the Node.js P0 dataset operations surface for LLMObs Experiments, aligned with the Python SDK flow where datasets are created locally, pushed to Datadog, and then pulled back for validation/reuse.

Dataset creation and push/pull

  • Adds tracer.llmobs.createDataset(name, options) as a top-level alias for tracer.llmobs.experiments.createDataset(...).
  • Supports createDataset(name, { description, records }).
  • Supports explicit await dataset.push() to create the remote dataset and upload records.
  • Supports await tracer.llmobs.pullDataset(name, options) as a top-level alias for experiments.pullDataset(...).
  • Supports read-after-write validation with pullDataset(name, { expectedRecordCount, maxWaitMs }).

Dataset versions

  • Supports pullDataset(name, { version }).
  • Tracks dataset.version() and dataset.latestVersion().
  • Includes dataset_version in experiment create payloads when a pulled/pinned dataset version is available, so experiment runs are tied to the dataset version they used.

CSV dataset creation

  • Adds tracer.llmobs.createDatasetFromCsv(csvPath, name, options).
  • Supports selecting CSV columns for:
    • inputDataColumns
    • expectedOutputColumns
    • metadataColumns
    • idColumn
  • Supports custom CSV delimiters via csvDelimiter.
  • Uses csv-parse rather than a hand-written CSV parser.

Record IDs and validation

  • Supports custom record IDs for records passed through createDataset(..., { records }) and CSV import.
  • Includes custom IDs in pushed record payloads.
  • Validates duplicate custom record IDs before push.

Types, URLs, and tests

  • Updates public declarations in index.d.ts and index.d.v5.ts.
  • Maps staging UI URLs from datad0g.com to dd.datad0g.com.
  • Adds tests for:
    • dataset creation options
    • push/pull behavior
    • pinned-version pulls
    • CSV import
    • custom record IDs
    • dataset-version experiment linkage
    • staging URL generation

Motivation

This is the dataset-focused slice of the Node.js LLMObs Experiments P0 SDK work. It brings the Node dataset API closer to the existing dd-trace-py experiments ergonomics while keeping deeper dataset parity, such as tags, update/delete, batch update, and bulk upload, for follow-up work.

Testing notebooks / examples

Validation

Validated locally with targeted LLMObs experiments tests and lint.

./node_modules/.bin/eslint packages/dd-trace/src/llmobs/experiments/index.js   packages/dd-trace/src/llmobs/experiments/dataset.js   packages/dd-trace/src/llmobs/experiments/noop.js   packages/dd-trace/src/llmobs/sdk.js   packages/dd-trace/src/llmobs/noop.js   packages/dd-trace/test/llmobs/experiments/index.spec.js

unset OTEL_TRACES_EXPORTER OTEL_LOGS_EXPORTER OTEL_METRICS_EXPORTER
./node_modules/.bin/mocha packages/dd-trace/test/llmobs/experiments/index.spec.js
./node_modules/.bin/mocha packages/dd-trace/test/llmobs/experiments/experiment.spec.js

Follow-up work not included here

  • dataset tags and tag-based pulls
  • record tag operations
  • record update/delete
  • push options for deduplication and create-new-version
  • batch update endpoint support
  • bulk upload support
  • experiment tracing / row span correlation, which is handled in the stacked experiment tracing PR

@dd-octo-sts

dd-octo-sts Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Overall package size

Self size: 7.52 MB
Deduped: 8.18 MB
No deduping: 8.18 MB

Dependency sizes | name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.3.2 | 124.41 kB | 440.65 kB | | opentracing | 0.14.7 | 194.81 kB | 194.81 kB | | dc-polyfill | 0.1.11 | 25.74 kB | 25.74 kB |

🤖 This report was automatically generated by heaviest-objects-in-the-universe

@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Jul 21, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🔄 Datadog retried 2 tests - 2 passed on retry View in Datadog

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 98.39% (-0.06%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 947c4bb | Docs | Datadog PR Page | Give us feedback!

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.40%. Comparing base (ea21c8f) to head (947c4bb).
⚠️ Report is 29 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #9458      +/-   ##
==========================================
- Coverage   98.45%   98.40%   -0.06%     
==========================================
  Files         947      952       +5     
  Lines      128063   130038    +1975     
  Branches    10779    10998     +219     
==========================================
+ Hits       126087   127959    +1872     
- Misses       1976     2079     +103     
Flag Coverage Δ
aiguard 57.33% <17.45%> (-0.18%) ⬇️
aiguard-integration 56.20% <17.45%> (-0.14%) ⬇️
apm-bucket-0 57.58% <17.45%> (-0.24%) ⬇️
apm-bucket-1 63.84% <17.45%> (-0.20%) ⬇️
apm-bucket-2 62.60% <17.45%> (-0.22%) ⬇️
apm-bucket-3 60.06% <17.45%> (-0.19%) ⬇️
apm-capabilities-tracing 62.64% <17.45%> (-1.68%) ⬇️
apm-integrations-aerospike 56.62% <7.54%> (-0.23%) ⬇️
apm-integrations-confluentinc-kafka-javascript 61.47% <17.45%> (-0.22%) ⬇️
apm-integrations-couchbase 57.03% <17.45%> (-0.18%) ⬇️
apm-integrations-http 62.63% <17.45%> (-0.19%) ⬇️
apm-integrations-kafkajs 62.08% <17.45%> (-0.23%) ⬇️
apm-integrations-next 59.11% <17.45%> (-0.19%) ⬇️
apm-integrations-prisma 58.61% <17.45%> (-0.17%) ⬇️
appsec 72.69% <17.45%> (-0.29%) ⬇️
appsec-express_fastify_graphql 70.21% <17.45%> (-0.23%) ⬇️
appsec-integration 51.08% <17.45%> (-0.42%) ⬇️
appsec-kafka_ldapjs_lodash 63.82% <17.45%> (-0.21%) ⬇️
appsec-mongodb-core_mongoose_mysql 67.53% <17.45%> (-0.21%) ⬇️
appsec-next 57.48% <17.45%> (-0.14%) ⬇️
appsec-node-serialize_passport_postgres 67.21% <17.45%> (-0.21%) ⬇️
appsec-sourcing_stripe_template 65.56% <17.45%> (-0.20%) ⬇️
debugger 64.74% <17.45%> (-0.22%) ⬇️
instrumentations-bucket-0 51.74% <17.45%> (-0.12%) ⬇️
instrumentations-bucket-1 60.14% <17.45%> (-0.20%) ⬇️
instrumentations-bucket-10 61.94% <17.45%> (-0.20%) ⬇️
instrumentations-bucket-11 51.65% <17.45%> (-0.13%) ⬇️
instrumentations-bucket-12 52.22% <17.45%> (-0.12%) ⬇️
instrumentations-bucket-13 51.76% <17.45%> (-0.13%) ⬇️
instrumentations-bucket-2 53.63% <17.45%> (-0.13%) ⬇️
instrumentations-bucket-3 59.16% <17.45%> (-0.19%) ⬇️
instrumentations-bucket-4 52.30% <17.45%> (-0.13%) ⬇️
instrumentations-bucket-5 57.53% <17.45%> (-0.14%) ⬇️
instrumentations-bucket-6 60.69% <17.45%> (-0.19%) ⬇️
instrumentations-bucket-7 58.37% <17.45%> (-0.17%) ⬇️
instrumentations-bucket-8 59.50% <17.45%> (-0.19%) ⬇️
instrumentations-bucket-9 61.47% <17.45%> (-0.20%) ⬇️
instrumentations-instrumentation-couchbase 50.99% <6.60%> (-0.18%) ⬇️
instrumentations-integration-esbuild 34.01% <27.36%> (-0.27%) ⬇️
llmobs-ai_anthropic_bedrock 62.82% <17.45%> (-0.10%) ⬇️
llmobs-bucket-1 61.57% <17.45%> (-0.16%) ⬇️
llmobs-openai 62.15% <17.45%> (-0.31%) ⬇️
llmobs-openai-agents_vertex-ai 60.06% <17.45%> (+0.24%) ⬆️
llmobs-sdk 65.94% <100.00%> (+0.31%) ⬆️
master-coverage 98.40% <100.00%> (?)
openfeature 56.06% <17.45%> (-0.12%) ⬇️
openfeature-unit 53.36% <17.45%> (-0.14%) ⬇️
platform-core_esbuild_instrumentations-misc 40.66% <17.45%> (+0.01%) ⬆️
platform-integration 61.03% <17.45%> (-0.47%) ⬇️
platform-shimmer_unit-guardrails_webpack 39.09% <17.45%> (-0.20%) ⬇️
plugins-bucket-0 57.04% <17.45%> (-0.13%) ⬇️
plugins-bucket-1 54.32% <17.45%> (-0.13%) ⬇️
plugins-bucket-11 62.19% <17.45%> (-0.20%) ⬇️
plugins-bucket-18 61.89% <17.45%> (-0.20%) ⬇️
plugins-bucket-19 59.98% <17.45%> (-0.20%) ⬇️
plugins-bucket-20 61.97% <17.45%> (-0.20%) ⬇️
plugins-bucket-4 58.52% <17.45%> (-0.18%) ⬇️
plugins-bullmq_cassandra_cookie 61.66% <17.45%> (-0.21%) ⬇️
plugins-cookie-parser_crypto_dd-trace-api 56.68% <17.45%> (-0.18%) ⬇️
plugins-fetch_fs_generic-pool 58.78% <17.45%> (-0.12%) ⬇️
plugins-google-cloud-pubsub_grpc_handlebars 64.57% <17.45%> (-0.23%) ⬇️
plugins-hapi_hono_ioredis 60.16% <17.45%> (-0.19%) ⬇️
plugins-jest_knex_langgraph 55.57% <17.45%> (-0.16%) ⬇️
plugins-ldapjs_light-my-request_limitd-client 58.45% <17.45%> (-0.18%) ⬇️
plugins-lodash_mariadb_memcached 58.03% <17.45%> (-0.19%) ⬇️
plugins-moleculer_mongodb_mongodb-core 61.83% <17.45%> (-0.20%) ⬇️
plugins-mongoose_multer_mysql 59.03% <17.45%> (-0.18%) ⬇️
plugins-mysql2_nats_node-serialize 60.64% <17.45%> (-0.20%) ⬇️
plugins-opensearch_passport-http_pino 59.50% <17.45%> (-0.17%) ⬇️
plugins-postgres_process_pug 58.23% <17.45%> (-0.18%) ⬇️
plugins-redis_router_sequelize 62.03% <17.45%> (-0.19%) ⬇️
plugins-test-and-upstream-rhea_undici_url 61.57% <17.45%> (-0.19%) ⬇️
plugins-valkey_vm_winston 57.97% <17.45%> (-0.18%) ⬇️
plugins-ws 59.53% <17.45%> (-0.19%) ⬇️
profiling 62.06% <17.45%> (-0.21%) ⬇️
serverless-aws-sdk-aws-sdk 55.07% <17.45%> (-0.11%) ⬇️
serverless-aws-sdk-base-inject-field 50.93% <17.45%> (?)
serverless-aws-sdk-bedrockruntime 54.75% <17.45%> (-0.13%) ⬇️
serverless-aws-sdk-client 56.42% <17.45%> (-0.14%) ⬇️
serverless-aws-sdk-dynamodb 55.65% <17.45%> (-0.14%) ⬇️
serverless-aws-sdk-eventbridge 49.42% <17.45%> (-0.09%) ⬇️
serverless-aws-sdk-kinesis 59.29% <17.45%> (-0.18%) ⬇️
serverless-aws-sdk-lambda 57.38% <17.45%> (-0.14%) ⬇️
serverless-aws-sdk-s3 55.74% <17.45%> (-0.14%) ⬇️
serverless-aws-sdk-serverless-peer-service 59.71% <17.45%> (-0.16%) ⬇️
serverless-aws-sdk-sns 60.14% <17.45%> (-0.19%) ⬇️
serverless-aws-sdk-sqs 60.57% <17.45%> (-0.17%) ⬇️
serverless-aws-sdk-stepfunctions 55.57% <17.45%> (-0.14%) ⬇️
serverless-aws-sdk-util 51.49% <17.45%> (-0.13%) ⬇️
serverless-bucket-0 54.19% <6.60%> (-0.15%) ⬇️
serverless-bucket-1 59.19% <17.45%> (-0.18%) ⬇️
test-optimization-cucumber 71.93% <17.45%> (-0.09%) ⬇️
test-optimization-cypress 65.78% <17.45%> (+<0.01%) ⬆️
test-optimization-jest 73.21% <17.45%> (-0.14%) ⬇️
test-optimization-mocha 73.20% <17.45%> (-0.30%) ⬇️
test-optimization-playwright-playwright-atr 60.50% <17.45%> (+0.02%) ⬆️
test-optimization-playwright-playwright-efd 60.69% <17.45%> (+0.03%) ⬆️
test-optimization-playwright-playwright-final-status 60.80% <17.45%> (+0.17%) ⬆️
test-optimization-playwright-playwright-impacted-tests 60.38% <17.45%> (+0.17%) ⬆️
test-optimization-playwright-playwright-reporting 61.82% <17.45%> (+0.05%) ⬆️
test-optimization-playwright-playwright-test-management 61.19% <17.45%> (-0.09%) ⬇️
test-optimization-playwright-playwright-test-span 60.59% <17.45%> (-0.06%) ⬇️
test-optimization-selenium 60.07% <17.45%> (-0.17%) ⬇️
test-optimization-testopt 58.55% <17.45%> (+0.06%) ⬆️
test-optimization-vitest 70.30% <17.45%> (-0.03%) ⬇️
test-optimization-webdriverio 57.19% <17.45%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pr-commenter

pr-commenter Bot commented Jul 22, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-07-28 19:41:15

Comparing candidate commit 947c4bb in PR branch feat/llmobs-node-experiments-p0 with baseline commit ea21c8f in branch master.

📊 Benchmarking dashboard

Found 0 performance improvements and 0 performance regressions! Performance is the same for 2318 metrics, 40 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

Unstable benchmarks

These benchmarks have a confidence interval too wide to call a change; treat them as noise rather than signal.

scenario:appsec-appsec-enabled-24

  • unstable execution_time [-207.361ms; +218.044ms] or [-7.767%; +8.167%]

scenario:appsec-appsec-enabled-26

  • unstable execution_time [-238.512ms; +235.990ms] or [-9.376%; +9.277%]

scenario:appsec-appsec-enabled-with-attacks-26

  • unstable execution_time [-197.111ms; +189.268ms] or [-6.780%; +6.510%]

scenario:appsec-control-20

  • unstable execution_time [-113.841ms; +137.495ms] or [-6.981%; +8.432%]

scenario:appsec-control-24

  • unstable execution_time [-111675.749µs; +112366.082µs] or [-8.985%; +9.040%]

scenario:appsec-control-26

  • unstable execution_time [-129.123ms; +121.017ms] or [-10.488%; +9.830%]

scenario:appsec-iast-no-vulnerability-control-20

  • unstable execution_time [-11.112ms; +16.549ms] or [-4.346%; +6.472%]

scenario:appsec-iast-no-vulnerability-iast-enabled-always-active-20

  • unstable execution_time [-9.171ms; +18.305ms] or [-3.601%; +7.188%]

scenario:appsec-iast-no-vulnerability-iast-enabled-default-config-20

  • unstable execution_time [-9.527ms; +17.451ms] or [-3.698%; +6.773%]

scenario:debugger-line-probe-with-snapshot-default-24

  • unstable cpu_user_time [-2789.869ms; +2770.882ms] or [-31.338%; +31.125%]
  • unstable execution_time [-2812.654ms; +2786.310ms] or [-29.256%; +28.982%]
  • unstable instructions [-23886.6M instructions; +23862.1M instructions] or [-32.846%; +32.812%]
  • unstable max_rss_usage [-11862.614KB; +11790.614KB] or [-7.442%; +7.396%]
  • unstable throughput [-738.084op/s; +747.737op/s] or [-21.121%; +21.397%]

scenario:debugger-line-probe-with-snapshot-default-26

  • unstable cpu_user_time [-934.483ms; +297.538ms] or [-10.284%; +3.274%]
  • unstable execution_time [-931.995ms; +321.138ms] or [-9.480%; +3.266%]
  • unstable instructions [-8.3G instructions; +2.8G instructions] or [-10.999%; +3.751%]
  • unstable throughput [-90.497op/s; +253.137op/s] or [-2.753%; +7.701%]

scenario:debugger-line-probe-with-snapshot-minimal-24

  • unstable cpu_user_time [-1742.558ms; +574.110ms] or [-20.984%; +6.913%]
  • unstable execution_time [-1754.621ms; +592.509ms] or [-19.462%; +6.572%]
  • unstable instructions [-15.0G instructions; +4.9G instructions] or [-22.154%; +7.193%]
  • unstable throughput [-162.389op/s; +464.585op/s] or [-4.444%; +12.715%]

scenario:debugger-line-probe-without-snapshot-24

  • unstable cpu_user_time [-2056.679ms; +3198.159ms] or [-24.776%; +38.527%]
  • unstable execution_time [-2188.205ms; +3355.042ms] or [-24.276%; +37.221%]
  • unstable instructions [-17.1G instructions; +27.2G instructions] or [-25.284%; +40.089%]
  • unstable max_rss_usage [-8.408MB; +13.287MB] or [-5.356%; +8.465%]
  • unstable throughput [-868.722op/s; +582.741op/s] or [-23.717%; +15.909%]

scenario:debugger-line-probe-without-snapshot-26

  • unstable cpu_user_time [-3611.138ms; +3646.531ms] or [-35.032%; +35.376%]
  • unstable execution_time [-3648.495ms; +3698.823ms] or [-33.041%; +33.497%]
  • unstable instructions [-32498.1M instructions; +32600.6M instructions] or [-37.584%; +37.702%]
  • unstable max_rss_usage [-12529.578KB; +12405.578KB] or [-7.775%; +7.699%]
  • unstable throughput [-726.122op/s; +711.264op/s] or [-23.585%; +23.102%]

scenario:dogstatsd-with-tags-20

  • unstable cpu_user_time [-357.670ms; +290.742ms] or [-7.503%; +6.099%]
  • unstable execution_time [-355.645ms; +286.771ms] or [-7.352%; +5.928%]
  • unstable throughput [-100934.404op/s; +126505.651op/s] or [-5.824%; +7.299%]

scenario:plugin-graphql-long-with-depth-off-26

  • unstable max_rss_usage [-45.578MB; +16.892MB] or [-22.484%; +8.333%]

scenario:plugin-memcached-get-26

  • unstable cpu_user_time [-124.900ms; +202.276ms] or [-5.364%; +8.686%]
  • unstable execution_time [-125.506ms; +203.357ms] or [-5.240%; +8.490%]
  • unstable throughput [-1055422.642op/s; +646748.582op/s] or [-6.352%; +3.892%]

scenario:plugin-pg-service-26

  • unstable execution_time [-32.692ms; +57.500ms] or [-3.688%; +6.487%]

@mehulsonowal
mehulsonowal force-pushed the feat/llmobs-node-experiments-p0 branch from 40c29bd to f6ffbcd Compare July 22, 2026 19:20
@mehulsonowal
mehulsonowal marked this pull request as ready for review July 22, 2026 20:26
@mehulsonowal
mehulsonowal requested review from a team as code owners July 22, 2026 20:26

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e3f8e89d77

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

for (let i = 0; i < header.length; i++) row[header[i]] = values[i] ?? ''
return {
inputData: selectedColumns(row, inputDataColumns),
expectedOutput: selectedColumns(row, expectedOutputColumns),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Omit expected output for input-only CSV rows

When expectedOutputColumns is omitted or explicitly empty, this still sets every CSV record's expectedOutput to {}. DatasetRecord treats that as a real value, so ensureCreatedAndPushed() emits expected_output: {} and evaluators receive {} instead of the same null/absent expected output that createDataset(..., { records }) uses when expectedOutput is optional. This affects input-only CSV datasets and makes them appear to have an expected output.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an issue if true

Comment on lines +119 to +120
this.#version = response?.data?.attributes?.current_version ?? this.#version
this.#latestVersion = response?.data?.attributes?.current_version ?? this.#latestVersion

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh dataset version after appending records

For a new dataset with pending records, this stores current_version from the dataset-create response before the later records POST, and nothing below refreshes it after appending. When the create response includes the initial dataset version, Experiment.run() now sends dataset_version from this stale value, so create-and-run flows can pin the experiment to the pre-append/empty dataset version instead of the version containing the records just pushed.

Useful? React with 👍 / 👎.

Comment thread index.d.ts Outdated
records (): Array<{ input: any, expectedOutput: any, metadata: Record<string, any> }>
version (): number | null
latestVersion (): number | null
records (): Array<{ id: string | null, input: any, expectedOutput: any, metadata: Record<string, any> }>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Populate ids on pulled dataset records

This new public contract says records returned from records() have an id, but the pullDataset() path still builds each DatasetRecord without passing the backend item.id and only saves those ids in the separate private recordIds() array. For pulled datasets, pulled.records()[i].id is therefore always null even when the API returned a record id, so callers cannot use the advertised records API to correlate or update pulled rows.

Useful? React with 👍 / 👎.

Comment on lines +68 to +72
const rows = parseCsv(content, {
delimiter: csvDelimiter,
bom: true,
relaxColumnCount: true,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip blank CSV rows before creating records

Because the parser options do not skip empty lines, a CSV that contains a blank line between rows or at the end is turned into an extra dataset record with empty strings for the selected columns. In the createDatasetFromCsv() flow this silently uploads and evaluates a bogus empty-input row instead of ignoring the blank line, which is a common shape for hand-edited CSV files.

Useful? React with 👍 / 👎.

const query = cursor ? `?page[cursor]=${encodeURIComponent(cursor)}` : ''
const query = new URLSearchParams()
if (cursor) query.set('page[cursor]', cursor)
if (version !== undefined && version !== null) query.set('filter[version]', String(version))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pin latest pulls when fetching records

When callers pull the latest dataset without an explicit version, the code records current_version from the dataset listing but then fetches records without filter[version], so the records endpoint can return whatever version is current at request time. If the dataset is updated after the listing or between paginated requests, the local rows can come from a newer/mixed version while dataset.version() remains the older value, and Experiment.run() will create the experiment pinned to the wrong dataset version.

Useful? React with 👍 / 👎.

gsvigruha
gsvigruha previously approved these changes Jul 23, 2026
for (let i = 0; i < header.length; i++) row[header[i]] = values[i] ?? ''
return {
inputData: selectedColumns(row, inputDataColumns),
expectedOutput: selectedColumns(row, expectedOutputColumns),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an issue if true

@sabrenner sabrenner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as discussed offline, csv parsing will be moved to a different PR, so i reviewed the surrounding code. some suggestions but nothing blocking. will approve once the comments i left are either deemed new code worthy or not (although i think maybe only like one or two of them are actually potentially addressable, most are notes for follow-ups)!

Comment thread index.d.v5.ts Outdated
records (): Array<{ input: any, expectedOutput: any, metadata: Record<string, any> }>
version (): number | null
latestVersion (): number | null
records (): Array<{ id: string | null, input: any, expectedOutput: any, metadata: Record<string, any> }>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would say in a follow up if we can more strongly type this expectedOutput and input other than any, that'd be great (i could just be missing some context though, but as a rule of thumb it's a good practice to avoid any typing where possible!)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh ok thanks for pointing out; i'll mimic what we have for python here

fs.writeFileSync(csvPath, 'id;question;context;answer\nr1;"hello;world";ctx;ok\nr2;bye;ctx2;no\n')

const calls = []
global.fetch.callsFake(async (url, opts) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit for a follow-up: generally, i like doing anything like global overrides, or any kind of mocking, in beforeEach hooks and restore in afterEach hooks. otherwise, if this test fails, this mock could pollute other tests and have some unwanted side effects

assert.equal(create.body.data.attributes.ensure_unique, true)
assert.deepEqual(create.body.data.attributes.config, { approach: 'kw' })
})

it('uses the version returned by appending records when creating an experiment', async () => {
installFetch(calls, {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another note for follow ups - we can probably use the VCR for these tests like we do in Python so we can make real calls to the actual backend and just record responses. could be nicer than mocking out, but def not necessary for this PR

Comment thread packages/dd-trace/src/llmobs/sdk.js Outdated
Comment on lines +72 to +83
createDataset (name, descriptionOrOptions) {
return this.experiments.createDataset(name, descriptionOrOptions)
}

createDatasetFromCsv (csvPath, name, options) {
return this.experiments.createDatasetFromCsv(csvPath, name, options)
}

pullDataset (name, options) {
return this.experiments.pullDataset(name, options)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just wanna check on the semantics here - to me, it feels like it would make more sense to keep these under experiments, ie

llmobs.experiments.createDataset(...)

etc., and not expose them at the top level (they just call the underlying experiments object anyways). just my two cents tho, not blocking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah you're right, i'll reduce the scope so its just exposed via the experiments object

Comment thread packages/dd-trace/src/llmobs/noop.js Outdated
return new NoopExperiments('LLM Observability is not enabled')
}

createDataset (name, descriptionOrOptions) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we do decide to remove these from the top-level interface, then we can remove these as well, as a note!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note for a follow-up - throwing here seems aggressive if it's a no-op because someone intentionally disabled experiments. a warning log or something might be nicer. but i could also be missing context on the experience we wanna offer for users, so feel free to ignore or correct me!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totally makes sense! will change to warn log to be more graceful

Comment thread packages/dd-trace/src/llmobs/experiments/index.js Outdated

@sabrenner sabrenner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mostly optional nits again except for testing - it's alright, i think the old style of testing is preferable to a force-created cassette (if you look at the bodies and file titles of some other cassettes, example, you'll see that they have specific signatures and file contents that were auto-generated by the VCR capabilities of the testagent). i ideally don't wanna mix hand-made/force-created cassettes in with the generated ones from the testagent.

seeing as that is more work than it might be worth for just landing this, i think reverting back to the old testing style with inline mocks is OK! we can do the VCR approach in a follow-up down the line, if that sounds good 😄

Comment thread packages/dd-trace/src/llmobs/experiments/dataset.js
Comment thread packages/dd-trace/src/llmobs/experiments/dataset.js Outdated
Comment on lines 103 to 108
const queryString = query.toString() ? `?${query.toString()}` : ''
// eslint-disable-next-line no-await-in-loop
const resp = await this.#client.request(
'GET',
`${API_BASE_PATH}/${projectId}/datasets/${datasetId}/records${query}`
`${API_BASE_PATH}/${projectId}/datasets/${datasetId}/records${queryString}`
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const queryString = query.toString() ? `?${query.toString()}` : ''
// eslint-disable-next-line no-await-in-loop
const resp = await this.#client.request(
'GET',
`${API_BASE_PATH}/${projectId}/datasets/${datasetId}/records${query}`
`${API_BASE_PATH}/${projectId}/datasets/${datasetId}/records${queryString}`
)
// eslint-disable-next-line no-await-in-loop
const resp = await this.#client.request(
'GET',
`${API_BASE_PATH}/${projectId}/datasets/${datasetId}/records?${query.toString()}`
)

try this out but i think this should be fine? this way we dont have to stringify the query params twice. if it's an empty string, we'll just have something like /proj_id/datasets/dataset_id/record?, which should still be a valid url

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah looks cleaner and seems like it will work, will double check

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think these are cassettes created from using the VCR in the testagent docker fixture defined in docker-compose.yml. to use it, you can

  1. in docker-compose.yml, add an entry under VCR_PROVIDER_MAP for experiments, ie
VCR_PROVIDER_MAP=claude-agent-sdk=https://api.anthropic.com,experiments=https://api.datadoghq.com/api/v2/llm-obs/v1/proj/datasets`
  1. start up the container
docker compose up -d testagent
  1. locally, run tests with a DD_API_KEY to record real responses one time that can be re-used in CI without any additional configuration (hopefully!).

but it might be more work than it's worth. i think how you had the tests before with inline mocks was OK, and i think between that and this (ie force-created cassettes), reverting back to the other testing style of mocking is preferable.

Comment thread packages/dd-trace/test/llmobs/sdk/index.spec.js Outdated
mehulsonowal and others added 4 commits July 27, 2026 12:16
Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>
Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>
@mehulsonowal

mehulsonowal commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the experiments cassette feedback: removed the hand-made mock cassette files and moved those tests back to inline/stubbed client responses. Will follow up with correctly constructed cassettes

@mehulsonowal
mehulsonowal merged commit a57a59d into master Jul 28, 2026
665 checks passed
@mehulsonowal
mehulsonowal deleted the feat/llmobs-node-experiments-p0 branch July 28, 2026 23:02
dd-octo-sts Bot pushed a commit that referenced this pull request Jul 29, 2026
* feat(llmobs): add experiments dataset operations

* feat(llmobs): add CSV dataset creation

* chore(llmobs): use csv parser for dataset import

* chore(llmobs): add csv parser license

* feat(llmobs): include dataset version in experiments

* fix(llmobs): address dataset review feedback

* create jsontype to replace any

* remove dataset operations from llmobs

* remove csv dataset func

* noops produce warn logs instead of error throwing

* use vcr testing instead

* Update packages/dd-trace/src/llmobs/experiments/index.js

Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>

* fix(llmobs): address experiments ci failures

* test(llmobs): cover experiments noop accessors

* Apply suggestion from @sabrenner

Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>

* Apply suggestion from @sabrenner

Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>

* fix(llmobs): simplify dataset records query route

* test(llmobs): remove handmade experiment cassettes

* test(llmobs): isolate experiment fetch mocks

---------

Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>
@dd-octo-sts dd-octo-sts Bot mentioned this pull request Jul 29, 2026
dd-octo-sts Bot pushed a commit that referenced this pull request Jul 29, 2026
* feat(llmobs): add experiments dataset operations

* feat(llmobs): add CSV dataset creation

* chore(llmobs): use csv parser for dataset import

* chore(llmobs): add csv parser license

* feat(llmobs): include dataset version in experiments

* fix(llmobs): address dataset review feedback

* create jsontype to replace any

* remove dataset operations from llmobs

* remove csv dataset func

* noops produce warn logs instead of error throwing

* use vcr testing instead

* Update packages/dd-trace/src/llmobs/experiments/index.js

Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>

* fix(llmobs): address experiments ci failures

* test(llmobs): cover experiments noop accessors

* Apply suggestion from @sabrenner

Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>

* Apply suggestion from @sabrenner

Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>

* fix(llmobs): simplify dataset records query route

* test(llmobs): remove handmade experiment cassettes

* test(llmobs): isolate experiment fetch mocks

---------

Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>
@dd-octo-sts dd-octo-sts Bot mentioned this pull request Jul 29, 2026
sabrenner added a commit that referenced this pull request Jul 30, 2026
* feat(llmobs): add experiments dataset operations

* feat(llmobs): add CSV dataset creation

* chore(llmobs): use csv parser for dataset import

* chore(llmobs): add csv parser license

* feat(llmobs): include dataset version in experiments

* fix(llmobs): address dataset review feedback

* create jsontype to replace any

* remove dataset operations from llmobs

* remove csv dataset func

* noops produce warn logs instead of error throwing

* use vcr testing instead

* Update packages/dd-trace/src/llmobs/experiments/index.js

Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>

* fix(llmobs): address experiments ci failures

* test(llmobs): cover experiments noop accessors

* Apply suggestion from @sabrenner

Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>

* Apply suggestion from @sabrenner

Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>

* fix(llmobs): simplify dataset records query route

* test(llmobs): remove handmade experiment cassettes

* test(llmobs): isolate experiment fetch mocks

---------

Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>
sabrenner added a commit that referenced this pull request Jul 30, 2026
* feat(llmobs): add experiments dataset operations

* feat(llmobs): add CSV dataset creation

* chore(llmobs): use csv parser for dataset import

* chore(llmobs): add csv parser license

* feat(llmobs): include dataset version in experiments

* fix(llmobs): address dataset review feedback

* create jsontype to replace any

* remove dataset operations from llmobs

* remove csv dataset func

* noops produce warn logs instead of error throwing

* use vcr testing instead

* Update packages/dd-trace/src/llmobs/experiments/index.js

Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>

* fix(llmobs): address experiments ci failures

* test(llmobs): cover experiments noop accessors

* Apply suggestion from @sabrenner

Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>

* Apply suggestion from @sabrenner

Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>

* fix(llmobs): simplify dataset records query route

* test(llmobs): remove handmade experiment cassettes

* test(llmobs): isolate experiment fetch mocks

---------

Co-authored-by: Sam Brenner <106700075+sabrenner@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants