Skip to content

v2.1.0 - #471

Open
sjvans wants to merge 17 commits into
mainfrom
develop
Open

v2.1.0#471
sjvans wants to merge 17 commits into
mainfrom
develop

Conversation

@sjvans

@sjvans sjvans commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Release: v2.1.0

Standing release PR — accumulates everything merged into develop for the next minor. Do not squash-merge; review and merge with a team member to satisfy the main review requirement. Description kept current as PRs land on develop.

Changes so far

Follow-ups tracked (not blocking this release)


compiled of:

sjvans added 2 commits August 7, 2026 10:03
Prepare `develop` for the next minor release.

- bump version `2.0.1` → `2.1.0`
- add `## Version 2.1.0 - tbd` changelog skeleton (Added / Changed /
Fixed)

Feature PRs merging into `develop` add their bullet under this section;
the date is set when `develop` → `main` is promoted.
Add `develop` to the CI workflow's `push` and `pull_request` branch
triggers so PRs into `develop` (the new integration branch) get lint +
the test matrix.

`main` triggers unchanged. HANA remains `workflow_dispatch`-only.
@hyperspace-pr-bot

Copy link
Copy Markdown
Contributor

Summary

The following content is AI-generated and provides a summary of the pull request:


Release: v2.1.0

Chore

🔖 Standing release PR accumulating changes from develop for the next minor version bump (2.0.12.1.0). Do not squash-merge.

Changes

  • package.json: Bumped package version from 2.0.1 to 2.1.0.
  • CHANGELOG.md: Added the ## Version 2.1.0 - tbd section with placeholder entries for Added, Changed, and Fixed.
  • .github/workflows/ci.yml: Extended CI trigger branches to include develop for both push and pull_request events, so the lint and test matrix runs on the integration branch as well.

Compiled from


  • 🔄 Regenerate and Update Summary
  • ✏️ Insert as PR Description (deletes this comment)
  • 🗑️ Delete comment
PR Bot Information

Version: 1.29.18

  • Summary Prompt: Default Prompt
  • File Content Strategy: Full file content
  • Correlation ID: fd848e30-9239-11f1-802e-ae2522cdec59
  • LLM: anthropic--claude-4.6-sonnet
  • Event Trigger: pull_request.opened
  • Output Template: Default Template

@hyperspace-pr-bot hyperspace-pr-bot Bot left a comment

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 a clean, minimal release-preparation PR. The only thing to address before merging to main is trimming the empty changelog sub-sections (### Added, ### Changed, ### Fixed) that have no entries — leaving them in would result in bare headings in the published changelog.

PR Bot Information

Version: 1.29.18

  • LLM: anthropic--claude-4.6-sonnet
  • Correlation ID: fd848e30-9239-11f1-802e-ae2522cdec59
  • Event Trigger: pull_request.opened
  • File Content Strategy: Full file content

Comment thread CHANGELOG.md
Comment on lines +9 to +13
### Added

### Changed

### Fixed

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.

Best Practices: Empty changelog sub-sections will be published as-is if merged without content.

The ### Added, ### Changed, and ### Fixed headers under ## Version 2.1.0 are currently empty placeholders. Before merging into main, any sub-section that has no entries should be removed so the published changelog doesn't contain bare headings with nothing beneath them. Consider removing the unused sub-sections as part of the final merge preparation.

Suggested change
### Added
### Changed
### Fixed
### Changed

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

sjvans and others added 11 commits August 7, 2026 14:54
## Problem

Cloud SDK v4 exposes `executeHttpRequest` /
`executeHttpRequestWithOrigin` as **getter-only properties**. The plugin
patched them with plain assignment, which silently fails on a getter —
so the Cloud SDK outbound path (CAP's default when
`@sap-cloud-sdk/http-client` is installed) produced **no CLIENT span**
and no `sap.btp.destination`.

## Fix

Patch via `Object.defineProperty(cloudSDK, name, { value, writable:
true, configurable: true })` — `writable`/`configurable` keep the
exports re-patchable. Verified the wrapper now fires end-to-end.

## Tests

- `test/tracing-remote-cloudsdk.test.js` — Cloud SDK path: asserts a
`@cap-js/telemetry` CLIENT span with `sap.btp.destination`, and no
undici span.
- `test/tracing-remote-native.test.js` — native-fetch path: asserts the
span comes from `@opentelemetry/instrumentation-undici` (not `-http`)
with `http.*`/`url.*`/`server.*` attributes.

Both drive a real local HTTP call; gated on `cds.version >= 9`.
Changelog updated.

---------

Co-authored-by: sjvans <sjvans@users.noreply.github.com>
Add `target-branch: 'develop'` to both dependabot update entries (npm +
github-actions) so dependency-bump PRs open against `develop` instead of
the default branch (`main`). They then promote to `main` via the
reviewed `develop → main` PR, like every other change.
…ucture (#465)

## Added

Wraps `cds.Service.prototype.tx()` so the queue worker's two-transaction
structure (tx1: SELECT+UPDATE lock, tx2: handle+DELETE dispatch) appears
as coherent `<service> - tx` spans under the `cds.spawn - run task`
root, instead of each top-level CAP call becoming an orphan root.
Guarded so `$batch` sub-requests (active `EventContext`) are unaffected;
file-based messaging consumer delivery (bare `{}` context) still gets a
root span.

## Test infrastructure

Replaces fragile `cds.test.log()` regex assertions with a structured
in-memory span exporter (`MyInMemorySpanExporter`) and
`groupedByTrace()` / `rootSpans()` helpers. Rewrites the existing
tracing suites and adds coverage for scheduled tasks, outboxed batch
fan-out, and inbox/outbox messaging combinations.

## SQLite note

Queue-worker suites skip on sqlite (published `@sap/cds` uses a
`setTimeout` bypass, not `cds.spawn`) — verified on HANA in CI.
Follow-up #467 removes the skips once the cds queue-spawn fix ships.

Changelog updated. Targets `develop`.
Supersedes the #437 stash. Migrates the test runner jest → vitest.

## Why it's a clean win
Vitest with `pool: 'forks'` + per-file isolation tears each test file's
child process down when the file finishes — so the OTLP exporter's
lingering handles die with the child, and **the suite exits cleanly with
no `--forceExit`**. This is the same open-handle class that hangs jest
(see #472/#466). Verified: exit 0, ~8s, no hang across repeated runs.

## Config (`vitest.config.mjs`)
- `globals: true` — `describe/test/beforeEach/...` stay available, test
bodies unchanged.
- `pool: 'forks'`, `isolate: true`, `teardownTimeout: 1000` — the
clean-exit mechanism (documented inline).
- Ports the old `jest.config.js` HANA logic faithfully: default
`testTimeout: 42000`; under `CI && HANA_DRIVER` → `include` restricted
to `tracing-attributes` + `passport`, timeout ×10,
`cds_requires_telemetry_tracing` set when `HANA_PROM`. Verified the
subset selection.

## Changes
- `package.json`: `test` → `vitest run --silent`; jest removed, vitest
added. `jest.config.js` deleted.
- 8 `jest.spyOn`/`jest.fn` → `vi.spyOn`/`vi.fn`.
- 5 `beforeAll(done => …)` hooks → promise-returning (vitest treats a
hook arg as a fixture).
- `eslint.config.mjs`: test-files override declaring `vi` global. Lint
clean.
- Lockfile regenerated against public npm (0 internal-registry URLs).

## ⚠️ Behavioral note — HTTP instrumentation disabled in the test app
Under jest, OTel's `require-in-the-middle` http patching was **silently
broken** by jest's module sandbox, so incoming HTTP SERVER spans never
existed in tests (the existing `xtest` skips document this). Under
vitest (real `require`) the instrumentation works and reparents trace
trees, breaking several assertions. To keep this migration
**behavior-neutral**, `test/bookshop/package.json` now sets
`disableIncomingRequestInstrumentation` +
`disableOutgoingRequestInstrumentation` on the http instrumentation —
reproducing jest's effective environment.

Consequence: the HTTP-instrumentation path stays untested (same blind
spot as jest, now explicit config rather than an accident). **Follow-up
issue filed to enable it and assert on the real incoming spans.** The
tracing-attributes client-span assertions still pass because those spans
come via undici / cloud-sdk, not instrumentation-http.

## Coordination
Parallel PR #473 (prettier) touches `package.json` (additive) +
lockfile. Lockfile will conflict — whichever merges second rebases. #473
excludes `jest.config.js` from formatting (this PR deletes it).

## Verified
`npm run test` 53 pass / 14 skip, exit 0, ~8s, clean exit ×3 · `npm run
lint` clean · HANA subset selection confirmed.
Supersedes the #438 oxfmt spike — adopts **oxfmt** (`0.63.0`, pinned)
properly.

## Config (`.oxfmtrc.jsonc`)
Based on the #438 spike, verified against the `lib/*.js` house style:
`singleQuote`, `semi: false`, `printWidth: 120`, `tabWidth: 2`,
`trailingComma: none`, `arrowParens: avoid`. `ignorePatterns` excludes
`*.md`, `node_modules`, `package-lock.json`, `CHANGELOG.md`, and
`jest.config.js` (see coordination).

## Scripts
- `format` → `npx oxfmt` (write is oxfmt's default)
- `format:check` → `npx oxfmt --check`

No git hook / husky / lint-staged — CI `format:check` + scripts only
(the intrusive hook from the #438 spike is intentionally not carried
over).

## Commits (reviewable split)
1. `chore: add oxfmt formatter tooling` — config + scripts + devDep +
lockfile + CI step
2. `chore: apply oxfmt formatting` — repo-wide reformat (11 files,
line-wrapping only; verified non-semantic via `git diff -w`)

## eslint coexistence
`@sap/cds/eslint.config.mjs` is `recommended` +
`no-unused-vars`/`no-console` only (no stylistic rules) → no conflict.
`npm run lint` stays green.

## CI
One line added to the `lint` job in `ci.yml`: `npm run format:check`.

## Verified
`npm run format:check` ✅ (56 files) · `npm run lint` (--max-warnings=0)
✅ · `npm run test` → 53 pass / 14 skip, exit 0 · lockfile resolved from
public npm (0 internal-registry URLs).

## Coordination
Parallel PR #474 (jest→vitest) deletes `jest.config.js` — this PR
excludes it from formatting (0-line diff confirmed) so no collision.
`package.json` change here is additive (scripts + devDep); lockfile will
conflict with #474 — whichever merges second rebases.
… ConsoleMetricExporter (#479)

## What

Consolidates all outbox/metrics test-quality work into one PR (formerly
split as #479 + the stacked #480).

- **In-memory metric reader** —
`test/bookshop/lib/MyInMemoryMetricReader.js`, the metrics counterpart
to `MyInMemorySpanExporter` (#465). Mirrors production **DELTA**
temporality: SUM counters are accumulated across flushes into per-series
running totals; GAUGE datapoints keep the latest absolute value. Wired
via the `metrics-outbox`, `metrics-outbox-disabled`, and `metrics`
profiles in `.cdsrc.json`.
- **Outbox suites off console spying** — the three
`metrics-outbox*.test.js` suites drop the `console.dir` spy and fixed
`wait()` sleeps in favor of the reader + an `expectEventually()`
force-flush polling helper (fails fast if the meter provider isn't
wired). Folds in #445's polling approach.
- **ConsoleMetricExporter unit test** — new
`test/console-metric-exporter.test.js`, a pure unit test of the
exporter's formatting (db.pool table, queue table, other
single-vs-array, tenant variants, host-metrics aggregation,
shutdown→FAILED), mirroring `console-span-exporter.test.js`.
- **`metrics.test.js`** converted from scraping `cds.test.log()` output
to asserting on the in-memory reader's datapoints.

Metrics testing now mirrors the tracing side exactly: a to-console unit
test **plus** in-memory-exporter–based integration tests.

## Why

Follow-up to #465 (span test infra): eliminate console/log spying in the
metrics suite and give `ConsoleMetricExporter` direct unit coverage.

## Review addressed

- Bot review triaged: explicit `COUNTER_METRIC_NAMES` dispatch for
`isCounter`; real wall-clock debounce in the multitenant test; isolation
NOTE on the module-level singletons.
- Dropped the unused debug-log silencer in the multitenant suite (never
asserted). Kept the single-tenant `debugLog` mock — it backs a real
`unknown service` assertion.

Test-only change (no `lib/` change), so no CHANGELOG entry — consistent
with #465/#474/#476.

closes #478

Supersedes #445 and #480 (both folded in here) — I'll close them once
this merges.
…-logs, #482) (#483)

## What

Recreates dependabot #472 against `develop`, bumping the OTLP
dev-dependency group to 0.221:

- `@opentelemetry/exporter-metrics-otlp-grpc`: `^0.219` → `^0.221`
- `@opentelemetry/exporter-metrics-otlp-proto`: `^0.219` → `^0.221`
- `@opentelemetry/exporter-trace-otlp-grpc`: `^0.219` → `^0.221`
- `@opentelemetry/exporter-trace-otlp-proto`: `^0.219` → `^0.221`
- `@opentelemetry/instrumentation-host-metrics`: `^0.2.0` → `^0.4.0`
- `@opentelemetry/instrumentation-runtime-node`: `^0.32.0` → `^0.34.0`

## The pin

Adds a top-level `overrides` block pinning `@opentelemetry/sdk-logs` to
`0.219.0`:

```json
"overrides": {
  "@opentelemetry/sdk-logs": "0.219.0"
}
```

`@opentelemetry/sdk-logs` 0.221 introduces an unbounded memory leak that
OOM-crashes `test/logging.test.js` (heap climbs to ~3.8GB, crash after
~110s). Root-caused and tracked in #482. The pin keeps sdk-logs at 0.219
while everything else moves to 0.221, so `logging.test.js` behaves like
`develop` again (passes in ~5s).

Remove the pin once #482 is fixed.

## Verification

- `test/logging.test.js`: passes in ~4.9s, exits promptly (no OOM/hang)
- Full suite: 63 passed / 14 skipped, exits cleanly in ~7.4s
- Lint (`eslint . --max-warnings=0`) + format (`oxfmt --check`): clean
- Lock resolves sdk-logs to `0.219.0` while `exporter-trace-otlp-proto`
is `0.221.0` — pin is surgical

Supersedes #472.

No CHANGELOG entry (dev-deps only).

Refs #482.
…, §5) (#481)

## What

Started as removing dead test exclusions for #477 (§2 cds<9 guards, §5
HANA CI test-subset). Removing the HANA subset surfaced HANA-only
failures — including **two real production bugs** that the old 2-file
subset had been masking — so this PR also fixes those.

### Production fixes (lib/)
- **fix(tracing):** raw SQL leaked into HANA `INSERT` `prepare` span
names. The name-normalization regex used `.` which doesn't match
newlines, so HANA's multi-line `INSERT … WITH SRC AS (…)` SQL survived
in the span name. Now uses `[\s\S]` so it's stripped to operation +
table (matching SELECT); the SQL stays in `db.query.text`.
- **fix(metrics):** `*_storage_time_in_seconds` gauges were skewed by
the machine's UTC offset on HANA — HANA's `min()`/`max()` aggregates
return timezone-naive timestamps that `new Date()` parsed as local time.
Normalize to UTC before parsing.

Both carry CHANGELOG `### Fixed` entries.

### Test-suite changes (§2/§5 + HANA robustness)
- Remove all 8 dead `cds < 9` guards (§2) and the HANA CI 2-file
test-subset (§5) so the full suite runs on HANA.
- Convert queue/outbox span assertions to force-flush + poll (spans
export after fixed waits on slower HANA); filter the outbox-scan trace
primer out of the logging assertion; fix a lifecycle bug where a retry
handler fired with an undefined counter.
- **HANA CI config** (`vitest.config.mjs`, HANA-only): run files
serially (all files share one HDI container vs sqlite's per-file
in-memory DB), raise `hookTimeout`, HANA-only outbox settle in
`afterAll`, and `retry: 2` for residual shared-remote-container timing
variance. sqlite unchanged (retry:0, full parallelism).

### Skips (deviation from "no skips except passport" — conscious)
- Multitenancy tests (`tracing-mt`, `metrics-outbox-multitenant`) **skip
on HANA** with an explanatory comment: they need a bound BTP Service
Manager for MTX tenant subscription, which the single pre-provisioned
HDI container in CI doesn't provide. They still run fully on sqlite.
This means multitenancy has no HANA coverage — acknowledged; can be
revisited if the CI HANA setup gains MTX.

## Verification
- sqlite: 63 passed / 14 skipped / 0 failed.
- HANA (serial + retry:2): 64 passed / 6 skipped / 0 failed, stable
across repeated runs (the 6 skips = 2 multitenancy + 4 pre-existing
xtest/TODO in tracing.test.js).

Refs #477 (§1 queue-worker sqlite skips remain, gated on the cds
queue-spawn fix; §3 stubs pending).
# Chore: Clean Up Release Workflow

♻️ **Refactor**: Removed an outdated workaround comment from the release
workflow.

### Changes

* `.github/workflows/release.yml`: Removed the `# REVISIT: remove "npm
explore better-sqlite3 -- npm run install" with cds^10` comment that was
no longer relevant, cleaning up the release workflow configuration.

- [ ] 🔄 Regenerate and Update Summary



<details>
<summary>PR Bot Information</summary>

**Version:** `1.29.26`

- Output Template: [Default
Template](https://github.tools.sap/Code-Change-Intelligence/pr-bot/blob/main/src/services/llm/prompts/summary_default_output_template.md)
- Summary Prompt: [Default
Prompt](https://github.tools.sap/Code-Change-Intelligence/pr-bot/blob/main/src/services/llm/prompts/summary_instructions_prompt.md)
- File Content Strategy: Full file content
- Correlation ID: `8ac5e9d0-97bf-11f1-9e0b-c00ff05b9b1b`
- LLM: `anthropic--claude-4.6-sonnet`
- Event Trigger: `pull_request.opened`
</details>
…unpin sdk-logs (#482) (#489)

## Problem

Bumping the OTLP dev-deps to the `0.221` line pulled
`@opentelemetry/sdk-logs` 0.219 → 0.221, which made
`test/logging.test.js` OOM-crash (heap climbed to ~3.8 GB, ~110 s of GC
thrashing before dying). #483 shipped an interim workaround pinning
sdk-logs to `0.219.0` via a top-level `overrides` block. This PR removes
that pin and fixes the actual defect. Closes #482.

## Root cause

Two things combine:

1. **Constructor signature change (the real trigger).** In sdk-logs
0.221 the `SimpleLogRecordProcessor` and `BatchLogRecordProcessor`
constructors changed from positional `(exporter)` to an options object
`({ exporter })`. `lib/logging/index.js` still passed the exporter
positionally (both the built-in path and the custom-processor path in
`_getCustomProcessor`; the test's `MySimpleLogRecordProcessor` extends
the SDK base and forwards its args). So `options.exporter` was
`undefined`, and every emit called `core.internal._export(undefined,
…)`, which throws.

2. **Diag re-entrancy loop.** The thrown export error hits
`.catch(globalErrorHandler)` → `diag.error(...)`. Because `lib/index.js`
wires `diag.setLogger(cds.log('telemetry'), …)`, that diagnostic goes
back through `cds.log('telemetry')` → the overridden `cds.log.format` →
`logger.emit()` → export throws again → … an unbounded loop. Each hop is
a **microtask** (`processTicksAndRejections`), so the recursion is
asynchronous.

## Fix

- Construct the log processors with the `{ exporter }` options object
0.221 expects (built-in + custom-processor paths). This stops the export
from throwing, which removes the source of the diag error loop — the OOM
is gone.
- Add a re-entrancy guard (`let emitting`) around `logger.emit()` in the
`cds.log.format` interception: while inside our own `emit()` we still
run the original format work (log output unchanged) but skip
re-emitting. This is defense-in-depth against any **synchronous**
re-entry through the export/diag path; `try/finally` guarantees the flag
resets even if `emit()` throws.

## Unpin

- Removed the `@opentelemetry/sdk-logs` `overrides` pin; regenerated the
lockfile. sdk-logs now floats to `0.221.0`, matching the other OTLP
deps. This removes the #483 workaround pin.

## Verification

- `vitest run test/logging.test.js` — passes in ~1.3 s (was ~3.8 GB /
~110 s OOM crash on 0.221). The `logging.test.js` OOM was the repro; ran
it repeatedly, stable.
- Full sqlite suite: 63 passed / 14 skipped, no regressions.
- eslint `--max-warnings=0` clean; `oxfmt --check` clean.
- Lockfile: no internal-registry URLs; `npm ci` reproduces cleanly.
…er; drop tracing-attributes profile (#478) (#490)

## What

Group 1 of #478. Converts the three tracing tests that still spied on
`console.dir` to read structured `ReadableSpan` objects directly from
the in-memory span exporter:

- `test/tracing-remote-cloudsdk.test.js`
- `test/tracing-remote-native.test.js`
- `test/tracing-span-names.test.js`

Each now uses `--profile tracing-in-memory` (which wires
`MyInMemorySpanExporter` via `.cdsrc.json`) and reads over `captured`
(the module-level array of `ReadableSpan`s) instead of the `console.dir`
spy. `beforeEach(reset)` clears the buffer per test. All existing
assertions are preserved verbatim — `instrumentationScope.name` filters
(`@cap-js/telemetry`, `@opentelemetry/instrumentation-undici`), span
names, and attributes (`code.function.name`, `db.query.text`,
`sap.btp.destination`, the no-raw-SQL / no-URL-in-span-name checks).
`captured` holds full ReadableSpans with `instrumentationScope`, so the
filters just point at `captured`.

No flush/poll was needed: the spans for these single request/DB ops
appear synchronously in `captured` after the awaited call. In
`tracing-span-names.test.js`, `data.reset()` is itself traced, so the
buffer is cleared *after* reset (mirroring
`tracing-attributes.test.js`).

## Why

No more `console.dir` spying in the tracing tests. With all three files
migrated, the `[tracing-attributes]` profile in
`test/bookshop/.cdsrc.json` has no remaining consumers (verified: only
these 3 used it; `tracing-attributes.test.js` despite its name already
uses `tracing-in-memory`), so it is removed. This also resolves the
deferred "rename tracing-attributes → tracing-console" note — the
profile simply goes away.

Test-only change. No lib change, no CHANGELOG.

Refs #478 (group 1 only; group 2 done via #479, group 3 stays).
sjvans added 4 commits August 20, 2026 15:58
…ans + traceparent propagation (#475) (#491)

## What

Re-enables HTTP instrumentation in the test app (disabled by #474 to
stay behavior-neutral under jest) and asserts the incoming-request
tracing behavior it produces.

- **Test-app config** (`test/bookshop/package.json`): removed
`disableIncomingRequestInstrumentation` /
`disableOutgoingRequestInstrumentation` so both default to on; kept
`ignoreIncomingRequestHook`. Incoming HTTP requests now produce a SERVER
span (the root of each request trace) and outgoing requests produce
CLIENT spans.
- **`GET with traceparent is traced`** (was `xtest`): asserts the
incoming SERVER span adopts the W3C context from the `traceparent`
header — trace id `0af7651916cd43dd8448eb211c80319c`, parent span id
`b7ad6b7169203331`.
- **`instrumentation hooks`** (was empty `xtest`): asserts both
suppression mechanisms — the sampler's `ignoreIncomingPaths`
(`/odata/v4/admin/Authors`) and `MyIgnoreIncomingRequestHook`
(`/Books(252)`) — produce NO incoming SERVER span, while a non-ignored
path (`/Books`) does.
- **`$batch is traced`**: updated for reparenting — the single `$batch`
POST now yields ONE incoming SERVER root with both batch sub-operations
(CREATE Genres draft, READ Genres) nested beneath it (was 2 roots).
- **tracing-messaging CHECKs** (`without-outbox`, `inboxed`,
`persistent-outbox`): the producer trace is now rooted at the incoming
SERVER span (`AdminService - tx` nests under it), so the root assertion
was updated from name `AdminService - tx` to `SpanKind.SERVER` while
keeping the same nested-span assertions.

## Why

The test's HTTP client runs in-process; with outgoing instrumentation
now on, it would itself create a CLIENT span (an artificial extra root
that also overwrites a manually-set `traceparent`). A shared
`asExternalClient` helper runs client-side requests under
`suppressTracing` to model a real external, un-instrumented caller — the
outgoing CLIENT artifact is skipped and the genuine incoming SERVER span
is the trace root.

No `lib/` change (config + tests only). No CHANGELOG.

## Verification (sqlite)

- `test/tracing.test.js` — 11 passed / 2 skipped (the 2 formerly-`xtest`
now real + passing; `$batch` fixed; #477 §3 stubs remain skipped)
- `test/tracing-remote-cloudsdk.test.js` +
`test/tracing-remote-native.test.js` — pass (outgoing CLIENT
instrumentation didn't break them)
- Full suite — 65 passed / 12 skipped (was 63 / 14; the 2 newly-enabled
tests moved skipped→passed)
- lint + oxfmt clean; lockfile untouched

## HANA note

`inboxed` and `persistent-outbox` messaging tests are HANA-only (skipped
on sqlite) but share the same reparented producer-root assertion, so
they were updated too — HANA CI must be verified on this PR.

closes #475
## What

Extracts the six test helpers that had been copy-pasted across ~10 test
files into one shared module, `test/bookshop/lib/test-utils.js`:

- `flushSpans()` — unwrap the ProxyTracerProvider → delegate and
`forceFlush()`.
- `eventually(fn, { flush, timeout, interval })` — the unified
state-based poll. The span `eventually` and the metric
`expectEventually` were structurally identical, differing only in the
flush target + defaults, so they are now one function. `flush` defaults
to `flushSpans` (span callers); metric callers pass the reader's
`forceFlush` and their own timeout/interval.
- `clearOutbox(timeout = 5000)` — timeout-bounded `DELETE FROM
cds.outbox.Messages`.
- `asExternalClient(fn)` — runs `fn` under `suppressTracing`.
- `isOutboxScanTrace(g)` / `meaningful(groups)` — the outbox-scan-trace
filter, reconciled to a single `meaningful(groups)` signature.

## Why

These helpers landed independently during the HANA / instrumentation
work and drifted into ~10 near-identical copies. Centralizing them
removes the duplication (net -141 lines) and gives one place to maintain
the polling / flush / outbox-scan-filter logic.

## Notes

- **Behavior-preserving refactor** — no assertion changes, no
timeout/interval drift (each call site passes through its original
values), no predicate changes.
- Test-only. No `lib/**` change, no CHANGELOG (test-only), no dependency
change.
- `test-utils.js` stays dependency-light: it only requires
`@opentelemetry/api`, `@opentelemetry/core`, and `node:timers/promises`
— it does **not** `require('@sap/cds')` at module top (same rule that
protects the sibling exporters/reader). `clearOutbox` uses the global
`DELETE` provided by `cds.test()` at call time.

## Verification

- Full sqlite suite: **65 passed / 12 skipped** — matches the develop
baseline exactly.
- Per-file runs (metrics-outbox, metrics-outbox-multitenant, metrics,
tracing, tracing-messaging-without-outbox) all pass; the sqlite-gated
tracing files (outboxed-batch, scheduled) skip as before.
- eslint `--max-warnings=0` clean, `oxfmt --check` clean.

## HANA CI

Several affected files run **only on HANA** and share these helpers, so
they cannot be validated locally — please confirm HANA CI is green for:
- `test/tracing-scheduled.test.js`
- `test/tracing-messaging-inboxed.test.js`
- `test/tracing-messaging-persistent-outbox.test.js`
- `test/tracing-outboxed-batch.test.js`

(`test/tracing-messaging-without-outbox.test.js` does run on sqlite and
exercises the shared `meaningful`/`eventually` path — confirmed
passing.)

closes #488
…#486) (#493)

## What

Replaces the load-order-fragile `process.env.cds_requires_*` /
`process.env.cds_*` string-JSON test config with proper cds config:
`.cdsrc.json` profiles (composed with `--profile`) and `cds.test()`
args. Test-only + test-app-config change; no `lib/` change.

## Why

Setting config via `process.env.cds_*` string-JSON at module top is
load-order-sensitive: it must run before any `@sap/cds` require or it is
silently ignored (the root of the removed `delete cds.env` hack). cds
already supports the same config declaratively via `.cdsrc.json`
profiles.

Root cause of the old `// REVISIT: ... package.json wins` comments (why
some config had to be done via env): cds loads config sources
sequentially, `package.json` **after** `.cdsrc.json`, last-writer-wins.
So a `.cdsrc.json` profile could never override a key that
`package.json` set at its base. Fix: move those base defaults
(`log.cls_custom_fields`, `messaging.kind`/`file`) from
`test/bookshop/package.json` into `test/bookshop/.cdsrc.json` base, so
the `.cdsrc.json` profiles (same source) win as intended.

## Sites moved to profiles (all 8 — zero `process.env.cds_*` remain)

| Site | Was | Now |
|---|---|---|
| `logging.test.js` (cls_custom_fields, tracing.exporter:false) |
`cds_log`, `cds_requires_telemetry_tracing` | `[logging]` profile (+
base moved out of package.json) |
| `tracing.test.js` (sampler ignoreIncomingPaths) |
`cds_requires_telemetry_tracing_sampler` | new
`[sampler-ignore-authors]` profile, `--profile 'tracing-in-memory,
sampler-ignore-authors'` |
| `tracing-remote-native.test.js`, `tracing-attributes.test.js`
(native_fetch) | `cds_remote_native__fetch` | new `[native-fetch]`
profile (`remote.native_fetch: true`), `--profile 'tracing-in-memory,
native-fetch'` |
| `passport.test.js` (scheduling off) | `cds_requires_scheduling` | new
`[no-scheduling]` profile, `cds.test(dir, '--profile', 'no-scheduling')`
|
| `tracing-messaging-{inboxed,persistent-outbox,without-outbox}.test.js`
(messaging kind/file/flags) | `cds_requires_messaging` | existing
`[inboxed]`/`[persistent-outbox]`/`[without-outbox]` profiles now fully
carry it (messaging base moved out of package.json) |

## .cdsrc.json changes

- Added base `requires.messaging` (local-messaging / msg-box) and
`log.cls_custom_fields` (moved from package.json).
- `[logging]`: added `requires.telemetry.tracing.exporter: false`.
- New profiles: `[sampler-ignore-authors]`, `[native-fetch]`,
`[no-scheduling]`.

## Verification

- Full sqlite suite: **65 passed / 12 skipped** (identical to baseline,
same skip set).
- Each converted file passes individually on sqlite.
- `grep -rn "process.env.cds_" test/` → none remain.
- eslint (test/) + oxfmt clean; `grep -c int.repositories.cloud.sap
package-lock.json` = 0.

## HANA CI caveat

Profile changes are DB-agnostic, but these run on HANA and could only be
validated for config equivalence locally (config resolves identically to
the old env), not executed: `logging` (runs on both), and the HANA-only
`tracing-messaging-inboxed` + `tracing-messaging-persistent-outbox`.
HANA CI must validate them.

No lib change. No behavior change.

Closes #486
#494)

## What

Consolidates the hard-won test-suite knowledge that was scattered (and
duplicated) across `test/**` comment blocks into a single, authoritative
top-level **`TESTING.md`**, then trims the duplicated in-file comments
down to short pointers.

**New `TESTING.md`** covers:
- Running the tests (`npm test` / vitest, sqlite default; CI matrix Node
22/24 × cds 9/10; where the bookshop app lives)
- sqlite vs HANA (per-file in-memory DB vs one shared HDI container, and
the implications: serial file execution, raised timeouts, `retry: 2`,
outbox clear/settle; how the HANA path is signalled via
`TELEMETRY_TEST_HANA`; HANA is a separate `workflow_dispatch`-only
workflow, not PR CI)
- Config via `.cdsrc.json` profiles (not env) — the profile table +
compose syntax + the #486 load-order gotcha (don't reintroduce
`process.env.cds_*`)
- In-memory test infrastructure (`MyInMemorySpanExporter` /
`MyInMemoryMetricReader`, DELTA temporality, the
no-`require('@sap/cds')`-at-top rule)
- Shared test helpers (`test/utils.js` from #488) and the flush+poll
pattern
- HTTP instrumentation (#475) and why client requests are wrapped in
`asExternalClient`
- The only two sanctioned skips (#477): SAP Passport on sqlite;
multitenancy on HANA — plus the #477-tracked debt skips
- Known caveats (the `startup > NO_TELEMETRY=true` local-env artifact;
the internal-registry lockfile trap)

**Trimmed duplicated comments to pointers** (comment-only, no logic
change) in: `tracing-scheduled`, `tracing-outboxed-batch`,
`tracing-messaging-inboxed`, `tracing-messaging-persistent-outbox`,
`tracing-messaging.js`, `tracing-mt`, `metrics-outbox-multitenant`. The
repeated `cds.spawn on sqlite` skip rationale, the shared-HANA-container
outbox-bleed explanation, and the multitenancy Service-Manager note now
live in TESTING.md; each site keeps a one-line pointer. Genuinely local
rationale (timezone-bug explanation, per-test tree shapes,
`test/utils.js` own doc comments) is left untouched.

**README** gets a one-line link to TESTING.md under the contributing
section.

## Notes
- No lib/behavior change. Test-file edits are **comment-only**
(verified: every changed line in `test/**` starts with `//`). No change
to test logic, assertions, config, `vitest.config.mjs`, `.cdsrc.json`,
the exporters/reader, or the #477 gated skip logic.
- Full sqlite suite unchanged: **65 passed / 12 skipped**.
- `oxfmt --check` clean; changed files lint clean; `grep -c
int.repositories.cloud.sap package-lock.json` = 0 (lockfile untouched).
- No CHANGELOG entry (docs/test-only, not user-facing lib).

closes #487
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory leak: sdk-logs 0.221 re-enters cds.log.format interception → logging.test.js OOM

1 participant