Skip to content

refactor(test): replace consecutive assert.strictEqual calls with assertObjectContains#7693

Merged
bm1549 merged 15 commits intomasterfrom
brian.marks/eslint-prefer-assert-object-contains
Mar 10, 2026
Merged

refactor(test): replace consecutive assert.strictEqual calls with assertObjectContains#7693
bm1549 merged 15 commits intomasterfrom
brian.marks/eslint-prefer-assert-object-contains

Conversation

@bm1549
Copy link
Copy Markdown
Contributor

@bm1549 bm1549 commented Mar 6, 2026

What does this PR do?

Replaces 3+ consecutive assert.strictEqual(obj.prop, value) calls on the same root object with a single assertObjectContains(obj, { ... }) call across ~50 test files.

This was originally generated via an ESLint auto-fix rule (now dropped per reviewer feedback), then manually reviewed and corrected.

Motivation

assertObjectContains produces much better error output when assertions fail — showing the full expected/actual objects rather than one property at a time. This makes test failures easier to diagnose.

What was changed

  • ~50 test files converted from consecutive assert.strictEqual to assertObjectContains
  • integration-tests/helpers/index.js unchanged — original Object.hasOwn semantics preserved
  • ESLint rule dropped per reviewer feedback (the auto-generated transformations are sufficient; the rule can't reliably detect all edge cases like prototype properties)

What was NOT changed (kept as assert.strictEqual)

  • Assertions checking undefined values (property absence) — Object.hasOwn doesn't match absent keys
  • Assertions on class instances with prototype properties (URL, Histogram, CountMetric, etc.)
  • Array indexing patterns and length/size property checks

Additional Notes

The assertObjectContains helper falls back to a custom partial-check implementation on Node.js < 22 (where assert.partialDeepStrictEqual doesn't exist), maintaining Object.hasOwn semantics throughout.

@bm1549 bm1549 added the AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos label Mar 6, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 6, 2026

Overall package size

Self size: 4.95 MB
Deduped: 5.79 MB
No deduping: 5.79 MB

Dependency sizes | name | version | self size | total size | |------|---------|-----------|------------| | import-in-the-middle | 3.0.0 | 81.15 kB | 815.98 kB | | dc-polyfill | 0.1.10 | 26.73 kB | 26.73 kB |

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

@datadog-official

This comment has been minimized.

bm1549 added a commit that referenced this pull request Mar 6, 2026
Move the eslint-prefer-assert-object-contains rule and its auto-fix
to a separate PR (#7693) for independent review and merge.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@pr-commenter
Copy link
Copy Markdown

pr-commenter bot commented Mar 6, 2026

Benchmarks

Benchmark execution time: 2026-03-10 15:53:44

Comparing candidate commit 343b852 in PR branch brian.marks/eslint-prefer-assert-object-contains with baseline commit 49c1975 in branch master.

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

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.90%. Comparing base (49c1975) to head (343b852).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #7693      +/-   ##
==========================================
- Coverage   80.47%   73.90%   -6.57%     
==========================================
  Files         740      712      -28     
  Lines       32023    30759    -1264     
==========================================
- Hits        25770    22732    -3038     
- Misses       6253     8027    +1774     
Flag Coverage Δ
aiguard-macos 38.88% <ø> (-0.10%) ⬇️
aiguard-ubuntu 39.00% <ø> (-0.10%) ⬇️
aiguard-windows 38.72% <ø> (-0.10%) ⬇️
apm-capabilities-tracing-macos 48.89% <ø> (ø)
apm-capabilities-tracing-ubuntu ?
apm-capabilities-tracing-windows 48.64% <ø> (-0.02%) ⬇️
apm-integrations-child-process 38.44% <ø> (-0.10%) ⬇️
apm-integrations-couchbase-18 ?
apm-integrations-couchbase-eol 37.67% <ø> (-0.11%) ⬇️
apm-integrations-oracledb ?
appsec-express 55.15% <ø> (-0.07%) ⬇️
appsec-fastify 51.50% <ø> (-0.07%) ⬇️
appsec-graphql 51.68% <ø> (-0.07%) ⬇️
appsec-kafka 44.21% <ø> (-0.16%) ⬇️
appsec-ldapjs 43.90% <ø> (-0.08%) ⬇️
appsec-lodash 43.56% <ø> (-0.08%) ⬇️
appsec-macos 58.15% <ø> (-0.07%) ⬇️
appsec-mongodb-core 48.69% <ø> (-0.08%) ⬇️
appsec-mongoose 49.36% <ø> (-0.08%) ⬇️
appsec-mysql 50.74% <ø> (-0.07%) ⬇️
appsec-node-serialize 43.08% <ø> (-0.08%) ⬇️
appsec-passport 47.48% <ø> (-0.09%) ⬇️
appsec-postgres 50.48% <ø> (-0.07%) ⬇️
appsec-sourcing 42.49% <ø> (-0.08%) ⬇️
appsec-template 43.25% <ø> (-0.08%) ⬇️
appsec-ubuntu ?
appsec-windows 58.01% <ø> (-0.06%) ⬇️
instrumentations-instrumentation-bluebird 32.29% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-body-parser 40.38% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-child_process 37.75% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-cookie-parser 34.27% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-express 34.59% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-express-mongo-sanitize 34.40% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-express-session 40.01% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-fs 31.91% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-generic-pool 29.91% <ø> (ø)
instrumentations-instrumentation-http 39.73% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-knex 32.30% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-mongoose 33.42% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-multer 40.13% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-mysql2 38.21% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-passport 43.88% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-passport-http 43.56% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-passport-local 44.09% <ø> (-0.09%) ⬇️
instrumentations-instrumentation-pg 37.65% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-promise 32.22% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-promise-js 32.23% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-q 32.27% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-url 32.20% <ø> (-0.10%) ⬇️
instrumentations-instrumentation-when 32.24% <ø> (-0.10%) ⬇️
llmobs-ai ?
llmobs-anthropic 40.19% <ø> (-0.09%) ⬇️
llmobs-bedrock 39.17% <ø> (-0.08%) ⬇️
llmobs-google-genai 39.73% <ø> (-0.09%) ⬇️
llmobs-langchain 39.97% <ø> (-0.08%) ⬇️
llmobs-openai ?
llmobs-vertex-ai 39.98% <ø> (-0.09%) ⬇️
platform-core 31.53% <ø> (ø)
platform-esbuild 34.48% <ø> (ø)
platform-instrumentations-misc 48.40% <ø> (ø)
platform-shimmer 37.63% <ø> (ø)
platform-unit-guardrails 32.95% <ø> (ø)
plugins-azure-event-hubs ?
plugins-azure-service-bus 25.19% <ø> (?)
plugins-bullmq ?
plugins-cassandra ?
plugins-cookie 26.89% <ø> (ø)
plugins-cookie-parser 26.67% <ø> (ø)
plugins-crypto ?
plugins-dd-trace-api 38.27% <ø> (-0.10%) ⬇️
plugins-express-mongo-sanitize ?
plugins-express-session ?
plugins-fastify 42.10% <ø> (-0.09%) ⬇️
plugins-fetch 38.26% <ø> (-0.10%) ⬇️
plugins-fs 38.54% <ø> (-0.10%) ⬇️
plugins-generic-pool 25.87% <ø> (ø)
plugins-google-cloud-pubsub 45.31% <ø> (-0.09%) ⬇️
plugins-grpc ?
plugins-handlebars 26.86% <ø> (ø)
plugins-hapi ?
plugins-hono ?
plugins-ioredis ?
plugins-knex ?
plugins-ldapjs ?
plugins-light-my-request ?
plugins-limitd-client ?
plugins-lodash ?
plugins-mariadb ?
plugins-memcached ?
plugins-microgateway-core ?
plugins-moleculer ?
plugins-mongodb ?
plugins-mongodb-core ?
plugins-mongoose ?
plugins-multer 26.63% <ø> (ø)
plugins-mysql ?
plugins-mysql2 ?
plugins-node-serialize ?
plugins-opensearch ?
plugins-passport-http ?
plugins-postgres ?
plugins-process ?
plugins-pug 26.89% <ø> (ø)
plugins-redis ?
plugins-router ?
plugins-sequelize ?
plugins-test-and-upstream-amqp10 38.43% <ø> (+0.04%) ⬆️
plugins-test-and-upstream-amqplib ?
plugins-test-and-upstream-apollo 38.93% <ø> (-0.09%) ⬇️
plugins-test-and-upstream-avsc 38.60% <ø> (-0.10%) ⬇️
plugins-test-and-upstream-bunyan 33.83% <ø> (-0.10%) ⬇️
plugins-test-and-upstream-connect ?
plugins-test-and-upstream-graphql ?
plugins-test-and-upstream-koa ?
plugins-test-and-upstream-protobufjs 38.82% <ø> (-0.10%) ⬇️
plugins-test-and-upstream-rhea ?
plugins-undici ?
plugins-url ?
plugins-valkey ?
plugins-vm 26.79% <ø> (ø)
plugins-winston ?
plugins-ws ?
profiling-macos 39.85% <ø> (-0.10%) ⬇️
profiling-ubuntu ?
profiling-windows 41.16% <ø> (-0.49%) ⬇️
serverless-azure-functions-client ?
serverless-azure-functions-eventhubs ?
serverless-azure-functions-servicebus ?

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

☔ View full report in Codecov by Sentry.
📢 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.

Copy link
Copy Markdown
Member

@BridgeAR BridgeAR left a comment

Choose a reason for hiding this comment

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

I am in favor of having such a rule, while this seems a bit too eager right now 😅

With a less eager check, the overall changes would also be less. I originally intended to write such a rule by myself after migrating more entries in a few smaller intermediate PRs.

@bm1549
Copy link
Copy Markdown
Contributor Author

bm1549 commented Mar 6, 2026

Addressed @BridgeAR's feedback:

Rule changes (eslint-prefer-assert-object-contains.mjs):

  • Added guard to skip groups where any property path uses a numeric computed index (e.g. arr[0], payload[1]) — these indicate array indexing, and using string/numeric keys in the expected object doesn't work correctly with assertObjectContains/partialDeepStrictEqual
  • Added guard to skip groups where the terminal property is length or size — these collection size properties don't work reliably on arrays/maps/sets
  • Added targeted test cases covering both new guards

Test file reverts:

  • Reverted integration-tests/helpers/index.js back to original Object.hasOwn check (removes the semantic change)
  • Reverted 65 test files that had invalid transformations (numeric indices like { 0: ..., 1: ... } or length:/size: as terminal properties inside assertObjectContains)
  • Re-ran npm run lint:fix with the corrected rule — only 10 files remain transformed, all clean (no numeric indices, no length/size terminal properties)

@BridgeAR
Copy link
Copy Markdown
Member

BridgeAR commented Mar 6, 2026

Added guard to skip groups where any property path uses a numeric computed index (e.g. arr[0], payload[1]) — these indicate array indexing, and using string/numeric keys in the expected object doesn't work correctly with assertObjectContains/partialDeepStrictEqual

Claude misunderstood a comment :) It works just fine for arrays.

@bm1549 bm1549 changed the title feat(eslint): add eslint-prefer-assert-object-contains with auto-fix refactor(test): replace consecutive assert.strictEqual calls with assertObjectContains Mar 6, 2026
@bm1549 bm1549 requested a review from BridgeAR March 6, 2026 18:05
@bm1549 bm1549 marked this pull request as ready for review March 6, 2026 18:11
@bm1549 bm1549 requested review from a team as code owners March 6, 2026 18:11
@bm1549 bm1549 enabled auto-merge (squash) March 6, 2026 18:12
@@ -128,9 +129,13 @@ describe('AppsecFsPlugin', () => {
let store = appsecFsPlugin._onFsOperationStart()

assert.ok(Object.hasOwn(store, 'fs'))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
assert.ok(Object.hasOwn(store, 'fs'))

@@ -242,11 +251,15 @@ describe('vulnerability-analyzer', () => {
const location = { path: 'file-name.js', line: 15 }
const vulnerability = vulnerabilityAnalyzer._createVulnerability(type, evidence, undefined, location)
assert.notStrictEqual(vulnerability, null)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
assert.notStrictEqual(vulnerability, null)

@@ -229,11 +234,15 @@ describe('vulnerability-analyzer', () => {
const spanId = 888
const vulnerability = vulnerabilityAnalyzer._createVulnerability(type, evidence, spanId)
assert.notStrictEqual(vulnerability, null)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
assert.notStrictEqual(vulnerability, null)

@@ -216,11 +217,15 @@ describe('vulnerability-analyzer', () => {
const spanId = 888
const vulnerability = vulnerabilityAnalyzer._createVulnerability(type, evidence, spanId, location)
assert.notStrictEqual(vulnerability, null)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
assert.notStrictEqual(vulnerability, null)

@@ -227,12 +240,16 @@ describe('Plugin', () => {
agent
.assertSomeTraces(traces => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We could use assertFirstTraceSpan directly. That would be a bit nicer.

status: 'error',
})

assertObjectContains(payload.tags, ['error_type:error type'])
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This can be combined with the above.

Comment on lines 256 to 257
assert.strictEqual(formattedEvent.subject.type, undefined)
assert.strictEqual(formattedEvent.subject.attributes, undefined)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can these not be included with the above?

Comment on lines 366 to 368
assert.strictEqual(trace.meta['service.name'], undefined)
assert.strictEqual(trace.meta['span.type'], undefined)
assert.strictEqual(trace.meta['resource.name'], undefined)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could any of these be included in the above?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe the undefined check here is actually an not existence check? Could we just change these cases to assert.ok(!Object.hasOwn(foo, 'property'))? That way the intent is clearer and all other undefined checks can be inlined into the assertObjectContains calls.

Comment on lines 557 to 559
assert.strictEqual(trace.meta['nested.A'], undefined)
assert.strictEqual(trace.meta['nested.A.B'], undefined)
assert.strictEqual(trace.meta['nested.A.num'], undefined)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could these be included above?

abc: '',
})
assert.ok(!('' in carrier))
assert.strictEqual(carrier.valid, '')
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This could be included above.

@bm1549 bm1549 requested a review from BridgeAR March 6, 2026 19:02
@bm1549
Copy link
Copy Markdown
Contributor Author

bm1549 commented Mar 6, 2026

Hi @BridgeAR! I've applied all the feedback from your review. Here's a summary of what was changed:

Removed redundant pre-checks before assertObjectContains:

  • packages/dd-trace/test/appsec/rasp/fs-plugin.spec.js — removed assert.ok(Object.hasOwn(store, 'fs'))
  • packages/dd-trace/test/appsec/iast/analyzers/vulnerability-analyzer.spec.js — removed 3× assert.notStrictEqual(vulnerability, null)
  • integration-tests/appsec/endpoints-collection.spec.js — removed assert.ok(found)
  • integration-tests/openfeature/openfeature-exposure-events.spec.js — removed 2× assert.ok(Object.hasOwn(payload, 'context'))

Converted assertSomeTraces to assertFirstTraceSpan:

  • packages/datadog-plugin-couchbase/test/index.spec.js — 6 cases
  • packages/datadog-plugin-google-cloud-vertexai/test/index.spec.js — 4 cases
  • packages/datadog-plugin-mongodb-core/test/core.spec.js and mongodb.spec.js
  • packages/datadog-plugin-undici/test/index.spec.js
  • packages/dd-trace/test/appsec/extended-data-collection.express.plugin.spec.js
  • packages/dd-trace/test/appsec/extended-data-collection.fastify.plugin.spec.js
  • packages/dd-trace/test/appsec/index.body-parser.plugin.spec.js
  • packages/datadog-plugin-graphql/test/index.spec.js — callback form to preserve negative key check

Merged split assertObjectContains calls into single calls:

  • packages/datadog-plugin-amqplib/test/index.spec.js — merged 3 pairs; callback form for async queue/consumerTag variables
  • packages/datadog-plugin-amqp10/test/index.spec.js — merged send (3→1) and receive (2→1)
  • packages/dd-trace/test/encode/agentless-ci-visibility.spec.js — merged two calls, inlined type/version

Moved assert.strictEqual calls into assertObjectContains blocks:

  • integration-tests/jest/jest.spec.js — 11 meta assertions
  • integration-tests/mocha/mocha.spec.js — 10 meta assertions
  • integration-tests/code-origin.spec.js
  • packages/dd-trace/test/encode/agentless-json.spec.jsmeta and metrics
  • packages/dd-trace/test/tagger.spec.js
  • packages/dd-trace/test/config/index.spec.js — merged calls, removed redundant notStrictEqual
  • packages/dd-trace/test/debugger/config.spec.js
  • packages/dd-trace/test/llmobs/span_processor.spec.js

I skipped three locations where the suggestion would require asserting undefined values (which assertObjectContains doesn't support via Object.hasOwn): openfeature/writers/exposures.spec.js and span_format.spec.js (two spots). Also skipped extended-data-collection.next.plugin.spec.js since it uses getWebSpan(traces) rather than traces[0][0], so assertFirstTraceSpan wouldn't be a straightforward replacement there.

CI is fully green. Thanks for the detailed review!

BridgeAR
BridgeAR previously approved these changes Mar 6, 2026
Copy link
Copy Markdown
Member

@BridgeAR BridgeAR left a comment

Choose a reason for hiding this comment

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

Ignoring undefined does not seem correct by claude. This nevertheless improves a lot, so LGTM

@bm1549 bm1549 force-pushed the brian.marks/eslint-prefer-assert-object-contains branch from d59f34c to e8fffff Compare March 6, 2026 20:11
@bm1549 bm1549 requested a review from BridgeAR March 6, 2026 20:33
Copy link
Copy Markdown
Member

@BridgeAR BridgeAR left a comment

Choose a reason for hiding this comment

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

LGTM, just left a few nits and it needs a rebase

Comment on lines 248 to 249
assert.ok(test)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit

Suggested change
assert.ok(test)

Comment on lines 55 to 56
const span = payload[0][0]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: we could move this into assertObjectContains(payload, [[{...}]])

That way there is also no need to check for array anymore for payload and payload[0]. Only the length check would still be needed.

Comment on lines 98 to 109
assert.deepStrictEqual(spanEvent.content.meta, {
bar: 'baz',
})
assertObjectContains(spanEvent.content.metrics, {
float: 1.23456712345,
negativefloat: -1.23456789,
bigfloat: 12345678.9,
bignegativefloat: -12345678.9,
})

assert.strictEqual(spanEvent.content.metrics.positive, 123456712345)
assert.strictEqual(spanEvent.content.metrics.negative, -123456712345)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nit: these could all be included above.

Comment on lines 366 to 368
assert.strictEqual(trace.meta['service.name'], undefined)
assert.strictEqual(trace.meta['span.type'], undefined)
assert.strictEqual(trace.meta['resource.name'], undefined)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I believe the undefined check here is actually an not existence check? Could we just change these cases to assert.ok(!Object.hasOwn(foo, 'property'))? That way the intent is clearer and all other undefined checks can be inlined into the assertObjectContains calls.

bm1549 and others added 14 commits March 10, 2026 10:59
Add an ESLint rule that detects 3+ consecutive assert.strictEqual()
calls on properties of the same root object and auto-fixes them to use
assertObjectContains(). Enable the rule as 'error' for test files and
fix all existing violations (~290 auto-fixed).

The auto-fix:
- Builds nested object literals from property access paths
- Handles both dot notation and bracket notation with string literals
- Skips groups with non-literal computed properties (variables)
- Inserts the assertObjectContains import if not already present
- Computes relative require paths to integration-tests/helpers

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Object.hasOwn only checks own properties, missing prototype getters
(e.g. Histogram.min, Histogram.max). Using 'key in Object(actual)'
supports inherited properties and getters.
When the expected value is undefined, the original code used
assert.strictEqual(obj.key, undefined) which passes whether the
property is absent or explicitly set to undefined. The key-in check
should be skipped for undefined values to preserve this behavior.
…ct-contains

Address BridgeAR's review feedback:

- Rule now skips groups where any path uses a numeric computed index
  (e.g. arr[0], payload[1]) since these indicate array indexing and
  assertObjectContains does not work correctly when numeric string keys
  are used as object properties
- Rule now skips groups where the terminal property is 'length' or 'size'
  since these collection size properties do not work reliably with
  partialDeepStrictEqual on arrays/maps/sets
- Revert helpers/index.js semantic change ('in' operator and undefined
  guard) back to original Object.hasOwn check
- Revert all 65 test files that had invalid transformations (numeric
  indices or length/size terminal properties in assertObjectContains)
- Re-run lint:fix with corrected rule; 10 files remain with valid
  assertObjectContains transformations
- Add targeted test cases covering numeric index and length/size guards

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
When an expected property value is undefined, skip the Object.hasOwn
check — this allows asserting that a property is absent or undefined
(e.g. `path: undefined`) without requiring the property to exist on
the actual object as an own property.

The 'in' operator change that BridgeAR flagged is still reverted; only
the undefined guard (needed for absent-property assertions in ~13 test
files) is restored.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
assertObjectContains with Object.hasOwn fails on CountMetric class
instances because `type` is a prototype property, not an own property.
Revert to original separate assert.strictEqual calls.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Object.hasOwn is too strict for assertObjectContains — class instances
like CountMetric, URL, and Histogram have properties on their prototypes
(e.g. type, hostname, min/max) that are not own properties.

Restore 'in' operator for the existence check, keeping the undefined
guard. BridgeAR's specific concern (length/size checks on actual
array/map/set objects falling through to the fallback) is already
addressed by the ESLint rule fix that prevents those transformations.

Also re-apply the namespaces.spec.js assertObjectContains transformation
now that the helper works correctly with prototype properties.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…int rule

- Revert integration-tests/helpers/index.js to original Object.hasOwn check
  (restores identical semantics to assert.partialDeepStrictEqual)
- Remove eslint-prefer-assert-object-contains rule and test files
- Revert eslint.config.mjs and CODEOWNERS to remove rule registration
- Fix transformed test files that were incompatible with Object.hasOwn:
  - Revert url.spec.js, namespaces.spec.js, histogram.spec.js (prototype props)
  - Replace `undefined`-valued assertObjectContains keys with separate
    assert.strictEqual() calls in config, span_format, agentless-json,
    opentelemetry/logs, opentelemetry/metrics, text_map, llmobs/sdk,
    vulnerability-analyzer, extended-data-collection (3 files),
    profiling/config, openfeature/writers/exposures

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…mpat)

On Node.js < 22, assertObjectContains falls back to Object.hasOwn which
only checks own properties, not prototype chain. Prototype getters and
constructor-assigned-but-overridden properties fail this check.

Reverted or surgically fixed assertObjectContains calls on:
- PoissonProcessSamplingFilter (currentSamplingInstant, nextSamplingInstant, samplingInstantCount)
- LLMObsSDK (enabled, _config)
- LoggerProvider / MockedExporter (OTel class instances)
- MeterProvider / MockedExporter (OTel class instances)
- ExposuresWriter (writer._interval, _timeout, _bufferLimit)
- DataStreams DataStreamsProcessor (hostname, enabled, env)
- Profiling Config (v8ProfilerBugWorkaroundEnabled, etc.)
- AppSec utils DatadogRaspAbortError (name, message, etc.)
- SpanStatsProcessor (hostname, enabled, env)

For logs.spec.js and metrics.spec.js, kept assertObjectContains on
plain objects (log2/scope.logRecords entries, resourceAttrs built via
forEach) while reverting the class instance calls.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
… instance

URL.href is a prototype getter, not an own property, so Object.hasOwn
fails on Node.js < 22 where assertObjectContains uses the fallback.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Remove redundant Object.hasOwn/notStrictEqual checks before assertObjectContains
- Merge split assertObjectContains calls on same object into single call
- Convert assertSomeTraces(traces => { const span = traces[0][0]; ... }) to assertFirstTraceSpan() in couchbase, amqplib, and vertexai plugins
- Expand assertObjectContains in jest/mocha integration tests to include more meta properties from surrounding assertions
- Move _dd.code_origin.type assertion into assertObjectContains block in code-origin spec

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- Convert assertSomeTraces + traces[0][0] to assertFirstTraceSpan in:
  graphql (×2, callback form to preserve negative check), vertexai (4th case),
  mongodb-core/core, mongodb-core/mongodb (with inlined resource variable),
  undici, extended-data-collection express/fastify, body-parser
- Include component: 'graphql' in assertObjectContains for graphql parse/validate spans
- Merge assertObjectContains calls in config/index.spec.js (logLevel/logger/middlewareTracingEnabled
  + peerServiceMapping + plugins/port/... into single call)
- Remove redundant assert.notStrictEqual(config.telemetry, undefined) in config spec
- Include commitSHA/debug/dynamicInstrumentation into assertObjectContains in debugger/config spec
- Merge two assertObjectContains in agentless-ci-visibility + include type/version from above
- Include meta/metrics in assertObjectContains in agentless-json spec
- Merge assertObjectContains(payload.tags, ...) into assertObjectContains(payload, ...) in llmobs
- Include carrier.valid in assertObjectContains in tagger spec

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- span_format.spec.js: change assert.strictEqual(x, undefined) to
  assert.ok(!Object.hasOwn(meta, key)) for non-existence checks
- agentless-ci-visibility.spec.js: merge decodedTrace.version, metadata,
  and spanEvent assertions into a single assertObjectContains(decodedTrace)
- azure-functions client.spec.js: use assertObjectContains(payload, [[{...}]])
  and remove redundant Array.isArray checks
- mocha.spec.js: include ORIGIN_KEY, TEST_CODE_OWNERS, LIBRARY_VERSION
  in failing test assertObjectContains (matching passing tests coverage)
- jest.spec.js: remove redundant assert.ok(test) (nit)
- exposures.spec.js: keep assert.strictEqual(X, undefined) - the writer
  explicitly sets type/attributes as undefined properties (Object.hasOwn
  returns true for them, so existence check would fail)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@bm1549 bm1549 force-pushed the brian.marks/eslint-prefer-assert-object-contains branch from d86ed9c to de09314 Compare March 10, 2026 15:03
The current GitLab pipeline's CI job token lacks permissions to clone
auto_inject.git (403). This empty commit triggers a fresh pipeline with
a new token.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Copy link
Copy Markdown
Member

@BridgeAR BridgeAR left a comment

Choose a reason for hiding this comment

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

LGTM, thank you for the improvement :)

I think this will end up more people using the helper because they see it in tests.

@bm1549 bm1549 merged commit db73ae0 into master Mar 10, 2026
924 of 927 checks passed
@bm1549 bm1549 deleted the brian.marks/eslint-prefer-assert-object-contains branch March 10, 2026 17:03
dd-octo-sts bot pushed a commit that referenced this pull request Mar 11, 2026
…ertObjectContains (#7693)

* feat(eslint): add eslint-prefer-assert-object-contains with auto-fix

Add an ESLint rule that detects 3+ consecutive assert.strictEqual()
calls on properties of the same root object and auto-fixes them to use
assertObjectContains(). Enable the rule as 'error' for test files and
fix all existing violations (~290 auto-fixed).

The auto-fix:
- Builds nested object literals from property access paths
- Handles both dot notation and bracket notation with string literals
- Skips groups with non-literal computed properties (variables)
- Inserts the assertObjectContains import if not already present
- Computes relative require paths to integration-tests/helpers

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: use 'in' operator instead of Object.hasOwn in assertObjectContains

Object.hasOwn only checks own properties, missing prototype getters
(e.g. Histogram.min, Histogram.max). Using 'key in Object(actual)'
supports inherited properties and getters.

* chore: add eslint-rules/ to CODEOWNERS for lang-platform-js

* fix: skip key existence check when expected value is undefined

When the expected value is undefined, the original code used
assert.strictEqual(obj.key, undefined) which passes whether the
property is absent or explicitly set to undefined. The key-in check
should be skipped for undefined values to preserve this behavior.

* fix(eslint): skip array indices and length/size in prefer-assert-object-contains

Address BridgeAR's review feedback:

- Rule now skips groups where any path uses a numeric computed index
  (e.g. arr[0], payload[1]) since these indicate array indexing and
  assertObjectContains does not work correctly when numeric string keys
  are used as object properties
- Rule now skips groups where the terminal property is 'length' or 'size'
  since these collection size properties do not work reliably with
  partialDeepStrictEqual on arrays/maps/sets
- Revert helpers/index.js semantic change ('in' operator and undefined
  guard) back to original Object.hasOwn check
- Revert all 65 test files that had invalid transformations (numeric
  indices or length/size terminal properties in assertObjectContains)
- Re-run lint:fix with corrected rule; 10 files remain with valid
  assertObjectContains transformations
- Add targeted test cases covering numeric index and length/size guards

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: restore undefined guard in assertObjectContainsImpl

When an expected property value is undefined, skip the Object.hasOwn
check — this allows asserting that a property is absent or undefined
(e.g. `path: undefined`) without requiring the property to exist on
the actual object as an own property.

The 'in' operator change that BridgeAR flagged is still reverted; only
the undefined guard (needed for absent-property assertions in ~13 test
files) is restored.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: revert namespaces.spec.js - CountMetric has prototype properties

assertObjectContains with Object.hasOwn fails on CountMetric class
instances because `type` is a prototype property, not an own property.
Revert to original separate assert.strictEqual calls.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: restore 'in' operator for class instance prototype property checks

Object.hasOwn is too strict for assertObjectContains — class instances
like CountMetric, URL, and Histogram have properties on their prototypes
(e.g. type, hostname, min/max) that are not own properties.

Restore 'in' operator for the existence check, keeping the undefined
guard. BridgeAR's specific concern (length/size checks on actual
array/map/set objects falling through to the fallback) is already
addressed by the ESLint rule fix that prevents those transformations.

Also re-apply the namespaces.spec.js assertObjectContains transformation
now that the helper works correctly with prototype properties.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: address BridgeAR feedback - revert helpers/index.js and drop ESLint rule

- Revert integration-tests/helpers/index.js to original Object.hasOwn check
  (restores identical semantics to assert.partialDeepStrictEqual)
- Remove eslint-prefer-assert-object-contains rule and test files
- Revert eslint.config.mjs and CODEOWNERS to remove rule registration
- Fix transformed test files that were incompatible with Object.hasOwn:
  - Revert url.spec.js, namespaces.spec.js, histogram.spec.js (prototype props)
  - Replace `undefined`-valued assertObjectContains keys with separate
    assert.strictEqual() calls in config, span_format, agentless-json,
    opentelemetry/logs, opentelemetry/metrics, text_map, llmobs/sdk,
    vulnerability-analyzer, extended-data-collection (3 files),
    profiling/config, openfeature/writers/exposures

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: revert assertObjectContains on class instances (Object.hasOwn compat)

On Node.js < 22, assertObjectContains falls back to Object.hasOwn which
only checks own properties, not prototype chain. Prototype getters and
constructor-assigned-but-overridden properties fail this check.

Reverted or surgically fixed assertObjectContains calls on:
- PoissonProcessSamplingFilter (currentSamplingInstant, nextSamplingInstant, samplingInstantCount)
- LLMObsSDK (enabled, _config)
- LoggerProvider / MockedExporter (OTel class instances)
- MeterProvider / MockedExporter (OTel class instances)
- ExposuresWriter (writer._interval, _timeout, _bufferLimit)
- DataStreams DataStreamsProcessor (hostname, enabled, env)
- Profiling Config (v8ProfilerBugWorkaroundEnabled, etc.)
- AppSec utils DatadogRaspAbortError (name, message, etc.)
- SpanStatsProcessor (hostname, enabled, env)

For logs.spec.js and metrics.spec.js, kept assertObjectContains on
plain objects (log2/scope.logRecords entries, resourceAttrs built via
forEach) while reverting the class instance calls.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: revert llmobs writers/base.spec.js - requestOptions.url is a URL instance

URL.href is a prototype getter, not an own property, so Object.hasOwn
fails on Node.js < 22 where assertObjectContains uses the fallback.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
@dd-octo-sts dd-octo-sts bot mentioned this pull request Mar 11, 2026
CarlesDD pushed a commit that referenced this pull request Mar 16, 2026
…ertObjectContains (#7693)

* feat(eslint): add eslint-prefer-assert-object-contains with auto-fix

Add an ESLint rule that detects 3+ consecutive assert.strictEqual()
calls on properties of the same root object and auto-fixes them to use
assertObjectContains(). Enable the rule as 'error' for test files and
fix all existing violations (~290 auto-fixed).

The auto-fix:
- Builds nested object literals from property access paths
- Handles both dot notation and bracket notation with string literals
- Skips groups with non-literal computed properties (variables)
- Inserts the assertObjectContains import if not already present
- Computes relative require paths to integration-tests/helpers

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: use 'in' operator instead of Object.hasOwn in assertObjectContains

Object.hasOwn only checks own properties, missing prototype getters
(e.g. Histogram.min, Histogram.max). Using 'key in Object(actual)'
supports inherited properties and getters.

* chore: add eslint-rules/ to CODEOWNERS for lang-platform-js

* fix: skip key existence check when expected value is undefined

When the expected value is undefined, the original code used
assert.strictEqual(obj.key, undefined) which passes whether the
property is absent or explicitly set to undefined. The key-in check
should be skipped for undefined values to preserve this behavior.

* fix(eslint): skip array indices and length/size in prefer-assert-object-contains

Address BridgeAR's review feedback:

- Rule now skips groups where any path uses a numeric computed index
  (e.g. arr[0], payload[1]) since these indicate array indexing and
  assertObjectContains does not work correctly when numeric string keys
  are used as object properties
- Rule now skips groups where the terminal property is 'length' or 'size'
  since these collection size properties do not work reliably with
  partialDeepStrictEqual on arrays/maps/sets
- Revert helpers/index.js semantic change ('in' operator and undefined
  guard) back to original Object.hasOwn check
- Revert all 65 test files that had invalid transformations (numeric
  indices or length/size terminal properties in assertObjectContains)
- Re-run lint:fix with corrected rule; 10 files remain with valid
  assertObjectContains transformations
- Add targeted test cases covering numeric index and length/size guards

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: restore undefined guard in assertObjectContainsImpl

When an expected property value is undefined, skip the Object.hasOwn
check — this allows asserting that a property is absent or undefined
(e.g. `path: undefined`) without requiring the property to exist on
the actual object as an own property.

The 'in' operator change that BridgeAR flagged is still reverted; only
the undefined guard (needed for absent-property assertions in ~13 test
files) is restored.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: revert namespaces.spec.js - CountMetric has prototype properties

assertObjectContains with Object.hasOwn fails on CountMetric class
instances because `type` is a prototype property, not an own property.
Revert to original separate assert.strictEqual calls.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: restore 'in' operator for class instance prototype property checks

Object.hasOwn is too strict for assertObjectContains — class instances
like CountMetric, URL, and Histogram have properties on their prototypes
(e.g. type, hostname, min/max) that are not own properties.

Restore 'in' operator for the existence check, keeping the undefined
guard. BridgeAR's specific concern (length/size checks on actual
array/map/set objects falling through to the fallback) is already
addressed by the ESLint rule fix that prevents those transformations.

Also re-apply the namespaces.spec.js assertObjectContains transformation
now that the helper works correctly with prototype properties.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: address BridgeAR feedback - revert helpers/index.js and drop ESLint rule

- Revert integration-tests/helpers/index.js to original Object.hasOwn check
  (restores identical semantics to assert.partialDeepStrictEqual)
- Remove eslint-prefer-assert-object-contains rule and test files
- Revert eslint.config.mjs and CODEOWNERS to remove rule registration
- Fix transformed test files that were incompatible with Object.hasOwn:
  - Revert url.spec.js, namespaces.spec.js, histogram.spec.js (prototype props)
  - Replace `undefined`-valued assertObjectContains keys with separate
    assert.strictEqual() calls in config, span_format, agentless-json,
    opentelemetry/logs, opentelemetry/metrics, text_map, llmobs/sdk,
    vulnerability-analyzer, extended-data-collection (3 files),
    profiling/config, openfeature/writers/exposures

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: revert assertObjectContains on class instances (Object.hasOwn compat)

On Node.js < 22, assertObjectContains falls back to Object.hasOwn which
only checks own properties, not prototype chain. Prototype getters and
constructor-assigned-but-overridden properties fail this check.

Reverted or surgically fixed assertObjectContains calls on:
- PoissonProcessSamplingFilter (currentSamplingInstant, nextSamplingInstant, samplingInstantCount)
- LLMObsSDK (enabled, _config)
- LoggerProvider / MockedExporter (OTel class instances)
- MeterProvider / MockedExporter (OTel class instances)
- ExposuresWriter (writer._interval, _timeout, _bufferLimit)
- DataStreams DataStreamsProcessor (hostname, enabled, env)
- Profiling Config (v8ProfilerBugWorkaroundEnabled, etc.)
- AppSec utils DatadogRaspAbortError (name, message, etc.)
- SpanStatsProcessor (hostname, enabled, env)

For logs.spec.js and metrics.spec.js, kept assertObjectContains on
plain objects (log2/scope.logRecords entries, resourceAttrs built via
forEach) while reverting the class instance calls.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

* fix: revert llmobs writers/base.spec.js - requestOptions.url is a URL instance

URL.href is a prototype getter, not an own property, so Object.hasOwn
fails on Node.js < 22 where assertObjectContains uses the fallback.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>

---------

Co-authored-by: Claude Opus 4.6 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos semver-patch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants