From 45b0d9382206cf62f02536313b52645b3bb99c16 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:03:01 +0200 Subject: [PATCH 01/31] chore: prep 2.1.0 (#468) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72acf229..30c2c03f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). The format is based on [Keep a Changelog](http://keepachangelog.com/). +## Version 2.1.0 - tbd + +### Added + +### Changed + +### Fixed + ## Version 2.0.1 - 2026-07-03 ### Fixed diff --git a/package.json b/package.json index 08d883a4..a6291419 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cap-js/telemetry", - "version": "2.0.1", + "version": "2.1.0", "description": "CDS plugin providing observability features, incl. automatic OpenTelemetry instrumentation.", "repository": { "type": "git", From 3e9d3161b07089b839c1f05867c28d64f8f40e00 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:15:57 +0200 Subject: [PATCH 02/31] ci: run on pull requests to develop (#469) 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. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df995b1d..79da765d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,9 +6,9 @@ permissions: on: workflow_dispatch: push: - branches: [main] + branches: [main, develop] pull_request: - branches: [main] + branches: [main, develop] env: NPM_CONFIG_IGNORE_SCRIPTS: true From 29d002883f00151907ac18da0e96c004efa639fb Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Fri, 7 Aug 2026 14:54:10 +0200 Subject: [PATCH 03/31] fix: trace Cloud SDK outbound requests (getter-only exports) (#451) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- CHANGELOG.md | 2 + lib/tracing/cloud_sdk.js | 33 +- package-lock.json | 1276 ++++++++++++++++++++------ package.json | 1 + test/tracing-attributes.test.js | 2 +- test/tracing-remote-cloudsdk.test.js | 61 ++ test/tracing-remote-native.test.js | 57 ++ 7 files changed, 1151 insertions(+), 281 deletions(-) create mode 100644 test/tracing-remote-cloudsdk.test.js create mode 100644 test/tracing-remote-native.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 30c2c03f..3995a012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Fixed +- Cloud SDK outbound requests are traced again (patch getter-only `@sap-cloud-sdk/http-client` exports via `Object.defineProperty`) + ## Version 2.0.1 - 2026-07-03 ### Fixed diff --git a/lib/tracing/cloud_sdk.js b/lib/tracing/cloud_sdk.js index 792472a4..adc465fe 100644 --- a/lib/tracing/cloud_sdk.js +++ b/lib/tracing/cloud_sdk.js @@ -15,7 +15,10 @@ function _cloudSdkSpanName(destination, requestConfig) { return `${method}${path ? ' ' + path : ''}` } -// REVISIT: unverified! +// Instruments @sap-cloud-sdk/http-client's exports (the outbound path CAP uses by +// default when the cloud sdk is installed) to emit a CLIENT span per remote call. +// Note: cloud sdk v4 exposes executeHttpRequest(WithOrigin) as getter-only properties, +// so a plain assignment silently fails -> we must use Object.defineProperty with a value. module.exports = () => { try { require.resolve('@sap-cloud-sdk/http-client') @@ -25,26 +28,22 @@ module.exports = () => { const cloudSDK = require('@sap-cloud-sdk/http-client') const { executeHttpRequest: _execute, executeHttpRequestWithOrigin: _executeWithOrigin } = cloudSDK - cloudSDK.executeHttpRequest = wrap(_execute, { + const _executeHttpRequest = wrap(_execute, { wrapper: function executeHttpRequest(destination, requestConfig) { - return trace( - _cloudSdkSpanName(destination, requestConfig), - _execute, - this, - arguments, - { kind: SpanKind.CLIENT, outbound: destination.name } - ) + return trace(_cloudSdkSpanName(destination, requestConfig), _execute, this, arguments, { + kind: SpanKind.CLIENT, + outbound: destination.name + }) } }) - cloudSDK.executeHttpRequestWithOrigin = wrap(_executeWithOrigin, { + Object.defineProperty(cloudSDK, 'executeHttpRequest', { value: _executeHttpRequest, writable: true, configurable: true }) + const _executeHttpRequestWithOrigin = wrap(_executeWithOrigin, { wrapper: function executeHttpRequestWithOrigin(destination, requestConfig) { - return trace( - _cloudSdkSpanName(destination, requestConfig), - _executeWithOrigin, - this, - arguments, - { kind: SpanKind.CLIENT, outbound: destination.name } - ) + return trace(_cloudSdkSpanName(destination, requestConfig), _executeWithOrigin, this, arguments, { + kind: SpanKind.CLIENT, + outbound: destination.name + }) } }) + Object.defineProperty(cloudSDK, 'executeHttpRequestWithOrigin', { value: _executeHttpRequestWithOrigin, writable: true, configurable: true }) } diff --git a/package-lock.json b/package-lock.json index 79d0248e..434f7a0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { "name": "@cap-js/telemetry", - "version": "2.0.1", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "dev": true, "packages": { "": { "name": "@cap-js/telemetry", - "version": "2.0.1", + "version": "2.1.0", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -33,6 +33,7 @@ "@opentelemetry/exporter-trace-otlp-proto": "^0.219", "@opentelemetry/instrumentation-host-metrics": "^0.2.0", "@opentelemetry/instrumentation-runtime-node": "^0.32.0", + "@sap-cloud-sdk/http-client": "^4", "@sap/cds-mtxs": "^4", "axios": "^1.6.7", "eslint": "^10", @@ -600,6 +601,28 @@ "resolved": "", "link": true }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -635,9 +658,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -692,9 +715,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", - "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -879,91 +902,6 @@ "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -1481,10 +1419,9 @@ } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", - "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", - "dev": true, + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", + "integrity": "sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -1912,6 +1849,19 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-host-metrics/node_modules/@opentelemetry/api-logs": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/instrumentation-host-metrics/node_modules/@opentelemetry/instrumentation": { "version": "0.219.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.219.0.tgz", @@ -1966,6 +1916,19 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/api-logs": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/instrumentation": { "version": "0.219.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.219.0.tgz", @@ -2001,18 +1964,6 @@ "@opentelemetry/api": "^1.7.0" } }, - "node_modules/@opentelemetry/instrumentation/node_modules/@opentelemetry/api-logs": { - "version": "0.221.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", - "integrity": "sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.219.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.219.0.tgz", @@ -2102,6 +2053,19 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/api-logs": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", @@ -2205,6 +2169,19 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/api-logs": { + "version": "0.219.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", + "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", @@ -2399,16 +2376,74 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@sap-cloud-sdk/connectivity": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/connectivity/-/connectivity-4.8.0.tgz", + "integrity": "sha512-y8H0AKgDecm7+A8wxx+J1oxJTzYEs+4Hy7ghfrBD57e9MdRRFI5FryYqokZbDcMjEnsbvZ+giShm/A2UXmeHyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sap-cloud-sdk/resilience": "^4.8.0", + "@sap-cloud-sdk/util": "^4.8.0", + "@sap/xsenv": "^6.2.0", + "@sap/xssec": "^4.13.0", + "async-retry": "^1.3.3", + "axios": "^1.15.0", + "jks-js": "^1.1.6", + "jsonwebtoken": "^9.0.3", + "safe-stable-stringify": "^2.5.0" + } + }, + "node_modules/@sap-cloud-sdk/http-client": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/http-client/-/http-client-4.8.0.tgz", + "integrity": "sha512-ruQd7d0nObshm0fNkhUE1fp1qxevEYxblhM/X0rG1goN9r4nQiT6LgZUG5s+QeR1R2LdIHBXtsIycBfz/4kOog==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sap-cloud-sdk/connectivity": "^4.8.0", + "@sap-cloud-sdk/resilience": "^4.8.0", + "@sap-cloud-sdk/util": "^4.8.0", + "axios": "^1.15.0" + } + }, + "node_modules/@sap-cloud-sdk/resilience": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/resilience/-/resilience-4.8.0.tgz", + "integrity": "sha512-/XclOtUHhdN39acKH1otJdMILAm+HJkQ6wjpUvqBx7RNiQ1GQwe34qSqy8wGDJr5MbcxuhENYGWJRLE4P4OHqw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sap-cloud-sdk/util": "^4.8.0", + "async-retry": "^1.3.3", + "axios": "^1.15.0", + "opossum": "^10.0.0" + } + }, + "node_modules/@sap-cloud-sdk/util": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/util/-/util-4.8.0.tgz", + "integrity": "sha512-wLWxgxYwAL1N5dh+m8XGZTZ2vzooDHXKXrkay3rBpYSFHhZjsD2V37ezbAhbBKlFIELZA03C+Eb9o4YgHpvTKg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "axios": "^1.15.0", + "logform": "^2.7.0", + "voca": "^1.4.1", + "winston": "^3.19.0", + "winston-transport": "^4.9.0" + } + }, "node_modules/@sap/cds": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-10.0.3.tgz", - "integrity": "sha512-S9q8vcJXzIsO4KC49sb9JLIhY/k0MJTZcgEOzmhvoBW/lWLLR79Oci0xmyQqSxJwwzjavR9mW8I4wbMJQQMVAA==", + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-10.0.5.tgz", + "integrity": "sha512-H5vTMVsznF4q24OVYkiWcReQezLOzKlVC3LBiHlrkg1D9x24V/OGWKCrCG+yd77hyIsrA2l2yxWE3RU4CElgmg==", "license": "SEE LICENSE IN LICENSE", "peer": true, "dependencies": { @@ -2435,9 +2470,9 @@ } }, "node_modules/@sap/cds-compiler": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-7.0.1.tgz", - "integrity": "sha512-Qwk9jitwSSwPB9FTB/Q6gYPxFsJxswhfsO9Ux77HTxjIhse8O/Hlq2XzkpVSTyaAVr8Lo7wLfpytHwKQScZTwQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-7.0.3.tgz", + "integrity": "sha512-scgBPK0TcobT0tXIQOEBTeVuGaxWthKeIQjhBoeLWcQXwVC3s61U3GFdbW56h01pXFwwUUSsxjzZN7all5lRyg==", "license": "SEE LICENSE IN LICENSE", "peer": true, "bin": { @@ -2546,6 +2581,20 @@ "node": "^20.0.0 || ^22.0.0 || ^24.0.0" } }, + "node_modules/@sap/xssec": { + "version": "4.13.3", + "resolved": "https://registry.npmjs.org/@sap/xssec/-/xssec-4.13.3.tgz", + "integrity": "sha512-op5wFTpJGJFdwFcEFRDX30yKbB2Kknk+LJqXDcrHyPNLqvSqerTWXFdFnylhiIhoAdGFSccC6NimUhqFxIrUsA==", + "dev": true, + "license": "SAP DEVELOPER LICENSE AGREEMENT", + "dependencies": { + "debug": "^4.4.3", + "jwt-decode": "^4" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@sinclair/typebox": { "version": "0.34.52", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", @@ -2573,6 +2622,17 @@ "@sinonjs/commons": "^3.0.1" } }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -2678,9 +2738,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz", - "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==", + "version": "26.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", + "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", "dev": true, "license": "MIT", "dependencies": { @@ -2694,6 +2754,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -3076,9 +3143,10 @@ } }, "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "devOptional": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -3087,15 +3155,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -3153,13 +3212,16 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { @@ -3192,6 +3254,19 @@ "node": ">= 8" } }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -3202,6 +3277,16 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", @@ -3219,6 +3304,16 @@ "dev": true, "license": "MIT" }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3227,14 +3322,14 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", - "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", "dev": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", + "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } @@ -3401,16 +3496,16 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/braces": { @@ -3470,6 +3565,13 @@ "node-int64": "^0.4.0" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3538,9 +3640,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001807", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001807.tgz", - "integrity": "sha512-daRXJ9EB/rdRgu7kV+TTl1YUKtlsMWblPl2sLnpg9DZae16QCegol6A1SmCE31Lm9mXC1sRWGt/krouH+/dl7Q==", + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", "dev": true, "funding": [ { @@ -3658,20 +3760,83 @@ "node": ">=12" } }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.8" + "node": ">=8" } }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", "dev": true, "license": "MIT", "engines": { @@ -3686,6 +3851,20 @@ "dev": true, "license": "MIT" }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3706,6 +3885,52 @@ "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3912,6 +4137,16 @@ "dev": true, "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3920,9 +4155,9 @@ "peer": true }, "node_modules/electron-to-chromium": { - "version": "1.5.402", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.402.tgz", - "integrity": "sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==", + "version": "1.5.400", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.400.tgz", + "integrity": "sha512-96EWDNjM59SYflgeV5Ylsf4EMiq1a25YjCnJH7cxn/AF2H3pILRweaUnoLax0yKHWdpOzY6JKEu45e8irqZIHA==", "dev": true, "license": "ISC" }, @@ -3940,9 +4175,16 @@ } }, "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "dev": true, "license": "MIT" }, @@ -3984,6 +4226,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", @@ -4043,9 +4291,9 @@ } }, "node_modules/eslint": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.6.0.tgz", - "integrity": "sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==", + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", "devOptional": true, "license": "MIT", "workspaces": [ @@ -4055,7 +4303,7 @@ "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.6.0", + "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", "@eslint/plugin-kit": "^0.7.2", "@humanfs/node": "^0.16.6", @@ -4079,7 +4327,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", + "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -4365,6 +4613,13 @@ "bser": "2.1.1" } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "dev": true, + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -4445,12 +4700,19 @@ } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "devOptional": true, "license": "ISC" }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "dev": true, + "license": "MIT" + }, "node_modules/follow-redirects": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", @@ -4878,9 +5140,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "license": "MIT", "peer": true, "dependencies": { @@ -4905,14 +5167,13 @@ } }, "node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.3.tgz", + "integrity": "sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { @@ -5412,19 +5673,6 @@ "fsevents": "^2.3.3" } }, - "node_modules/jest-haste-map/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-leak-detector": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", @@ -5477,19 +5725,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-message-util/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-mock": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", @@ -5699,19 +5934,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-util/node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/jest-validate": { "version": "30.4.1", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", @@ -5796,6 +6018,18 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jks-js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/jks-js/-/jks-js-1.1.7.tgz", + "integrity": "sha512-BeiDRKsAi1NwEwgx2JB/9/0tar5BNGIv+foGm1G5GgiyR35s/iUnfd/BWqYd16mLDD8qTaAVBrIcOOuVqXJZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-forge": "^1.4.0", + "node-int64": "^0.4.0", + "node-rsa": "^1.1.1" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5871,6 +6105,75 @@ "node": ">=6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5881,6 +6184,13 @@ "json-buffer": "3.0.1" } }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "dev": true, + "license": "MIT" + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -5935,6 +6245,73 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -6001,13 +6378,17 @@ } }, "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", "license": "MIT", "peer": true, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/merge-descriptors": { @@ -6044,13 +6425,26 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "peer": true, - "engines": { + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "peer": true, + "engines": { "node": ">= 0.6" } }, @@ -6082,13 +6476,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "devOptional": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -6182,6 +6576,16 @@ "node": ">= 8.0.0" } }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6190,15 +6594,25 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.53", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", - "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "version": "2.0.52", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.52.tgz", + "integrity": "sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==", "dev": true, "license": "MIT", "engines": { "node": ">=18" } }, + "node_modules/node-rsa": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", + "integrity": "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "^0.2.4" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -6257,6 +6671,16 @@ "wrappy": "1" } }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -6273,6 +6697,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opossum": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/opossum/-/opossum-10.0.0.tgz", + "integrity": "sha512-sghtqL8Usj+et06Zui0nyn0R6FFsl7cyuoU+d7MctYU0nbS7Htzjleh+tWohFqk1Rp1srrFwbFa8Vxd0O64w6A==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^26 || ^24 || ^22" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -6442,13 +6876,13 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -6573,9 +7007,9 @@ } }, "node_modules/protobufjs": { - "version": "7.6.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", - "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", @@ -6710,6 +7144,21 @@ "dev": true, "license": "MIT" }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6756,6 +7205,16 @@ "node": ">=8" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/router": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", @@ -6773,12 +7232,42 @@ "node": ">= 18" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", @@ -6994,6 +7483,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -7027,6 +7526,16 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -7041,21 +7550,47 @@ "node": ">=10" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", @@ -7072,7 +7607,24 @@ "node": ">=8" } }, - "node_modules/strip-ansi": { + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", @@ -7085,6 +7637,22 @@ "node": ">=8" } }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", @@ -7099,6 +7667,16 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -7162,9 +7740,9 @@ } }, "node_modules/systeminformation": { - "version": "5.31.11", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.11.tgz", - "integrity": "sha512-I6O7iaUj23AXRgCPDDnvi3xHvdOLp4+1YMbF+X194lJwY1NeWojgHJPhslVKcmTtrLTguRk3QJK+xEdTiI3P0w==", + "version": "5.33.1", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.1.tgz", + "integrity": "sha512-DEN6ICHk3Tk0Uf/hrAHh7xlt7iL5CJFBtPZinA0H62DrGG/KPKqq/Nzj6lCXPS4Ay/sf/14zNnk9LpqKzBIc+w==", "dev": true, "license": "MIT", "os": [ @@ -7181,7 +7759,7 @@ "systeminformation": "lib/cli.js" }, "engines": { - "node": ">=8.0.0" + "node": ">=10.0.0" }, "funding": { "type": "Buy me a coffee", @@ -7256,6 +7834,13 @@ "node": "*" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "dev": true, + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -7286,6 +7871,16 @@ "node": ">=0.6" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -7473,6 +8068,13 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -7513,6 +8115,13 @@ "node": ">=0.6.0" } }, + "node_modules/voca": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/voca/-/voca-1.4.1.tgz", + "integrity": "sha512-NJC/BzESaHT1p4B5k4JykxedeltmNbau4cummStd4RjFojgq/kLew5TzYge9N2geeWyI2w8T30wUET5v+F7ZHA==", + "dev": true, + "license": "MIT" + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -7539,6 +8148,44 @@ "node": ">= 8" } }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7557,18 +8204,18 @@ "license": "MIT" }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -7593,6 +8240,64 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -7675,6 +8380,51 @@ "node": ">=12" } }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index a6291419..d9c2d5a2 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@opentelemetry/exporter-trace-otlp-proto": "^0.219", "@opentelemetry/instrumentation-host-metrics": "^0.2.0", "@opentelemetry/instrumentation-runtime-node": "^0.32.0", + "@sap-cloud-sdk/http-client": "^4", "@sap/cds-mtxs": "^4", "axios": "^1.6.7", "eslint": "^10", diff --git a/test/tracing-attributes.test.js b/test/tracing-attributes.test.js index 3b15627c..ac52e303 100644 --- a/test/tracing-attributes.test.js +++ b/test/tracing-attributes.test.js @@ -1,4 +1,4 @@ -// REVISIT: use native fetch in cds oq +// Use native fetch in CDS OQ so @opentelemetry/instrumentation-undici can see outbound calls process.env.cds_remote_native__fetch = 'true' const cds = require('@sap/cds') diff --git a/test/tracing-remote-cloudsdk.test.js b/test/tracing-remote-cloudsdk.test.js new file mode 100644 index 00000000..976ee348 --- /dev/null +++ b/test/tracing-remote-cloudsdk.test.js @@ -0,0 +1,61 @@ +const cds = require('@sap/cds') +const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-attributes') +const http = require('http') + +// Cloud SDK path: with @sap-cloud-sdk/http-client installed (as in the bookshop) and +// cds.env.remote.native_fetch NOT set, CAP routes outbound remote calls through +// getCloudSdk().executeHttpRequestWithOrigin(...). lib/tracing/cloud_sdk.js wraps that +// export so the outbound call produces a @cap-js/telemetry CLIENT span carrying +// the sap.btp.destination attribute. +describe('tracing remote via cloud sdk', () => { + // cloud-sdk resilience module resolution has issues on cds 8 + if (Number(cds.version.split('.')[0]) < 9) return + + const log = jest.spyOn(console, 'dir') + beforeEach(log.mockClear) + + const getSpans = () => log.mock.calls.map(c => c[0]).filter(Boolean) + const getCapSpans = () => getSpans().filter(s => s.instrumentationScope?.name === '@cap-js/telemetry') + + let server, port + + beforeAll(done => { + server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ value: [] })) + }) + server.listen(0, () => { + port = server.address().port + done() + }) + }) + + afterAll(() => new Promise(resolve => server.close(resolve))) + + test('outbound call is traced by the cloud_sdk wrapper with sap.btp.destination', async () => { + // a named destination object -> destination.name flows into the CLIENT span attribute + cds.env.requires.TestRemote = { + kind: 'odata', + credentials: { destination: { name: 'my-destination', url: `http://localhost:${port}` } } + } + const remote = await cds.connect.to('TestRemote') + + // no mock handler - let it make the actual HTTP call via the cloud sdk + await remote.send({ method: 'GET', path: '/test' }) + + // the cloud sdk path must not go through native fetch / undici + expect(cds.env.remote?.native_fetch).not.to.equal(true) + + // the outbound span comes from our tracer (not from undici) ... + const clientSpan = getCapSpans().find(s => s.attributes?.['code.function.name'] === 'executeHttpRequestWithOrigin') + expect(clientSpan, 'cloud_sdk wrapper did not produce a CLIENT span').to.exist + // ... is a CLIENT span (kind 2) ... + expect(clientSpan.kind).to.equal(2) + // ... and carries the destination name + expect(clientSpan.attributes['sap.btp.destination']).to.equal('my-destination') + + // no undici span for this call (cloud sdk path is used, not native fetch) + const undiciSpans = getSpans().filter(s => s.instrumentationScope?.name === '@opentelemetry/instrumentation-undici') + expect(undiciSpans.length).to.equal(0) + }) +}) diff --git a/test/tracing-remote-native.test.js b/test/tracing-remote-native.test.js new file mode 100644 index 00000000..5a6947cc --- /dev/null +++ b/test/tracing-remote-native.test.js @@ -0,0 +1,57 @@ +// Force CAP to use native fetch for outbound remote calls (instead of the cloud sdk). +// This must be set before @sap/cds is loaded, so it lives at the very top of the file. +process.env.cds_remote_native__fetch = 'true' + +const cds = require('@sap/cds') +const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-attributes') +const http = require('http') + +// Native fetch path: when cds.env.remote.native_fetch === true (or no cloud sdk is +// installed), CAP routes outbound remote calls through native fetch, which is +// instrumented by @opentelemetry/instrumentation-undici. The outbound span therefore +// comes from that instrumentation scope (NOT @opentelemetry/instrumentation-http, and +// NOT our cloud_sdk wrapper) and carries the standard http.* / url.* / server.* attributes. +describe('tracing remote via native fetch', () => { + // cloud-sdk resilience module resolution has issues on cds 8 + if (Number(cds.version.split('.')[0]) < 9) return + + const log = jest.spyOn(console, 'dir') + beforeEach(log.mockClear) + + const getSpans = () => log.mock.calls.map(c => c[0]).filter(Boolean) + + let server, port + + beforeAll(done => { + server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ value: [] })) + }) + server.listen(0, () => { + port = server.address().port + done() + }) + }) + + afterAll(() => new Promise(resolve => server.close(resolve))) + + test('outbound call is traced by @opentelemetry/instrumentation-undici', async () => { + expect(cds.env.remote?.native_fetch).to.equal(true) + + cds.env.requires.TestRemote = { kind: 'odata', credentials: { url: `http://localhost:${port}` } } + const remote = await cds.connect.to('TestRemote') + + // no mock handler - let it make the actual HTTP call via native fetch + await remote.send({ method: 'GET', path: '/test' }) + + const undiciSpan = getSpans().find( + s => s.instrumentationScope?.name === '@opentelemetry/instrumentation-undici' + ) + expect(undiciSpan, 'no span from @opentelemetry/instrumentation-undici').to.exist + expect(undiciSpan.attributes['http.request.method']).to.equal('GET') + expect(undiciSpan.attributes['http.response.status_code']).to.equal(200) + expect(undiciSpan.attributes['url.full']).to.equal(`http://localhost:${port}/test`) + expect(undiciSpan.attributes['server.address']).to.equal('localhost') + expect(undiciSpan.attributes['server.port']).to.equal(port) + }) +}) From b439634e103ea158ffb136f6b619a90a0b491869 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:01:55 +0200 Subject: [PATCH 04/31] ci: target develop for dependabot updates (#470) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/dependabot.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dab5e766..612ef038 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,7 @@ version: 2 updates: - package-ecosystem: 'npm' # See documentation for possible values directory: '/' # Location of package manifests + target-branch: 'develop' versioning-strategy: increase-if-necessary schedule: interval: 'weekly' @@ -26,6 +27,7 @@ updates: - package-ecosystem: 'github-actions' directory: '/' + target-branch: 'develop' schedule: interval: 'weekly' cooldown: From 7790a99603cc960c909b5cfb1945f7d9da3e492d Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:38:52 +0200 Subject: [PATCH 05/31] feat: trace queue worker transactions + structured span test infrastructure (#465) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 ` - 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`. --- CHANGELOG.md | 2 + lib/tracing/cds.js | 19 ++ test/bookshop/.cdsrc.json | 21 ++ test/bookshop/lib/MyInMemorySpanExporter.js | 59 +++++ test/bookshop/srv/admin-service.cds | 3 + test/bookshop/srv/admin-service.js | 24 ++ test/console-span-exporter.test.js | 243 ++++++++++++++++++ test/tracing-attributes.test.js | 79 +++--- test/tracing-messaging-inboxed.test.js | 66 +++++ ...racing-messaging-persistent-outbox.test.js | 104 +++++++- test/tracing-messaging-without-outbox.test.js | 39 ++- test/tracing-messaging.js | 22 +- test/tracing-mt.test.js | 20 +- test/tracing-outboxed-batch.test.js | 72 ++++++ test/tracing-scheduled.test.js | 73 ++++++ test/tracing.test.js | 71 +++-- 16 files changed, 818 insertions(+), 99 deletions(-) create mode 100644 test/bookshop/lib/MyInMemorySpanExporter.js create mode 100644 test/console-span-exporter.test.js create mode 100644 test/tracing-messaging-inboxed.test.js create mode 100644 test/tracing-outboxed-batch.test.js create mode 100644 test/tracing-scheduled.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 3995a012..39154001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Added +- Queue worker transactions are traced as coherent ` - tx` spans under the `cds.spawn - run task` root, instead of orphaned per-call spans + ### Changed ### Fixed diff --git a/lib/tracing/cds.js b/lib/tracing/cds.js index c6766273..07445ef0 100644 --- a/lib/tracing/cds.js +++ b/lib/tracing/cds.js @@ -46,6 +46,25 @@ module.exports = () => { } }) + // Wrap `srv.tx(fn)` so the queue worker's two transactions (SELECT+UPDATE lock tx, + // then handle+DELETE dispatch tx) appear as child spans of the `cds.spawn - run task` + // root instead of each top-level CAP call inside them becoming an orphan root. + // Only wraps when there is no current `srv.context` (i.e. not already inside a request). + const _tx_proto = cds.Service.prototype.tx + cds.Service.prototype.tx = wrap(_tx_proto, { + wrapper: function tx() { + const fnIdx = typeof arguments[0] === 'function' ? 0 : typeof arguments[1] === 'function' ? 1 : -1 + if (fnIdx < 0) return _tx_proto.apply(this, arguments) + // Skip if this service is already handling a request (has an active EventContext), + // or if this is a nested .tx() call (cds.Service.tx is a no-op when already in tx). + // Do NOT skip for a bare {} context set by processInboundMsg — that's the entry point + // for file-based messaging consumer delivery and should get a root span. + if (this.context instanceof cds.EventContext) return _tx_proto.apply(this, arguments) + const name = `${this.name || 'cds'} - tx` + return trace(name, _tx_proto, this, arguments, {}) + } + }) + const { spawn: _spawn } = cds cds.spawn = wrap(_spawn, { wrapper: function spawn() { diff --git a/test/bookshop/.cdsrc.json b/test/bookshop/.cdsrc.json index 1dc257b9..6571cbe7 100644 --- a/test/bookshop/.cdsrc.json +++ b/test/bookshop/.cdsrc.json @@ -82,6 +82,18 @@ } } }, + "[tracing-in-memory]": { + "requires": { + "telemetry": { + "tracing": { + "exporter": { + "module": "./lib/MyInMemorySpanExporter.js", + "class": "MyInMemorySpanExporter" + } + } + } + } + }, "[persistent-outbox]": { "requires": { "messaging": { @@ -90,6 +102,15 @@ } } }, + "[inboxed]": { + "requires": { + "messaging": { + "kind": "file-based-messaging", + "file": "../inboxed", + "inboxed": true + } + } + }, "[without-outbox]": { "requires": { "messaging": { diff --git a/test/bookshop/lib/MyInMemorySpanExporter.js b/test/bookshop/lib/MyInMemorySpanExporter.js new file mode 100644 index 00000000..9c7e4bdc --- /dev/null +++ b/test/bookshop/lib/MyInMemorySpanExporter.js @@ -0,0 +1,59 @@ +// In-memory span exporter for tests. Spans are accumulated in a module-level array that +// tests can import directly via `require('./lib/MyInMemorySpanExporter').captured`. +// Wired into the tracer provider via .cdsrc.json profile config (no provider-poking from tests). + +const { ExportResultCode } = require('@opentelemetry/core') + +const captured = [] + +class MyInMemorySpanExporter { + export(spans, resultCallback) { + captured.push(...spans) + resultCallback({ code: ExportResultCode.SUCCESS }) + } + + shutdown() { + return Promise.resolve() + } + + forceFlush() { + return Promise.resolve() + } +} + +// Returns the captured spans grouped by traceId, each group is a hierarchy: +// { traceId, root, all, byParent } +// `root` is the span with no parent inside the group (the visible root for the exporter's +// "elapsed times:" primer logic — i.e. spans whose parentSpanId is not present in this group). +function groupedByTrace() { + const byTrace = new Map() + for (const s of captured) { + const tid = s.spanContext().traceId + if (!byTrace.has(tid)) byTrace.set(tid, []) + byTrace.get(tid).push(s) + } + + return [...byTrace.entries()].map(([traceId, all]) => { + const ids = new Set(all.map(s => s.spanContext().spanId)) + const roots = all.filter(s => !s.parentSpanContext?.spanId || !ids.has(s.parentSpanContext.spanId)) + const byParent = new Map() + for (const s of all) { + const pid = s.parentSpanContext?.spanId + if (!byParent.has(pid)) byParent.set(pid, []) + byParent.get(pid).push(s) + } + return { traceId, root: roots[0], roots, all, byParent } + }) +} + +// Returns just the visible "root" spans across all captured traces. These correspond 1:1 to +// "elapsed times:" primers our ConsoleSpanExporter would emit for the same data. +function rootSpans() { + return groupedByTrace().flatMap(g => g.roots) +} + +function reset() { + captured.length = 0 +} + +module.exports = { MyInMemorySpanExporter, captured, groupedByTrace, rootSpans, reset } diff --git a/test/bookshop/srv/admin-service.cds b/test/bookshop/srv/admin-service.cds index 4ab230d8..3d8838ec 100644 --- a/test/bookshop/srv/admin-service.cds +++ b/test/bookshop/srv/admin-service.cds @@ -7,6 +7,9 @@ service AdminService @(requires: 'admin') { action test_spawn(); action test_emit(); + action test_outboxed_send(); + action test_outboxed_send_batch(); + action test_scheduled(); event foo { bar : String; diff --git a/test/bookshop/srv/admin-service.js b/test/bookshop/srv/admin-service.js index fb1cdf6c..9cb186fe 100644 --- a/test/bookshop/srv/admin-service.js +++ b/test/bookshop/srv/admin-service.js @@ -32,6 +32,30 @@ module.exports = class AdminService extends cds.ApplicationService { await messaging.emit('foo', { bar: 'baz' }) }) + // test_outboxed_send: writes a task to the persistent outbox addressed to ExternalServiceOne, + // whose handler the test installs. Exercises the queue-worker path (scan, lock, dispatch). + this.on('test_outboxed_send', async () => { + const externalOne = await cds.connect.to('ExternalServiceOne') + await cds.queued(externalOne).send('call', {}) + }) + + // test_outboxed_send_batch: writes multiple tasks to the persistent outbox to exercise chunkSize > 1 fan-out. + this.on('test_outboxed_send_batch', async () => { + const externalOne = await cds.connect.to('ExternalServiceOne') + const queued = cds.queued(externalOne) + await Promise.all([ + queued.send('call', {}), + queued.send('call', {}), + queued.send('call', {}) + ]) + }) + + // test_scheduled: schedules a one-shot task to fire after a short delay. + this.on('test_scheduled', async () => { + const externalOne = await cds.connect.to('ExternalServiceOne') + await cds.queued(externalOne).schedule('call', {}).after(10) + }) + return super.init() } } diff --git a/test/console-span-exporter.test.js b/test/console-span-exporter.test.js new file mode 100644 index 00000000..4c83e4e8 --- /dev/null +++ b/test/console-span-exporter.test.js @@ -0,0 +1,243 @@ +// Unit tests for ConsoleSpanExporter — verifies the user-friendly hierarchy formatting +// (the "elapsed times:" primer + indented child lines) by feeding the exporter crafted +// ReadableSpan-shaped fixtures and inspecting the formatted string passed to LOG.info. +// +// This is a pure unit test: no cds.test server, no real OTel SDK, no console spying. + +const cds = require('@sap/cds') + +// Hook LOG.info BEFORE requiring the exporter so the exporter's module-level +// `cds.log('telemetry')` resolves to a logger whose .info we control. +const infoCalls = [] +const telemetryLog = cds.log('telemetry') +const originalInfo = telemetryLog.info +telemetryLog.info = (...args) => infoCalls.push(args) + +const ConsoleSpanExporter = require('../lib/exporter/ConsoleSpanExporter') + +afterAll(() => { + telemetryLog.info = originalInfo +}) + +beforeEach(() => { + infoCalls.length = 0 +}) + +// --- helpers --------------------------------------------------------------- + +// Builds a minimal ReadableSpan-shaped object. Times are in OTel HrTime = [seconds, nanos]. +function span({ name, traceId, spanId, parentSpanId, startMs = 0, durationMs = 0, attributes = {} }) { + const startHr = msToHr(startMs) + const durationHr = msToHr(durationMs) + const endHr = msToHr(startMs + durationMs) + return { + name, + kind: 0, + spanContext: () => ({ traceId, spanId }), + parentSpanContext: parentSpanId ? { traceId, spanId: parentSpanId } : undefined, + startTime: startHr, + endTime: endHr, + duration: durationHr, + status: { code: 0 }, + attributes, + links: [], + events: [], + ended: true, + resource: { attributes: {} }, + instrumentationScope: { name: 'test' }, + droppedAttributesCount: 0, + droppedEventsCount: 0, + droppedLinksCount: 0 + } +} + +function msToHr(ms) { + const seconds = Math.floor(ms / 1000) + const nanos = Math.round((ms - seconds * 1000) * 1e6) + return [seconds, nanos] +} + +// Drives the exporter and returns the lines logged across all root primers. +function exportAndCapture(spans) { + const exporter = new ConsoleSpanExporter() + let result + exporter.export(spans, r => (result = r)) + expect(result).to.deep.equal({ code: 0 /* ExportResultCode.SUCCESS */ }) + return infoCalls.map(args => args[0]) +} + +// --- assertions ------------------------------------------------------------ + +const { expect } = require('@cap-js/cds-test') + +describe('ConsoleSpanExporter', () => { + describe('hierarchy formatting', () => { + it('emits a single "elapsed times:" primer per root and nests children by depth', () => { + // Tree shape: + // root (0 → 10 ms) + // childA (1 → 4 ms) + // grandchild (2 → 3 ms) + // childB (5 → 9 ms) + const TRACE = 'a'.repeat(32) + const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 10 }) + const childA = span({ name: 'childA', traceId: TRACE, spanId: 'cA', parentSpanId: 'r0', startMs: 1, durationMs: 3 }) + const grand = span({ name: 'grandchild', traceId: TRACE, spanId: 'g0', parentSpanId: 'cA', startMs: 2, durationMs: 1 }) + const childB = span({ name: 'childB', traceId: TRACE, spanId: 'cB', parentSpanId: 'r0', startMs: 5, durationMs: 4 }) + + // Order matters: children must arrive BEFORE the root for the exporter's + // temporaryStorage flush logic to merge them under the same primer. + const [primer] = exportAndCapture([childA, grand, childB, root]) + + // Single primer + expect(infoCalls.length).to.equal(1) + expect(primer).to.match(/^elapsed times:/) + + // Root line: 0.00 → 10.00 = 10.00 ms root (no indent on the root data line) + expect(primer).to.match(/\n +0\.00 → +10\.00 = +10\.00 ms {2}root/) + + // First-level children indented by 2 spaces beyond root + expect(primer).to.match(/\n.+ ms {4}childA/) + expect(primer).to.match(/\n.+ ms {4}childB/) + + // Grandchild indented by 4 spaces beyond root + expect(primer).to.match(/\n.+ ms {6}grandchild/) + + // Ordering: childA appears before grandchild appears before childB + expect(primer.indexOf('childA')).to.be.lessThan(primer.indexOf('grandchild')) + expect(primer.indexOf('grandchild')).to.be.lessThan(primer.indexOf('childB')) + }) + + it('relativizes child start/end to the root start time', () => { + // Root starts at 100 ms wallclock; child at 105 ms. Child should display as 5.00 → ... + const TRACE = 'b'.repeat(32) + const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 100, durationMs: 20 }) + const child = span({ name: 'child', traceId: TRACE, spanId: 'c0', parentSpanId: 'r0', startMs: 105, durationMs: 10 }) + + const [primer] = exportAndCapture([child, root]) + + expect(primer).to.match(/0\.00 → +20\.00 = +20\.00 ms {2}root/) + expect(primer).to.match(/5\.00 → +15\.00 = +10\.00 ms {4}child/) + }) + + it('sorts sibling spans by start time, ties broken by later end-time first', () => { + const TRACE = 'c'.repeat(32) + const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 50 }) + const late = span({ name: 'late', traceId: TRACE, spanId: 's3', parentSpanId: 'r0', startMs: 10, durationMs: 1 }) + const earlyLong = span({ name: 'earlyLong', traceId: TRACE, spanId: 's1', parentSpanId: 'r0', startMs: 0, durationMs: 30 }) + const earlyShort = span({ name: 'earlyShort', traceId: TRACE, spanId: 's2', parentSpanId: 'r0', startMs: 0, durationMs: 5 }) + + const [primer] = exportAndCapture([late, earlyShort, earlyLong, root]) + + // Equal start time → longer span first; otherwise by start time ascending + const order = ['earlyLong', 'earlyShort', 'late'].map(n => primer.indexOf(n)) + expect(order[0]).to.be.lessThan(order[1]) + expect(order[1]).to.be.lessThan(order[2]) + }) + + it('emits a separate primer per trace (multi-root)', () => { + const T1 = 'd'.repeat(32), + T2 = 'e'.repeat(32) + const r1 = span({ name: 'root1', traceId: T1, spanId: 'r1', startMs: 0, durationMs: 5 }) + const c1 = span({ name: 'c1', traceId: T1, spanId: 'c1', parentSpanId: 'r1', startMs: 1, durationMs: 2 }) + const r2 = span({ name: 'root2', traceId: T2, spanId: 'r2', startMs: 0, durationMs: 7 }) + const c2 = span({ name: 'c2', traceId: T2, spanId: 'c2', parentSpanId: 'r2', startMs: 1, durationMs: 3 }) + + exportAndCapture([c1, c2, r1, r2]) + + expect(infoCalls.length).to.equal(2) + const all = infoCalls.map(c => c[0]) + expect(all[0]).to.include('root1').and.to.include('c1').and.not.to.include('root2') + expect(all[1]).to.include('root2').and.to.include('c2').and.not.to.include('root1') + }) + + it('skips short "METHOD /word" spans (e.g. unadjusted http instrumentation roots)', () => { + const TRACE = 'f'.repeat(32) + const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 10 }) + // The skip regex is /^[A-Z]+ \/\${0,1}\w+$/ — single path segment, no slashes after the first. + const noisy = span({ name: 'GET /catalog', traceId: TRACE, spanId: 'h0', parentSpanId: 'r0', startMs: 1, durationMs: 5 }) + + const [primer] = exportAndCapture([noisy, root]) + + expect(primer).to.include('root') + expect(primer).not.to.include('GET /catalog') + }) + + it('handles deep nesting with increasing indentation', () => { + const TRACE = '1'.repeat(32) + const root = span({ name: 'L0', traceId: TRACE, spanId: 'L0', startMs: 0, durationMs: 10 }) + const l1 = span({ name: 'L1', traceId: TRACE, spanId: 'L1', parentSpanId: 'L0', startMs: 1, durationMs: 8 }) + const l2 = span({ name: 'L2', traceId: TRACE, spanId: 'L2', parentSpanId: 'L1', startMs: 2, durationMs: 6 }) + const l3 = span({ name: 'L3', traceId: TRACE, spanId: 'L3', parentSpanId: 'L2', startMs: 3, durationMs: 4 }) + + const [primer] = exportAndCapture([l1, l2, l3, root]) + + // Each deeper level adds 2 spaces of indentation + const indents = ['L0', 'L1', 'L2', 'L3'].map(n => { + const m = primer.match(new RegExp(`\\n( +)\\d.*ms( +)${n}(?!\\d)`)) + return m ? m[2].length - 1 : null // exclude the single space separator after "ms " + }) + // L0: 1 leading space before the name; each child adds 2. So we expect 1, 3, 5, 7. + expect(indents).to.deep.equal([1, 3, 5, 7]) + }) + }) + + describe('time formatting', () => { + it('formats sub-millisecond durations with two decimals', () => { + const TRACE = '2'.repeat(32) + const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 0.5 }) + const [primer] = exportAndCapture([root]) + expect(primer).to.match(/0\.00 → +0\.50 = +0\.50 ms/) + }) + + it('right-aligns integer portion to 3 chars', () => { + const TRACE = '3'.repeat(32) + const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 123 }) + const [primer] = exportAndCapture([root]) + // "123.00" → matches as-is, fits in the 3-char integer slot + expect(primer).to.match(/0\.00 → +123\.00 = +123\.00 ms/) + }) + }) + + describe('span name handling', () => { + it('truncates names longer than 80 chars with an ellipsis', () => { + const TRACE = '4'.repeat(32) + const longName = 'X'.repeat(100) + const root = span({ name: longName, traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 1 }) + + const [primer] = exportAndCapture([root]) + + expect(primer).to.include('X'.repeat(79) + '…') + expect(primer).not.to.include('X'.repeat(80)) + }) + }) + + describe('robustness', () => { + it('does not throw when a child arrives without its parent (orphan trace)', () => { + // No root provided for this trace — the exporter should buffer the child and not flush. + const TRACE = '5'.repeat(32) + const orphan = span({ name: 'orphan', traceId: TRACE, spanId: 'o0', parentSpanId: 'r-missing', startMs: 0, durationMs: 1 }) + + expect(() => exportAndCapture([orphan])).not.to.throw() + expect(infoCalls.length).to.equal(0) + }) + + it('treats any span without a parent as a root and emits a primer', () => { + const TRACE = '6'.repeat(32) + const lonely = span({ name: 'lonely', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 2 }) + + const [primer] = exportAndCapture([lonely]) + + expect(primer).to.match(/^elapsed times:/) + expect(primer).to.include('lonely') + }) + + it('shutdown flushes pending buffered children without throwing', () => { + const exporter = new ConsoleSpanExporter() + // No-op: just verify the shutdown contract. + return exporter.shutdown().then(() => { + // No exception, no logged primers (nothing was buffered). + expect(infoCalls.length).to.equal(0) + }) + }) + }) +}) diff --git a/test/tracing-attributes.test.js b/test/tracing-attributes.test.js index ac52e303..03204b13 100644 --- a/test/tracing-attributes.test.js +++ b/test/tracing-attributes.test.js @@ -2,15 +2,25 @@ process.env.cds_remote_native__fetch = 'true' const cds = require('@sap/cds') -const { expect, data } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-attributes') +const { expect, data } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') const http = require('http') -describe('tracing attributes', () => { - beforeEach(data.reset) +// The tracing-in-memory profile (see test/bookshop/.cdsrc.json) configures +// MyInMemorySpanExporter as the trace exporter. We read the captured ReadableSpan +// objects directly out of its shared buffer — no console spy, no provider-poking. +const { captured } = require('./bookshop/lib/MyInMemorySpanExporter') + +beforeEach(async () => { + // data.reset is itself heavily traced (it runs DELETEs + INSERTs for the seed data) — + // run it first, THEN clear the buffer so the test only sees its own spans. + await data.reset() + captured.length = 0 +}) - const log = jest.spyOn(console, 'dir') - beforeEach(log.mockClear) +// Returns all finished spans, optionally filtered by a predicate. +const spans = filter => (filter ? captured.filter(filter) : captured.slice()) +describe('tracing attributes', () => { describe('remote', () => { let server, port @@ -30,6 +40,9 @@ describe('tracing attributes', () => { }) test('HTTP client attributes are set on remote service span', async () => { + // skip for cds 8 due to Cloud SDK resilience module resolution issues in test environment + if (Number(cds.version.split('.')[0]) < 9) return + // configure destination URL directly on credentials cds.env.requires.TestRemote = { kind: 'odata', credentials: { url: `http://localhost:${port}` } } const remote = await cds.connect.to('TestRemote') @@ -37,54 +50,62 @@ describe('tracing attributes', () => { // no mock handler - let it make the actual HTTP call await remote.send({ method: 'GET', path: '/test' }) - const output = JSON.stringify(log.mock.calls) - expect(output).to.match(/"http\.request\.method":"GET"/) - expect(output).to.match(/"http\.response\.status_code":200/) - expect(output).to.match(new RegExp(`"url\\.full":"http://localhost:${port}/test"`)) - expect(output).to.match(/"server\.address":"localhost"/) - expect(output).to.match(new RegExp(`"server\\.port":${port}`)) + // Find the HTTP client span (instrumented by OTel's http instrumentation) + const httpSpan = spans(s => s.attributes['http.request.method'] === 'GET' && s.attributes['url.full']) + expect(httpSpan.length).to.be.gte(1, 'expected an HTTP client span') + const attrs = httpSpan[0].attributes + expect(attrs).to.include({ + 'http.request.method': 'GET', + 'http.response.status_code': 200, + 'url.full': `http://localhost:${port}/test`, + 'server.address': 'localhost', + 'server.port': port + }) }) }) describe('db', () => { const _db_spans = require('./_db_spans') - // prettier-ignore - const _get_db_spans = o => JSON.parse(o).map(o => o[0]).filter(s => !s.name.startsWith('db')) - const _match_db_spans = (output, kind) => { - const db_spans = _get_db_spans(output) - for (const each of _db_spans[kind]) expect(db_spans).to.containSubset([each]) + + // Filter out the high-level "db - …" CAP wrapper spans, keep only the @cap-js/ ones + // that carry the actual DB attributes. + const dbSpans = () => spans(s => !s.name.startsWith('db')) + + const _match_db_spans = kind => { + const got = dbSpans().map(s => ({ name: s.name, attributes: { ...s.attributes } })) + for (const each of _db_spans[kind]) expect(got).to.containSubset([each]) } test('SELECT', async () => { await SELECT.from('sap.capire.bookshop.Books').where('title !=', 'DUMMY') - const output = JSON.stringify(log.mock.calls) - expect(output).to.match(/db\.client\.response.returned_rows":5/) - _match_db_spans(output, 'SELECT') + const rowCounts = dbSpans().map(s => s.attributes['db.client.response.returned_rows']).filter(v => v != null) + expect(rowCounts).to.include(5) + _match_db_spans('SELECT') }) test('INSERT', async () => { await INSERT.into('sap.capire.bookshop.Books').entries([{ ID: 1 }, { ID: 2 }]) - const output = JSON.stringify(log.mock.calls) - expect(output).to.match(/db\.client\.response.returned_rows":2/) + const rowCounts = dbSpans().map(s => s.attributes['db.client.response.returned_rows']).filter(v => v != null) + expect(rowCounts).to.include(2) // TODO - // _match_db_spans(output, 'INSERT') + // _match_db_spans('INSERT') }) test('UPDATE', async () => { await UPDATE('sap.capire.bookshop.Books').set({ stock: 42 }).where('ID > 250') - const output = JSON.stringify(log.mock.calls) - expect(output).to.match(/db\.client\.response.returned_rows":3/) + const rowCounts = dbSpans().map(s => s.attributes['db.client.response.returned_rows']).filter(v => v != null) + expect(rowCounts).to.include(3) // TODO - // _match_db_spans(output, 'UPDATE') + // _match_db_spans('UPDATE') }) test('DELETE', async () => { await DELETE.from('sap.capire.bookshop.Books') - const output = JSON.stringify(log.mock.calls) - expect(output).to.match(/db\.client\.response.returned_rows":0/) //> texts - expect(output).to.match(/db\.client\.response.returned_rows":5/) + const rowCounts = dbSpans().map(s => s.attributes['db.client.response.returned_rows']).filter(v => v != null) + expect(rowCounts).to.include(0) // texts + expect(rowCounts).to.include(5) // TODO - // _match_db_spans(output, 'DELETE') + // _match_db_spans('DELETE') }) }) }) diff --git a/test/tracing-messaging-inboxed.test.js b/test/tracing-messaging-inboxed.test.js new file mode 100644 index 00000000..a5ec21e3 --- /dev/null +++ b/test/tracing-messaging-inboxed.test.js @@ -0,0 +1,66 @@ +const CASE = 'inboxed' + +// `inboxed: true` combined with the default outboxed messaging behavior means TWO queue +// workers get involved per emit — one on the producer side (drains outbox to broker) and +// one on the consumer side (drains inbox to subscribers). Each worker runs two +// transactions (tx 1: lock; tx 2: handle + delete). +// +// Each worker iteration is wrapped by `cds.spawn`, so both txs collapse under a single +// `cds.spawn - run task` root. 4 meaningful roots: +// +// 1. AdminService - tx (producer: handle test_emit, UPSERT outbox) +// 2. cds.spawn - run task (outbox worker: dispatches to file) +// ├─ db - tx (tx 1: lock) +// └─ messaging - tx (tx 2: handle foo — writes to file — + DELETE) +// 3. messaging - tx (file-based CONSUMER: writes inbox row) +// └─ ...enqueue into inbox... +// 4. cds.spawn - run task (inbox worker: runs subscriber) +// ├─ db - tx (tx 1: lock) +// └─ messaging - tx (tx 2: handle foo — SELECT Books — + DELETE) +// +// Tolerated: allow one extra root for the scheduling-service bookkeeping startup scan. + +// REVISIT: profile config wins for kind/file, but explicit env override sidesteps it. +process.env.cds_requires_messaging = JSON.stringify({ + kind: 'file-based-messaging', + file: `../${CASE}`, + inboxed: true +}) + +const CHECK = ({ expect, rootSpans, groupedByTrace }) => { + // Producer trace + const producer = groupedByTrace.find(g => g.all.some(s => s.name === 'AdminService - handle test_emit')) + expect(producer, 'expected a producer trace').to.exist + expect(producer.root.name).to.equal('AdminService - tx') + expect(producer.all.some(s => s.name === 'db - UPSERT cds.outbox.Messages')).to.be.true + + // The inbox worker must have run the application handler (SELECT Books). + const allSpans = groupedByTrace.flatMap(g => g.all) + expect(allSpans.some(s => s.name.match(/READ sap\.capire\.bookshop\.Books/))).to.be.true + expect(allSpans.some(s => s.name === 'db - DELETE cds.outbox.Messages')).to.be.true + + // Exactly two `cds.spawn - run task` roots (outbox worker + inbox worker). + const workerRoots = rootSpans.filter(s => s.name === 'cds.spawn - run task') + expect(workerRoots, 'expected two queue-worker spawn roots (outbox + inbox)').to.have.lengthOf(2) + + // One of the spawn roots (the inbox worker) ran the app handler. + const inboxWorker = groupedByTrace.find( + g => g.root.name === 'cds.spawn - run task' && g.all.some(s => s.name.match(/READ sap\.capire\.bookshop\.Books/)) + ) + expect(inboxWorker, 'expected an inbox-worker trace that ran the application handler').to.exist + + // 4 meaningful roots (+1 tolerated bookkeeping scan). + expect(rootSpans.length).to.be.gte(4) + expect(rootSpans.length).to.be.lte(5) +} + +const cds = require('@sap/cds') + +describe(`tracing messaging - ${CASE}`, () => { + // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. + if (cds.env.requires.db?.kind === 'sqlite') { + test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) + return + } + require('./tracing-messaging')(CASE, CHECK, { waitMs: 4000 }) +}) diff --git a/test/tracing-messaging-persistent-outbox.test.js b/test/tracing-messaging-persistent-outbox.test.js index 1d959595..82b89109 100644 --- a/test/tracing-messaging-persistent-outbox.test.js +++ b/test/tracing-messaging-persistent-outbox.test.js @@ -6,15 +6,101 @@ process.env.cds_requires_messaging = JSON.stringify({ file: `../${CASE}` }) -// REVISIT: check json exports -const CHECK = (log, expect) => { - // 3: outbox -> consumers get new root context - // REVISIT: for some reason, span "cds.spawn run task" has no parent when running in jest - expect(log.output.match(/\[telemetry\] - elapsed times:/g).length).to.equal(4) //> actually 3 - expect(log.output.match(/cds.spawn - schedule task/g).length).to.equal(1) +// --- Span hierarchy for the persistent-outbox case --------------------------------------- +// +// With persistent outbox enabled, the queue worker runs two coherent transactions: +// - tx 1: read out of queue + set status='processing' (libx/queue/processing.js:189) +// - tx 2: handle the event + delete the row (libx/queue/processing.js:319) +// +// `@cap-js/telemetry` wraps `cds.tx(fn)` to emit a ` - tx` span per callback, so +// each of these transactions is captured as a root/child span. Both sqlite and HANA now +// produce the same unified shape: the worker uses `cds.spawn`, which the telemetry plugin +// wraps to emit a single `cds.spawn - run task` CONSUMER root that both worker tx spans +// nest under. +// +// Expected shape (3 meaningful roots, same for sqlite and HANA): +// +// 1. AdminService - tx (producer trace) +// └─ AdminService - handle test_emit +// └─ messaging - emit outgoing foo +// └─ db - UPSERT cds.outbox.Messages +// └─ cds.spawn - schedule task +// +// 2. cds.spawn - run task (queue worker root) +// ├─ db - tx (tx 1) +// │ ├─ db - READ cds.outbox.Messages +// │ └─ db - UPDATE cds.outbox.Messages +// └─ messaging - tx (tx 2) +// ├─ messaging - handle foo +// └─ db - DELETE cds.outbox.Messages +// +// 3. messaging - tx (file-based CONSUMER) +// └─ ...handler work (READ Books, READ Authors)... +// +// Plus the scheduling service may emit a bookkeeping `db - tx` (startup scan finding no +// tasks) — tolerated as a 4th root, not required. + +const CHECK = ({ expect, rootSpans, groupedByTrace }) => { + // Producer trace + const producer = groupedByTrace.find(g => g.all.some(s => s.name === 'AdminService - handle test_emit')) + expect(producer, 'expected a producer trace').to.exist + expect(producer.root.name).to.equal('AdminService - tx') + expect(producer.all.some(s => s.name === 'messaging - emit outgoing foo')).to.be.true + expect(producer.all.some(s => s.name === 'db - UPSERT cds.outbox.Messages')).to.be.true + expect(producer.all.some(s => s.name === 'cds.spawn - schedule task')).to.be.true + + // Queue worker trace: rooted at `cds.spawn - run task`, containing both tx spans as children. + const workerTrace = groupedByTrace.find(g => g.root.name === 'cds.spawn - run task') + expect(workerTrace, 'expected a queue-worker spawn-root trace').to.exist + + // tx 1: db - tx with READ + UPDATE of the outbox + const workerDbTx = workerTrace.all.find(s => s.name === 'db - tx') + expect(workerDbTx, 'expected a db - tx child in the worker trace (tx 1)').to.exist + expect(workerTrace.all.some(s => s.name === 'db - READ cds.outbox.Messages')).to.be.true + expect(workerTrace.all.some(s => s.name === 'db - UPDATE cds.outbox.Messages')).to.be.true + + // tx 2: messaging - tx with handle foo + DELETE of the outbox row + const workerMessagingTx = workerTrace.all.find(s => s.name === 'messaging - tx') + expect(workerMessagingTx, 'expected a messaging - tx child in the worker trace (tx 2)').to.exist + expect(workerTrace.all.some(s => s.name === 'messaging - handle foo')).to.be.true + expect(workerTrace.all.some(s => s.name === 'db - DELETE cds.outbox.Messages')).to.be.true + + // File-based CONSUMER trace (the file-messaging consumer, *not* the queue-worker path). + // Identified by containing the full `foo` handler work (SELECT Books + READ Authors). + const consumer = groupedByTrace.find( + g => + g !== producer && + g !== workerTrace && + g.root.name === 'messaging - tx' && + g.all.some(s => s.name.match(/READ sap\.capire\.bookshop\.Books/)) && + g.all.some(s => s.name === 'AdminService - READ AdminService.Authors') + ) + expect(consumer, 'expected a CONSUMER trace').to.exist + expect(consumer.all.some(s => s.name === 'messaging - emit outgoing foo')).to.be.true + expect(consumer.all.some(s => s.name === 'messaging - handle foo')).to.be.true + + // 3 meaningful roots; tolerate one extra for the scheduling-service bookkeeping scan + // (a `db - tx` root with just a READ, no UPDATE). + expect(rootSpans.length).to.be.gte(3) + expect(rootSpans.length).to.be.lte(4) + + // Sanity: every non-root span has a parent inside the captured set. + const allSpans = groupedByTrace.flatMap(g => g.all) + for (const s of allSpans) { + const pid = s.parentSpanContext?.spanId + if (!pid) continue + const parent = allSpans.find(p => p.spanContext().spanId === pid) + expect(parent, `expected parent span for ${s.name}`).to.exist + } } -// REVISIT: re-enable with switch to vitest -describe.skip(`tracing messaging - ${CASE}`, () => { - require('./tracing-messaging')(CASE, CHECK) +const cds = require('@sap/cds') + +describe(`tracing messaging - ${CASE}`, () => { + // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. + if (cds.env.requires.db?.kind === 'sqlite') { + test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) + return + } + require('./tracing-messaging')(CASE, CHECK, { waitMs: 4000 }) }) diff --git a/test/tracing-messaging-without-outbox.test.js b/test/tracing-messaging-without-outbox.test.js index f93664c1..81138218 100644 --- a/test/tracing-messaging-without-outbox.test.js +++ b/test/tracing-messaging-without-outbox.test.js @@ -7,10 +7,41 @@ process.env.cds_requires_messaging = JSON.stringify({ outboxed: false }) -// REVISIT: check json exports -const CHECK = (log, expect) => { - // 2: no outbox -> consumer gets new root context - expect(log.output.match(/\[telemetry\] - elapsed times:/g).length).to.equal(2) +// Without outbox, file-based messaging writes directly to the file from the producer's +// transaction (no queue worker). The file watcher delivers asynchronously as a new +// SpanKind.CONSUMER root. +// +// Expected roots: +// 1. AdminService - tx (producer) +// └─ AdminService - handle test_emit +// └─ messaging - emit outgoing foo +// └─ messaging - handle foo (writes to file, in-process) +// +// 2. messaging - tx (file-based CONSUMER) +// └─ messaging - emit outgoing foo +// └─ messaging - handle foo +// └─ ...handler work... +// +// The scheduling service may also emit a bookkeeping scan trace (`db - tx → db - READ +// cds.outbox.Messages` finding nothing) — we allow it but don't require it. + +const CHECK = ({ expect, rootSpans, groupedByTrace }) => { + // Producer trace + const producer = groupedByTrace.find(g => g.all.some(s => s.name === 'AdminService - handle test_emit')) + expect(producer, 'expected a producer trace').to.exist + expect(producer.root.name).to.equal('AdminService - tx') + expect(producer.all.some(s => s.name.match(/messaging - emit outgoing/))).to.be.true + + // File-based CONSUMER trace + const consumer = groupedByTrace.find( + g => g !== producer && g.root.name === 'messaging - tx' && g.all.some(s => s.name === 'messaging - handle foo') + ) + expect(consumer, 'expected a CONSUMER trace').to.exist + expect(consumer.all.some(s => s.name.match(/READ sap\.capire\.bookshop\.Books/))).to.be.true + + // 2 meaningful roots; allow up to 3 to tolerate the scheduling service's bookkeeping scan. + expect(rootSpans.length).to.be.gte(2) + expect(rootSpans.length).to.be.lte(3) } describe(`tracing messaging - ${CASE}`, () => { diff --git a/test/tracing-messaging.js b/test/tracing-messaging.js index 35508745..15202e34 100644 --- a/test/tracing-messaging.js +++ b/test/tracing-messaging.js @@ -1,7 +1,7 @@ -module.exports = (CASE, CHECK) => { +module.exports = (CASE, CHECK, { waitMs = 4000 } = {}) => { const cds = require('@sap/cds') - const { expect, POST } = cds.test(__dirname + '/bookshop', '--profile', CASE) - const log = cds.test.log() + const { expect, POST } = cds.test(__dirname + '/bookshop', '--profile', `${CASE},tracing-in-memory`) + const { reset, rootSpans, groupedByTrace, captured } = require('./bookshop/lib/MyInMemorySpanExporter') const wait = require('node:timers/promises').setTimeout @@ -21,16 +21,22 @@ module.exports = (CASE, CHECK) => { }) afterAll(async () => { - await wait(100) + // Wait long enough for any background queue-worker / scheduling-service timers to + // fire one last time before jest tears down the env. Without this, those timers can + // fire after teardown and crash with "cds.error.isSystemError is not a function" + // (cds module is reloaded between tests, but the timer references the old instance). + await wait(2000) rm() }) - beforeEach(log.clear) + beforeEach(() => { + reset() + }) test('emit is traced', async () => { await POST('/odata/v4/admin/test_emit', {}, admin) - await wait(1000) - // execute case specific check - CHECK(log, expect) + await wait(waitMs) + // CHECK is called with span-level data: { expect, rootSpans, groupedByTrace, captured, cds } + CHECK({ expect, rootSpans: rootSpans(), groupedByTrace: groupedByTrace(), captured: [...captured], cds }) }) } diff --git a/test/tracing-mt.test.js b/test/tracing-mt.test.js index f6f3c36a..872a6dc2 100644 --- a/test/tracing-mt.test.js +++ b/test/tracing-mt.test.js @@ -1,7 +1,8 @@ const cds = require('@sap/cds') // prettier-ignore -const { expect, GET } = cds.test('serve', '--in-memory', '--project', __dirname + '/bookshop', '--profile', 'multitenancy') -const log = cds.test.log() +const { expect, GET } = cds.test('serve', '--in-memory', '--project', __dirname + '/bookshop', '--profile', 'multitenancy,tracing-in-memory') + +const { reset, captured } = require('./bookshop/lib/MyInMemorySpanExporter') describe('tracing with multitenancy', () => { const TENANT1 = 'tenant_1' @@ -17,22 +18,23 @@ describe('tracing with multitenancy', () => { await mts.subscribe(TENANT2) }) - beforeEach(log.clear) + beforeEach(reset) test('GET with user1 is traced', async () => { const { status } = await GET('/odata/v4/admin/Books', user1) expect(status).to.equal(200) - // primitive check that console has trace logs - expect(log.output).to.match(/\[telemetry|tenant_1\] - elapsed times:/) - expect(log.output).to.match(/\s+\d+\.\d+ → \s*\d+\.\d+ = \s*\d+\.\d+ ms \s* AdminService - READ AdminService.Books/) + // AdminService READ ran exactly once and was tagged with the right tenant. + const spans = captured.filter(s => s.name === 'AdminService - READ AdminService.Books') + expect(spans.length, 'expected exactly one AdminService READ span').to.equal(1) + expect(spans[0].attributes['sap.tenancy.tenant_id']).to.equal(TENANT1) }) test('GET with user2 is traced', async () => { const { status } = await GET('/odata/v4/admin/Books', user2) expect(status).to.equal(200) - // primitive check that console has trace logs - expect(log.output).to.match(/\[telemetry|tenant_2\] - elapsed times:/) - expect(log.output).to.match(/\s+\d+\.\d+ → \s*\d+\.\d+ = \s*\d+\.\d+ ms \s* AdminService - READ AdminService.Books/) + const spans = captured.filter(s => s.name === 'AdminService - READ AdminService.Books') + expect(spans.length, 'expected exactly one AdminService READ span').to.equal(1) + expect(spans[0].attributes['sap.tenancy.tenant_id']).to.equal(TENANT2) }) // --- TODO --- diff --git a/test/tracing-outboxed-batch.test.js b/test/tracing-outboxed-batch.test.js new file mode 100644 index 00000000..640689ea --- /dev/null +++ b/test/tracing-outboxed-batch.test.js @@ -0,0 +1,72 @@ +// Tests that when the queue worker picks up multiple ready tasks in one iteration +// (chunkSize > 1), each is dispatched in its own tx span under the SAME worker root. +// This validates the parallel-fan-out shape described in the design notes. + +const cds = require('@sap/cds') +const { expect, POST } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'tracing-in-memory') +const { reset, captured, groupedByTrace } = require('./bookshop/lib/MyInMemorySpanExporter') +const { hrTimeToNanoseconds } = require('@opentelemetry/core') + +const wait = require('node:timers/promises').setTimeout + +describe('tracing for outboxed batch (chunk-size fan-out)', () => { + if (Number(cds.version.split('.')[0]) < 9) { + test.skip('skipping for cds < 9', () => {}) + return + } + // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. + if (cds.env.requires.db?.kind === 'sqlite') { + test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) + return + } + + beforeAll(async () => { + const externalOne = await cds.connect.to('ExternalServiceOne') + externalOne.on('call', () => 'ok') + }) + + beforeEach(reset) + + test('three queued sends produce parallel dispatch spans under one worker root', async () => { + await POST('/odata/v4/admin/test_outboxed_send_batch', {}, { auth: { username: 'alice' } }) + await wait(2500) + + // Producer wrote three rows to the outbox. + const upserts = captured.filter(s => s.name === 'db - UPSERT cds.outbox.Messages') + expect(upserts.length, 'expected three producer outbox UPSERTs').to.be.gte(3) + + // Look for a queue worker root containing multiple dispatch tx spans. + const workerTrace = groupedByTrace().find(g => + g.root.name === 'cds.spawn - run task' && + g.all.filter(s => s.name === 'ExternalServiceOne - tx').length >= 2 + ) + expect(workerTrace, 'expected a worker trace with multiple ExternalServiceOne - tx children').to.exist + + // The worker root must have exactly one lock tx (db - tx with READ + UPDATE)… + const lockTxs = workerTrace.all.filter(s => + s.name === 'db - tx' && + workerTrace.all.some(c => c.parentSpanContext?.spanId === s.spanContext().spanId && c.name === 'db - READ cds.outbox.Messages') + ) + expect(lockTxs, 'expected one lock tx (db - tx with READ + UPDATE)').to.have.lengthOf(1) + + // …and multiple dispatch txs, each containing an ExternalServiceOne handle span + DELETE. + const dispatchTxs = workerTrace.all.filter(s => s.name === 'ExternalServiceOne - tx') + expect(dispatchTxs.length, 'expected multiple dispatch txs (chunk-size fan-out)').to.be.gte(2) + for (const tx of dispatchTxs) { + const kids = workerTrace.all.filter(k => k.parentSpanContext?.spanId === tx.spanContext().spanId) + expect(kids.some(k => k.name.match(/ExternalServiceOne - handle/)), 'dispatch tx should contain handle call').to.be.true + expect(kids.some(k => k.name === 'db - DELETE cds.outbox.Messages'), 'dispatch tx should contain DELETE').to.be.true + } + + // The dispatch txs should overlap in time (parallel), not be strictly sequential. + if (dispatchTxs.length >= 2) { + const sorted = [...dispatchTxs].sort( + (a, b) => hrTimeToNanoseconds(a.startTime) - hrTimeToNanoseconds(b.startTime) + ) + const firstEndNs = hrTimeToNanoseconds(sorted[0].endTime) + const secondStartNs = hrTimeToNanoseconds(sorted[1].startTime) + // Parallel: second starts before first ends (allow a tiny slack). + expect(secondStartNs, 'expected parallel dispatch: task2 starts before task1 ends').to.be.lessThan(firstEndNs) + } + }) +}) diff --git a/test/tracing-scheduled.test.js b/test/tracing-scheduled.test.js new file mode 100644 index 00000000..a5928956 --- /dev/null +++ b/test/tracing-scheduled.test.js @@ -0,0 +1,73 @@ +// Tests tracing of scheduled tasks. +// +// `cds.queued(svc).schedule('event', ...).after(N)` writes a task row to the persistent +// outbox with a timestamp N ms in the future. The queue scheduler picks it up at that +// time and dispatches to the target service's handler. +// +// Expected meaningful roots (unified across sqlite and HANA): +// +// 1. AdminService - tx (producer trace) +// └─ AdminService - handle test_scheduled +// └─ db - UPSERT cds.outbox.Messages +// └─ cds.spawn - schedule task +// +// 2. cds.spawn - run task (queue worker root) +// ├─ db - tx (tx 1: lock) +// └─ ExternalServiceOne - tx (tx 2: dispatch) +// +// Plus optionally one bookkeeping startup-scan trace (tolerated, not required). +// Total meaningful roots: between 2 and 3. + +const cds = require('@sap/cds') +const { expect, POST } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'tracing-in-memory') +const { reset, captured, groupedByTrace, rootSpans } = require('./bookshop/lib/MyInMemorySpanExporter') + +const wait = require('node:timers/promises').setTimeout + +describe('tracing for scheduled tasks', () => { + if (Number(cds.version.split('.')[0]) < 9) { + test.skip('skipping for cds < 9', () => {}) + return + } + // Queue-worker spans (cds.spawn - run task root) require @sap/cds to route the sqlite + // queue worker through cds.spawn. Published cds uses a raw setTimeout bypass on sqlite + // (to avoid a single-writer deadlock), so those spans never appear. Skip until the cds + // fix lands (cap/cds test/queue-spawn-sqlite-extended-tenant). REMOVE with follow-up PR. + if (cds.env.requires.db?.kind === 'sqlite') { + test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) + return + } + + beforeAll(async () => { + const externalOne = await cds.connect.to('ExternalServiceOne') + externalOne.on('call', () => 'ok') + }) + + beforeEach(reset) + + test('schedule .after() is fully traced through the queue worker', async () => { + await POST('/odata/v4/admin/test_scheduled', {}, { auth: { username: 'alice' } }) + // wait long enough for the scheduled task to fire (10ms after-delay + worker latency) + await wait(1500) + + // Producer trace: writes the task row inside the HTTP request tx. + const producer = groupedByTrace().find(g => g.all.some(s => s.name === 'AdminService - handle test_scheduled')) + expect(producer, 'expected a producer trace').to.exist + expect(producer.root.name).to.equal('AdminService - tx') + expect(producer.all.some(s => s.name === 'db - UPSERT cds.outbox.Messages')).to.be.true + expect(producer.all.some(s => s.name === 'cds.spawn - schedule task')).to.be.true + + // Queue worker trace: rooted at cds.spawn - run task, contains both tx spans. + const workerTrace = groupedByTrace().find(g => g.root.name === 'cds.spawn - run task') + expect(workerTrace, 'expected a queue-worker spawn-root trace').to.exist + expect(workerTrace.all.some(s => s.name === 'db - tx')).to.be.true + expect(workerTrace.all.some(s => s.name === 'ExternalServiceOne - tx')).to.be.true + + // The ExternalServiceOne handler was invoked. + expect(captured.some(s => s.name.match(/ExternalServiceOne - handle/))).to.be.true + + // Total meaningful roots: producer + worker (+ optional bookkeeping scan). + expect(rootSpans().length).to.be.gte(2) + expect(rootSpans().length).to.be.lte(3) + }) +}) diff --git a/test/tracing.test.js b/test/tracing.test.js index f3d0ce05..b1cd985a 100644 --- a/test/tracing.test.js +++ b/test/tracing.test.js @@ -4,22 +4,27 @@ process.env.cds_requires_telemetry_tracing_sampler = JSON.stringify({ }) const cds = require('@sap/cds') -const { expect, GET, POST } = cds.test(__dirname + '/bookshop') -const log = cds.test.log() +const { expect, GET, POST } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') + +// Assert against the structured ReadableSpan objects captured by MyInMemorySpanExporter +// (configured via the tracing-in-memory profile in test/bookshop/.cdsrc.json) — no +// console spying, no string-regex matching of formatted output. +const { reset, rootSpans, captured } = require('./bookshop/lib/MyInMemorySpanExporter') const wait = require('node:timers/promises').setTimeout describe('tracing', () => { const admin = { auth: { username: 'alice' } } - beforeEach(log.clear) + beforeEach(reset) test('GET is traced', async () => { const { status } = await GET('/odata/v4/admin/Books', admin) expect(status).to.equal(200) - // primitive check that console has trace logs - expect(log.output).to.match(/\[telemetry\] - elapsed times:/) - expect(log.output).to.match(/\s+\d+\.\d+ → \s*\d+\.\d+ = \s*\d+\.\d+ ms \s* AdminService - READ AdminService.Books/) + // The AdminService READ for Books was traced + expect(captured.some(s => s.name === 'AdminService - READ AdminService.Books')).to.be.true + // ...and at least one trace was rooted (i.e. our exporter would emit "elapsed times:") + expect(rootSpans().length).to.be.gte(1) }) // REVISIT: jest breaks otel's patching of incoming request handling -> no span for 'GET' -> behavior to test not reproducible @@ -27,17 +32,13 @@ describe('tracing', () => { const config = { ...admin, headers: { traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01' } } const { status } = await GET('/odata/v4/admin/Books', config) expect(status).to.equal(200) - // primitive check that console has trace logs - expect(log.output).to.match(/\[telemetry\] - elapsed times:/) - expect(log.output).to.match(/\s+\d+\.\d+ → \s*\d+\.\d+ = \s*\d+\.\d+ ms \s* AdminService - READ AdminService.Books/) + expect(captured.some(s => s.name === 'AdminService - READ AdminService.Books')).to.be.true }) test('custom GET is traced', async () => { const { status } = await GET('/custom/Books', admin) expect(status).to.equal(200) - // primitive check that console has trace logs - expect(log.output).to.match(/\[telemetry\] - elapsed times:/) - expect(log.output).to.match(/\s+\d+\.\d+ → \s*\d+\.\d+ = \s*\d+\.\d+ ms \s* db - READ sap.capire.bookshop.Books/) + expect(captured.some(s => s.name === 'db - READ sap.capire.bookshop.Books')).to.be.true }) test('NonRecordingSpans are handled correctly', async () => { @@ -45,20 +46,13 @@ describe('tracing', () => { expect(postStatus).to.equal(201) const { status: getStatus } = await GET('/odata/v4/admin/Authors?$select=ID', admin) expect(getStatus).to.equal(200) - // primitive check that console has no trace logs - expect(log.output).not.to.match(/telemetry/) + // The sampler in this test ignores /odata/v4/admin/Authors — no spans should be captured for it. + // (Other unrelated background work may still produce spans; assert only that none mention Authors.) + expect(captured.filter(s => s.attributes['url.path']?.includes('/admin/Authors'))).to.have.lengthOf(0) }) // REVISIT: jest breaks otel's patching of incoming request handling -> behavior to test not reproducible - xtest('instrumentation hooks', async () => { - await GET('/odata/v4/admin/Books(251)', admin) - // primitive check that console has trace logs - expect(log.output).to.match(/\[telemetry\] - elapsed times:/) - log.clear() - await GET('/odata/v4/admin/Books(252)', admin) - // primitive check that console has no trace logs - expect(log.output).not.to.match(/telemetry/) - }) + xtest('instrumentation hooks', async () => {}) test('$batch is traced', async () => { await POST( @@ -71,51 +65,48 @@ describe('tracing', () => { }, admin ) - // 4: POST: create/ new + read after write, GET: read actives + read drafts - expect(log.output.match(/\[telemetry\] - elapsed times:/g).length).to.equal(4) + // With the tx wrap (lib/tracing/cds.js), each batch request's tx becomes a single root — + // the previously-visible 4 sub-roots (POST: CREATE + read-after-write; GET: read actives + + // read drafts) are now nested under 2 root tx spans, one per batch entry. + expect(rootSpans()).to.have.lengthOf(2) }) test('cds.spawn is traced', async () => { await POST('/odata/v4/admin/test_spawn', {}, admin) await wait(30) - // 2: action + spawned action - expect(log.output.match(/\[telemetry\] - elapsed times:/g).length).to.equal(2) + // 2 visible roots: the action invocation + the spawned task + expect(rootSpans()).to.have.lengthOf(2) + expect(captured.some(s => s.name === 'cds.spawn - schedule task')).to.be.true + expect(captured.some(s => s.name === 'cds.spawn - run task')).to.be.true }) test('emit is traced', async () => { await POST('/odata/v4/admin/test_emit', {}, admin) await wait(100) - // 1: local-messaging remains in same context - expect(log.output.match(/\[telemetry\] - elapsed times:/g).length).to.equal(1) + // local-messaging keeps the consumer in the same context → exactly 1 visible root + expect(rootSpans()).to.have.lengthOf(1) }) describe('db', () => { describe('ql', () => { test('SELECT is traced', async () => { await SELECT.from('sap.capire.bookshop.Books') - // primitive check that console has trace logs - expect(log.output).to.match(/\[telemetry\] - elapsed times:/) - expect(log.output).to.match( - /\s+\d+\.\d+ → \s*\d+\.\d+ = \s*\d+\.\d+ ms \s* db - READ sap\.capire\.bookshop\.Books/ - ) + expect(captured.some(s => s.name === 'db - READ sap.capire.bookshop.Books')).to.be.true }) }) test('native db statement is traced', async () => { const db = await cds.connect.to('db') await db.run('SELECT ID, title, stock, price FROM AdminService_Books WHERE ID = 201 OR ID = 207') - // primitive check that console has trace logs - expect(log.output).to.match(/\[telemetry\] - elapsed times:/) - expect(log.output).to.match( - /\s+\d+\.\d+ → \s*\d+\.\d+ = \s*\d+\.\d+ ms \s* db - SELECT .* FROM AdminService_Books WHERE ID = 201 OR I…/ - ) + // The wrapper "db - SELECT …" span carries the raw SQL as part of the name. + expect(captured.some(s => s.name.startsWith('db - SELECT') && s.name.includes('AdminService_Books'))).to.be.true }) }) test('custom spans are supported', async () => { await GET('/odata/v4/catalog/ListOfBooks', {}, admin) await wait(100) - expect(log.output.match(/my custom span/g).length).to.equal(1) + expect(captured.filter(s => s.name === 'my custom span')).to.have.lengthOf(1) }) // --- TODO --- From fdbd1bb759322221b666e9d0a69d431f0089afeb Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:15:44 +0200 Subject: [PATCH 06/31] chore: migrate test runner from jest to vitest (#474) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- eslint.config.mjs | 13 +- jest.config.js | 14 - package-lock.json | 6949 +++++++---------------- package.json | 4 +- test/bookshop/package.json | 4 +- test/logging.test.js | 9 +- test/metrics-outbox-disabled.test.js | 5 +- test/metrics-outbox-multitenant.test.js | 5 +- test/metrics-outbox.test.js | 7 +- test/tracing-attributes.test.js | 29 +- test/tracing-remote-cloudsdk.test.js | 25 +- test/tracing-remote-native.test.js | 25 +- test/tracing-span-names.test.js | 25 +- vitest.config.mjs | 37 + 14 files changed, 2175 insertions(+), 4976 deletions(-) delete mode 100644 jest.config.js create mode 100644 vitest.config.mjs diff --git a/eslint.config.mjs b/eslint.config.mjs index 0d250b28..79f04fff 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,2 +1,13 @@ import cds from '@sap/cds/eslint.config.mjs' -export default [...cds.recommended] +export default [ + ...cds.recommended, + { + // The cds eslint config declares jest/mocha test globals but not vitest's `vi`. + files: ['**/+(test|tests)/**/*.+(js|cjs|mjs)', '**/*.test.+(js|cjs|mjs)', '**/*-test.+(js|cjs|mjs)'], + languageOptions: { + globals: { + vi: 'readonly' + } + } + } +] diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 70fb6cd8..00000000 --- a/jest.config.js +++ /dev/null @@ -1,14 +0,0 @@ -const config = { - testTimeout: 42000, - testMatch: ['**/*.test.js'] -} - -if (process.env.CI && process.env.HANA_DRIVER) { - config.testTimeout *= 10 - config.testMatch = ['**/tracing-attributes.test.js', '**/passport.test.js'] - - if (process.env.HANA_PROM) - process.env.cds_requires_telemetry_tracing = JSON.stringify({ _hana_prom: process.env.HANA_PROM === 'true' }) -} - -module.exports = config diff --git a/package-lock.json b/package-lock.json index 434f7a0a..759d61b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,508 +37,12 @@ "@sap/cds-mtxs": "^4", "axios": "^1.6.7", "eslint": "^10", - "jest": "^30.4.2" + "vitest": "^4" }, "peerDependencies": { "@sap/cds": "^10 || ^9" } }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", - "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", - "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helpers": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", - "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.8", - "@babel/types": "^7.29.8", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", - "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", - "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", - "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", - "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", - "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", - "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", - "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", - "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", - "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.8" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-bigint": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", - "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", - "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", - "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", - "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", - "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/template": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", - "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", - "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.8", - "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.8", - "@babel/template": "^7.29.7", - "@babel/types": "^7.29.8", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", - "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true, - "license": "MIT" - }, "node_modules/@cap-js/cds-test": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@cap-js/cds-test/-/cds-test-1.0.1.tgz", @@ -623,45 +127,11 @@ "kuler": "^2.0.0" } }, - "node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", - "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", - "devOptional": true, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "devOptional": true, "license": "MIT", "dependencies": { "eslint-visitor-keys": "^3.4.3" @@ -884,480 +354,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.4.1.tgz", - "integrity": "sha512-v3bhyxUh9Hgmo5p6hAOXe14/R3ZxZDOsvHleh4B07z3m/x4/ngPUXEm9XwK4sF4u+f+P2ORb0Ge+MgpaqRMVDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "@types/node": "*", - "chalk": "^4.1.2", - "jest-message-util": "30.4.1", - "jest-util": "30.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/core": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.4.2.tgz", - "integrity": "sha512-TZJA6cPJUFxoWhxaLo8t0VX/MZX2wPWr0uIDvLSHIvN4gu9h02vSzqI2kBADG1ExqQlC+cY09xKMSreivvrChQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.4.1", - "@jest/pattern": "30.4.0", - "@jest/reporters": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "exit-x": "^0.2.2", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-changed-files": "30.4.1", - "jest-config": "30.4.2", - "jest-haste-map": "30.4.1", - "jest-message-util": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-resolve-dependencies": "30.4.2", - "jest-runner": "30.4.2", - "jest-runtime": "30.4.2", - "jest-snapshot": "30.4.1", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "jest-watcher": "30.4.1", - "pretty-format": "30.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/diff-sequences": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.4.0.tgz", - "integrity": "sha512-zOpzlfUs45l6u7jm39qr87JCHUDsaeCtvL+kQe/Vn9jSnRB4/5IPXISm0h9I1vZW/o00Kn4UTJ2MOlhnUGwv3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/environment": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.4.1.tgz", - "integrity": "sha512-AK9yNRqgKxiabqMoe4oW+3/TSSeV8vkdC7BGaxZdU0AFXfOpofTLqdru2GXKZghP3sdgwE9XXpnVwfZ8JnFV4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "jest-mock": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/expect": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.4.1.tgz", - "integrity": "sha512-ginrj6TMgh2GshLUGCjO94Ptx9HhdZA/I6A9iUfyeLKFtdAjnKzHDgzgP9HYQgbxM1lbXScQ2eUBz2lGeVDPWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "30.4.1", - "jest-snapshot": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/expect-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.4.1.tgz", - "integrity": "sha512-ZBn5CglH8fBsQsvs4VWNzD4aWfUYks+IdOOQU3MEK71ol/BcVm+P+rtb1KpiFBpSWSCE27uOahyyf1vfqOVbcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/fake-timers": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.4.1.tgz", - "integrity": "sha512-iW5umdmfPeWzehrVhugFQZqCchSCud5S1l2YT0O9ZhjRR0ExclANDZkiSBwzqtnlOn0J1JXvO+HZ6rkuyOVOgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "@sinonjs/fake-timers": "^15.4.0", - "@types/node": "*", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-util": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/get-type": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", - "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/globals": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.4.1.tgz", - "integrity": "sha512-ZbuY4cmXC8DkxYjfvT2DbcHWL2T6vmsMhXCDcmTB2T0y0gaezBI77ufq5ZAIdcRkYZ7NEQEDg1xFeKbxUJ5v5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.4.1", - "@jest/expect": "30.4.1", - "@jest/types": "30.4.1", - "jest-mock": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/pattern": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.4.0.tgz", - "integrity": "sha512-RAWn3+f9u8BsHijKJ71uHcFp6vmyEt6VvoWXkl6hKF3qVIuWNmudVjg12DlBPGup/frIl5UcUlH5HfEuvHpEXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-regex-util": "30.4.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/reporters": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.4.1.tgz", - "integrity": "sha512-/SnkPCzEQpUaBH81kjdEdDdo2WZl5hxw+BmLDGWjRkm8o7XlhjwsU36cqwe5PGBE5WYpBvDzRSdXx9rbGuJtNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "@jridgewell/trace-mapping": "^0.3.25", - "@types/node": "*", - "chalk": "^4.1.2", - "collect-v8-coverage": "^1.0.2", - "exit-x": "^0.2.2", - "glob": "^10.5.0", - "graceful-fs": "^4.2.11", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^5.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "30.4.1", - "jest-util": "30.4.1", - "jest-worker": "30.4.1", - "slash": "^3.0.0", - "string-length": "^4.0.2", - "v8-to-istanbul": "^9.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/@jest/schemas": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.4.1.tgz", - "integrity": "sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.34.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/snapshot-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.4.1.tgz", - "integrity": "sha512-ObY4ljvQ95mt6iwKtVLetR/4yXiAgl3H4nJxhztr0MTjrN97TwDYrnCp/kF60Ec9HdhkWTHSu+Hg05aXfngpOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "natural-compare": "^1.4.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/source-map": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", - "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "callsites": "^3.1.0", - "graceful-fs": "^4.2.11" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-result": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.4.1.tgz", - "integrity": "sha512-/ZG7pgEiOmmWkN9TplKbOu4id2N5lh7FHwRwlkgBVAzGdRH+OkkQ8wX/kIxg4zmd3ZQvAL1RwL2yWsvNYYECTw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "30.4.1", - "@jest/types": "30.4.1", - "@types/istanbul-lib-coverage": "^2.0.6", - "collect-v8-coverage": "^1.0.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/test-sequencer": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.4.1.tgz", - "integrity": "sha512-PeYE+4td5rKjoRPxztObrXU+H8hsjZfxKMXOcmrr34JerSyB/ROOxbbicz8B7A5j9R9VayDnVPvBmedqCsFCdw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "30.4.1", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/transform": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.4.1.tgz", - "integrity": "sha512-Wz0LyktlTvRefoymh+n64hQ84KNXsRGcwdoZ8CSa0Ea+fgYcHZlnk+hDP7v2MS7il2bQ5uTEIxf4/NNfhMN4KQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/types": "30.4.1", - "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.1", - "chalk": "^4.1.2", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-util": "30.4.1", - "pirates": "^4.0.7", - "slash": "^3.0.0", - "write-file-atomic": "^5.0.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jest/types": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.4.1.tgz", - "integrity": "sha512-f1x/vJXIfjOlEmejYpbkbgw1gOqpPECwMvMEtBqe47j7H2Hg8h8w3o3ikhSXq3MI15kg+oQ0exWO0uCtTNJLoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/pattern": "30.4.0", - "@jest/schemas": "30.4.1", - "@types/istanbul-lib-coverage": "^2.0.6", - "@types/istanbul-reports": "^3.0.4", - "@types/node": "*", - "@types/yargs": "^17.0.33", - "chalk": "^4.1.2" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -1365,17 +361,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@js-sdsl/ordered-map": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", @@ -1387,28 +372,6 @@ "url": "https://opencollective.com/js-sdsl" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", - "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=23.5.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" - } - }, "node_modules/@opentelemetry/api": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", @@ -2292,28 +1255,14 @@ "node": ">=14" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@oxc-project/types": { + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", "dev": true, "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, - "node_modules/@pkgr/core": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", - "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, "funding": { - "url": "https://opencollective.com/pkgr" + "url": "https://github.com/sponsors/Boshen" } }, "node_modules/@protobufjs/aspromise": { @@ -2382,6 +1331,269 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, "node_modules/@sap-cloud-sdk/connectivity": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/connectivity/-/connectivity-4.8.0.tgz", @@ -2595,33 +1807,6 @@ "node": ">=18" } }, - "node_modules/@sinclair/typebox": { - "version": "0.34.52", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.52.tgz", - "integrity": "sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "15.4.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz", - "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, "node_modules/@so-ric/colorspace": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", @@ -2633,61 +1818,30 @@ "text-hex": "1.0.x" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } + "license": "MIT" }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" - } + "license": "MIT" }, "node_modules/@types/esrecurse": { "version": "4.3.1", @@ -2703,38 +1857,11 @@ "devOptional": true, "license": "MIT" }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "devOptional": true, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "devOptional": true, "license": "MIT" }, "node_modules/@types/node": { @@ -2747,13 +1874,6 @@ "undici-types": "~8.3.0" } }, - "node_modules/@types/stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -2761,718 +1881,619 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/yargs": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", - "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", "dependencies": { - "@types/yargs-parser": "*" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", - "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, - "license": "ISC" + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", - "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", - "cpu": [ - "arm" - ], + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", - "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", - "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", - "cpu": [ - "arm64" - ], + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", - "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", - "cpu": [ - "x64" - ], + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", - "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "peer": true, + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", - "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", - "cpu": [ - "arm" - ], - "dev": true, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "devOptional": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", - "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", - "cpu": [ - "arm" - ], - "dev": true, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "devOptional": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", - "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", - "cpu": [ - "arm64" - ], + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", - "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "devOptional": true, "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } }, - "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", - "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", - "cpu": [ - "loong64" - ], + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, - "node_modules/@unrs/resolver-binding-linux-loong64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", - "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", - "cpu": [ - "loong64" - ], + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "dev": true, - "libc": [ - "musl" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "safer-buffer": "~2.1.0" + } }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", - "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", - "cpu": [ - "ppc64" - ], + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=0.8" + } }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", - "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", - "cpu": [ - "riscv64" - ], + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">=12" + } }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", - "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", - "cpu": [ - "riscv64" - ], + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "MIT" }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", - "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", - "cpu": [ - "s390x" - ], + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "retry": "0.13.1" + } }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", - "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", - "cpu": [ - "x64" - ], + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "MIT" }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", - "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", - "cpu": [ - "x64" - ], + "node_modules/axios": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", + "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", "dev": true, - "libc": [ - "musl" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } }, - "node_modules/@unrs/resolver-binding-openharmony-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", - "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "devOptional": true, "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] + "engines": { + "node": "18 || 20 || >=22" + } }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", - "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", - "cpu": [ - "wasm32" - ], - "dev": true, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", - "optional": true, + "peer": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", - "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", - "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", - "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/accepts": { + "node_modules/body-parser/node_modules/content-type": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "license": "MIT", "peer": true, - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/acorn": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", - "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "devOptional": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=0.4.0" + "node": "20 || >=22" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "devOptional": true, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", - "dependencies": { - "debug": "4" - }, + "peer": true, "engines": { - "node": ">= 6.0.0" + "node": ">= 0.8" } }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "devOptional": true, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">= 0.4" } }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", + "peer": true, "dependencies": { - "type-fest": "^0.21.3" + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { - "node": ">=8" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/ansi-regex": { + "node_modules/chai": { "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=18" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/chai-as-promised": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-8.0.2.tgz", + "integrity": "sha512-1GadL+sEJVLzDjcawPM4kjfnL+p/9vrxiEUonowKOAzvVg0PixJUdtuDzdkDeQhK3zfOE76GqGkZIQ7/Adcrqw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "check-error": "^2.1.1" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "peerDependencies": { + "chai": ">= 2.1.2 < 7" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, + "license": "MIT", + "peer": true, "engines": { - "node": ">= 8" + "node": ">= 16" } }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "engines": { + "node": ">=12" } }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" + "engines": { + "node": ">=8" } }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { - "safer-buffer": "~2.1.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, "engines": { - "node": ">=0.8" + "node": ">=8" } }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "dev": true, - "license": "MIT" - }, - "node_modules/async-retry": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { - "retry": "0.13.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", - "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", "dev": true, "license": "MIT", - "dependencies": { - "follow-redirects": "^1.16.0", - "form-data": "^4.0.6", - "https-proxy-agent": "^5.0.1", - "proxy-from-env": "^2.1.0" + "engines": { + "node": ">=0.8" } }, - "node_modules/babel-jest": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.4.1.tgz", - "integrity": "sha512-fATAbM8piYxkiXQp3RBXmZHxZVNJZAVXXfyeyCN2Tida3+qJ8ea9UxhiJ2y4fLO90ZImKt6k9FlcH2+rLkJGhw==", + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "30.4.1", - "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.1", - "babel-preset-jest": "30.4.0", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "slash": "^3.0.0" + "color-convert": "^3.1.3", + "color-string": "^2.1.3" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-0" + "node": ">=18" } }, - "node_modules/babel-plugin-istanbul": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", - "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "license": "BSD-3-Clause", - "workspaces": [ - "test/babel-8" - ], + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-instrument": "^6.0.2", - "test-exclude": "^6.0.0" + "color-name": "~1.1.4" }, "engines": { - "node": ">=12" + "node": ">=7.0.0" } }, - "node_modules/babel-plugin-jest-hoist": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.4.0.tgz", - "integrity": "sha512-9EdtWM/sSfXLOGLwSn+GS6pIXyBnL07/8gyJlwFXjWy4DxMOyItqyUT29d4lQiS380EZwYlX7/At4PgBS+m2aA==", + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", "dev": true, "license": "MIT", "dependencies": { - "@types/babel__core": "^7.20.5" + "color-name": "^2.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=18" } }, - "node_modules/babel-preset-current-node-syntax": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", - "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5" - }, - "peerDependencies": { - "@babel/core": "^7.0.0 || ^8.0.0-0" + "node_modules/color-string/node_modules/color-name": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" } }, - "node_modules/babel-preset-jest": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.4.0.tgz", - "integrity": "sha512-lBY4jxsNmCnSiu7kquw8ZC9F4+XLMOKypT3RnNHPvU2Kpd4W0xaPuLr5ZkRyOsvLYAY4yaW1ZwTW4xB7NIiZzg==", + "node_modules/color/node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "30.4.0", - "babel-preset-current-node-syntax": "^1.2.0" + "color-name": "^2.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + "node": ">=14.6" } }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "devOptional": true, + "node_modules/color/node_modules/color-name": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", + "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", + "dev": true, "license": "MIT", "engines": { - "node": "18 || 20 || >=22" + "node": ">=12.20" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.11.12", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", - "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" }, "engines": { - "node": ">=6.0.0" + "node": ">= 0.8" } }, - "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", "peer": true, - "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" - }, "engines": { "node": ">=18" }, @@ -3481,1445 +2502,254 @@ "url": "https://opencollective.com/express" } }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", "peer": true, "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.6" } }, - "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", - "devOptional": true, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, + "peer": true, "engines": { - "node": "20 || >=22" + "node": ">= 0.6" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, + "peer": true, "engines": { - "node": ">=8" + "node": ">=6.6.0" } }, - "node_modules/browserslist": { - "version": "4.28.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", - "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.44", - "caniuse-lite": "^1.0.30001806", - "electron-to-chromium": "^1.5.393", - "node-releases": "^2.0.51", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "dev": true, "license": "MIT" }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "devOptional": true, "license": "MIT", "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, "engines": { - "node": ">= 0.4" + "node": ">= 8" } }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", - "peer": true, "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" + "ms": "^2.1.3" }, "engines": { - "node": ">= 0.4" + "node": ">=6.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=0.4.0" } }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", + "peer": true, "engines": { - "node": ">=6" + "node": ">= 0.8" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001806", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", - "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "dev": true, - "license": "MIT", - "peer": true, + "license": "BSD-2-Clause", "engines": { - "node": ">=18" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, - "node_modules/chai-as-promised": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-8.0.2.tgz", - "integrity": "sha512-1GadL+sEJVLzDjcawPM4kjfnL+p/9vrxiEUonowKOAzvVg0PixJUdtuDzdkDeQhK3zfOE76GqGkZIQ7/Adcrqw==", - "dev": true, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", - "peer": true, "dependencies": { - "check-error": "^2.1.1" + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" }, - "peerDependencies": { - "chai": ">= 2.1.2 < 7" + "engines": { + "node": ">= 0.4" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "safe-buffer": "^5.0.1" } }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT", + "peer": true + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", + "peer": true, "engines": { - "node": ">=10" + "node": ">= 0.8" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", - "peer": true, "engines": { - "node": ">= 16" + "node": ">= 0.4" } }, - "node_modules/ci-info": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", - "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.4" } }, - "node_modules/cjs-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", - "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "license": "MIT" }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, - "license": "ISC", + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "es-errors": "^1.3.0" }, "engines": { - "node": ">=12" + "node": ">= 0.4" } }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, - "node_modules/collect-v8-coverage": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", - "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", - "dev": true, - "license": "MIT" - }, - "node_modules/color": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", - "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^3.1.3", - "color-string": "^2.1.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-string": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", - "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/color-string/node_modules/color-name": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", - "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", - "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=14.6" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", - "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.6.0" - } - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/dedent": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.2.tgz", - "integrity": "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "babel-plugin-macros": "^3.1.0" - }, - "peerDependenciesMeta": { - "babel-plugin-macros": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT", - "peer": true - }, - "node_modules/electron-to-chromium": { - "version": "1.5.400", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.400.tgz", - "integrity": "sha512-96EWDNjM59SYflgeV5Ylsf4EMiq1a25YjCnJH7cxn/AF2H3pILRweaUnoLax0yKHWdpOzY6JKEu45e8irqZIHA==", - "dev": true, - "license": "ISC" - }, - "node_modules/emittery": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", - "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sindresorhus/emittery?sponsor=1" - } - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", - "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", - "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT", - "peer": true - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", - "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", - "devOptional": true, - "license": "MIT", - "workspaces": [ - "packages/*" - ], - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.7.0", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.2", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "devOptional": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "devOptional": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "devOptional": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "devOptional": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "devOptional": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "devOptional": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "devOptional": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/execa/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/exit-x": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", - "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/expect": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.4.1.tgz", - "integrity": "sha512-PMARsyh/JtqC20HoGqlFcIlQAyqUtW4PlI1rup1uhYJtKuwAjbvWi3GQMAn+STdHum/dk8xrKfUM1+5SAwpolA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/expect-utils": "30.4.1", - "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.4.1", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-util": "30.4.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "peer": true, - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "bser": "2.1.1" - } - }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", - "dev": true, - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" - }, - "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", - "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", - "devOptional": true, - "license": "ISC" - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", - "dev": true, - "license": "MIT" - }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/form-data": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", - "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/forwarded-parse": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", - "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", - "license": "MIT" - }, - "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.0.0" + "node": ">=6" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } + "peer": true }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "devOptional": true, "license": "MIT", "engines": { "node": ">=10" @@ -4928,1096 +2758,801 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/eslint": { + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", "devOptional": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", - "dev": true, "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" }, - "engines": { - "node": ">=16 || 14 >=14.17" + "bin": { + "eslint": "bin/eslint.js" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", "engines": { - "node": ">= 0.4" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", - "dev": true, - "license": "MIT", + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "devOptional": true, + "license": "BSD-2-Clause", "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" - }, - "bin": { - "handlebars": "bin/handlebars" + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=0.4.7" + "node": "^20.19.0 || ^22.13.0 || >=24" }, - "optionalDependencies": { - "uglify-js": "^3.1.4" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "devOptional": true, + "license": "Apache-2.0", "engines": { - "node": ">= 0.4" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "devOptional": true, + "license": "BSD-2-Clause", "dependencies": { - "has-symbols": "^1.0.3" + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": ">= 0.4" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://opencollective.com/eslint" } }, - "node_modules/hasown": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", - "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", - "license": "MIT", + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "devOptional": true, + "license": "BSD-3-Clause", "dependencies": { - "function-bind": "^1.1.2" + "estraverse": "^5.1.0" }, "engines": { - "node": ">= 0.4" + "node": ">=0.10" } }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "peer": true, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "devOptional": true, + "license": "BSD-2-Clause", "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" + "estraverse": "^5.2.0" }, "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">=4.0" } }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "devOptional": true, + "license": "BSD-2-Clause", "engines": { - "node": ">= 6" + "node": ">=4.0" } }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", - "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "license": "MIT", - "peer": true, "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "devOptional": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "devOptional": true, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", + "peer": true, "engines": { - "node": ">= 4" + "node": ">= 0.6" } }, - "node_modules/import-in-the-middle": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.3.tgz", - "integrity": "sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==", + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, "license": "Apache-2.0", - "dependencies": { - "cjs-module-lexer": "^2.2.0", - "es-module-lexer": "^2.2.0", - "module-details-from-path": "^1.0.4" - }, "engines": { - "node": ">=18" + "node": ">=12.0.0" } }, - "node_modules/import-local": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", - "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", - "dev": true, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">=8" + "node": ">= 18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } + "license": "MIT" }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "devOptional": true, + "license": "MIT" }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "devOptional": true, + "license": "MIT" }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "license": "MIT", - "peer": true, "engines": { - "node": ">= 0.10" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "dev": true, "license": "MIT" }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "devOptional": true, "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=16.0.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, "engines": { "node": ">=8" } }, - "node_modules/is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", + "peer": true, + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, "engines": { - "node": ">=6" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "devOptional": true, "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT", - "peer": true - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "devOptional": true, "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=16" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "devOptional": true, "license": "ISC" }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, - "license": "BSD-3-Clause", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } } }, - "node_modules/istanbul-lib-instrument": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", - "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "@babel/core": "^7.23.9", - "@babel/parser": "^7.23.9", - "@istanbuljs/schema": "^0.1.3", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^7.5.4" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { - "node": ">=10" + "node": ">= 6" } }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">= 0.6" } }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" + "mime-db": "1.52.0" }, "engines": { - "node": ">=10" + "node": ">= 0.6" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "peer": true, "engines": { - "node": ">=10" + "node": ">= 0.6" } }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } + "node_modules/forwarded-parse": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", + "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", + "license": "MIT" }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.8" } }, - "node_modules/jest": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.4.2.tgz", - "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "@jest/core": "30.4.2", - "@jest/types": "30.4.1", - "import-local": "^3.2.0", - "jest-cli": "30.4.2" - }, - "bin": { - "jest": "bin/jest.js" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/jest-changed-files": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.4.1.tgz", - "integrity": "sha512-IuctmYrxi21iOSOaIXpJWalHyPAsVv0GeBHKDn8C1CA4W5htHn7INL+wdnL4Bo0+olEndvAFkmb++tIQJG+vvg==", - "dev": true, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", - "dependencies": { - "execa": "^5.1.1", - "jest-util": "30.4.1", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-circus": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.4.2.tgz", - "integrity": "sha512-rvHH7VlY6LgbJXJTQ87GW62g1FntOtbhh0zT+v04kC+pgL6aBKyYINXxWukCpj3dcIBMw5/XUbtDS9dU9JTXeQ==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.4.1", - "@jest/expect": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "chalk": "^4.1.2", - "co": "^4.6.0", - "dedent": "^1.6.0", - "is-generator-fn": "^2.1.0", - "jest-each": "30.4.1", - "jest-matcher-utils": "30.4.1", - "jest-message-util": "30.4.1", - "jest-runtime": "30.4.2", - "jest-snapshot": "30.4.1", - "jest-util": "30.4.1", - "p-limit": "^3.1.0", - "pretty-format": "30.4.1", - "pure-rand": "^7.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" - }, + "license": "ISC", "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/jest-cli": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.4.2.tgz", - "integrity": "sha512-jfA2ocvVHMXS2QijrJ0d31ektP+d/W0T5RpcTX2Pq+3sVqHlsXVCM2+FmwpL+bdY8OfHpIg9xMxLF17Zg0U49Q==", - "dev": true, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { - "@jest/core": "30.4.2", - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", - "chalk": "^4.1.2", - "exit-x": "^0.2.2", - "import-local": "^3.2.0", - "jest-config": "30.4.2", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "yargs": "^17.7.2" - }, - "bin": { - "jest": "bin/jest.js" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/jest-config": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.4.2.tgz", - "integrity": "sha512-rNHAShJQqQwFNoL0hbf3BphSBOWnpOUAKvidLS/AjNVLPfoj5mSf4jQMfW3cYOs6hXeZC7nF7mDHaBnbxELOzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@jest/get-type": "30.1.0", - "@jest/pattern": "30.4.0", - "@jest/test-sequencer": "30.4.1", - "@jest/types": "30.4.1", - "babel-jest": "30.4.1", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "deepmerge": "^4.3.1", - "glob": "^10.5.0", - "graceful-fs": "^4.2.11", - "jest-circus": "30.4.2", - "jest-docblock": "30.4.0", - "jest-environment-node": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-runner": "30.4.2", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "parse-json": "^5.2.0", - "pretty-format": "30.4.1", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "esbuild-register": ">=3.4.0", - "ts-node": ">=9.0.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "esbuild-register": { - "optional": true - }, - "ts-node": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-diff": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.4.1.tgz", - "integrity": "sha512-CRpFK0RtLriVDGcPPAnR6HMVI8bSR2jnUIgralhauzYQZIb4RH9AtEInTuQr65LmmGggGcRT6HIASxwqsVsmlA==", - "dev": true, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { - "@jest/diff-sequences": "30.4.0", - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "pretty-format": "30.4.1" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.4" } }, - "node_modules/jest-docblock": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.4.0.tgz", - "integrity": "sha512-ZPMabUZCx5MpbZ2eBYSvZ0J8fvo3dR9oM+eeUpb3aKNQFuS2tu3Duw1TNlMoP8k3WQgKGJuhcMFvwcVuq6T7oA==", - "dev": true, - "license": "MIT", + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "devOptional": true, + "license": "ISC", "dependencies": { - "detect-newline": "^3.1.0" + "is-glob": "^4.0.3" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=10.13.0" } }, - "node_modules/jest-each": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.4.1.tgz", - "integrity": "sha512-/8MJbH6fuj48TstjrMf+u/pd06Qezz5xOXvZA6442heNOWr8bdeoGZX2d9fCn028CoMgYmroH9//zky5GfyYmA==", - "dev": true, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.4.1", - "chalk": "^4.1.2", - "jest-util": "30.4.1", - "pretty-format": "30.4.1" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-environment-node": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.4.1.tgz", - "integrity": "sha512-4FZYVOk85hz2AyT6BbarKy9u37g6DbrDyCdFhsnDdXqyrueYQvB+0zO4f/kqLCRD0BsPRXPMNJeQwihKZV8naw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "30.4.1", - "@jest/fake-timers": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "jest-mock": "30.4.1", - "jest-util": "30.4.1", - "jest-validate": "30.4.1" + "node": ">= 0.4" }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-haste-map": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.4.1.tgz", - "integrity": "sha512-rFrcONd8jeFsyw+Z9CrScJgglRf2+NFmNam8dKu7n+SoHqNYT47mn0DdEcVUZJpvh7Iz6/si7f7yUH7GJHVgnw==", + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.4.1", - "@types/node": "*", - "anymatch": "^3.1.3", - "fb-watchman": "^2.0.2", - "graceful-fs": "^4.2.11", - "jest-regex-util": "30.4.0", - "jest-util": "30.4.1", - "jest-worker": "30.4.1", - "picomatch": "^4.0.3", - "walker": "^1.0.8" + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=0.4.7" }, "optionalDependencies": { - "fsevents": "^2.3.3" + "uglify-js": "^3.1.4" } }, - "node_modules/jest-leak-detector": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.4.1.tgz", - "integrity": "sha512-IpmyiioeHxiWDhesHnUFmOxcTzwCwKpgACgWajtAP+nYQXiY7DakTxB6Bx9JFiRMljr0AX1PvnQdaU1KFoz6NQ==", - "dev": true, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "pretty-format": "30.4.1" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-matcher-utils": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.4.1.tgz", - "integrity": "sha512-zvYfX5CaeEkFrrLS9suWe9rvJrm9J1Iv3ua8kIBv9GEPzcnsfBf0bob37la7s67fs0nlBC3EuvkOLnXQKxtx4A==", + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/get-type": "30.1.0", - "chalk": "^4.1.2", - "jest-diff": "30.4.1", - "pretty-format": "30.4.1" + "has-symbols": "^1.0.3" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/jest-message-util": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.4.1.tgz", - "integrity": "sha512-kwCKIvq0MCW1HzLoGola9Te6JUdzgV0loyKJ3Qghrkz9i5/RRIHsL95BMQc2HBBhlBKC4j22K9p11TGHH8RBpQ==", - "dev": true, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@jest/types": "30.4.1", - "@types/stack-utils": "^2.0.3", - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-util": "30.4.1", - "picomatch": "^4.0.3", - "pretty-format": "30.4.1", - "slash": "^3.0.0", - "stack-utils": "^2.0.6" + "function-bind": "^1.1.2" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.4" } }, - "node_modules/jest-mock": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.4.1.tgz", - "integrity": "sha512-/i8SVb8/NSB7RfNi8gfqu8gxLV23KaL5EpAttyb9iz8qWRIqXRLflycz/32wXsYkOnaUlx8NAKnJYtpsmXUmfw==", - "dev": true, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", + "peer": true, "dependencies": { - "@jest/types": "30.4.1", - "@types/node": "*", - "jest-util": "30.4.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-pnp-resolver": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", - "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "peerDependencies": { - "jest-resolve": "*" + "node": ">= 0.8" }, - "peerDependenciesMeta": { - "jest-resolve": { - "optional": true - } - } - }, - "node_modules/jest-regex-util": { - "version": "30.4.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.4.0.tgz", - "integrity": "sha512-mWlvLviKIgIQ8VCuM1xRdD0TWp3zlzionlmDBjuXVBs+VkmXq6FgW9T4Emr7oGz/Rk6feDCGyiugolcQEyp3mg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/jest-resolve": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.4.1.tgz", - "integrity": "sha512-Zry8Yq/yJcNAZ7dJ5F2heic8AheXvbFZ7XI5V+h28nrYZ7Qoyy4dItq8OodjnYD270mvX+ZudmrNV9cysqhW5Q==", + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^4.1.2", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.4.1", - "jest-validate": "30.4.1", - "slash": "^3.0.0", - "unrs-resolver": "^1.7.11" + "agent-base": "6", + "debug": "4" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 6" } }, - "node_modules/jest-resolve-dependencies": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.4.2.tgz", - "integrity": "sha512-gDiVh1I+GxYzz9oXlyw+1wv6VOYX1WYxMOfjsA3iGKePV2oxmbHhwxfkALxNxYy1ciw6APWwkW2zZONwP97aEQ==", - "dev": true, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", "license": "MIT", + "peer": true, "dependencies": { - "jest-regex-util": "30.4.0", - "jest-snapshot": "30.4.1" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/jest-runner": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.4.2.tgz", - "integrity": "sha512-2dw0PslVYXxffXGpLo+Ejad+KcI1Qkjn7f4X4619gf21oCUmL+SPfjqIa/losUem3yEOvfNZe/F1HWUcNpODcg==", - "dev": true, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "devOptional": true, "license": "MIT", - "dependencies": { - "@jest/console": "30.4.1", - "@jest/environment": "30.4.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "exit-x": "^0.2.2", - "graceful-fs": "^4.2.11", - "jest-docblock": "30.4.0", - "jest-environment-node": "30.4.1", - "jest-haste-map": "30.4.1", - "jest-leak-detector": "30.4.1", - "jest-message-util": "30.4.1", - "jest-resolve": "30.4.1", - "jest-runtime": "30.4.2", - "jest-util": "30.4.1", - "jest-watcher": "30.4.1", - "jest-worker": "30.4.1", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 4" } }, - "node_modules/jest-runtime": { - "version": "30.4.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.4.2.tgz", - "integrity": "sha512-3/5e8iPz2k/VLqlr8DgTftYyLUv8Su3FkCAO2/Od81UsUTpSxOrS6O5x5KkoQwyUjmpYyDJKeyAvg2T2nvpNkQ==", - "dev": true, - "license": "MIT", + "node_modules/import-in-the-middle": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.3.tgz", + "integrity": "sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==", + "license": "Apache-2.0", "dependencies": { - "@jest/environment": "30.4.1", - "@jest/fake-timers": "30.4.1", - "@jest/globals": "30.4.1", - "@jest/source-map": "30.0.1", - "@jest/test-result": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "chalk": "^4.1.2", - "cjs-module-lexer": "^2.1.0", - "collect-v8-coverage": "^1.0.2", - "glob": "^10.5.0", - "graceful-fs": "^4.2.11", - "jest-haste-map": "30.4.1", - "jest-message-util": "30.4.1", - "jest-mock": "30.4.1", - "jest-regex-util": "30.4.0", - "jest-resolve": "30.4.1", - "jest-snapshot": "30.4.1", - "jest-util": "30.4.1", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" + "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", + "module-details-from-path": "^1.0.4" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=18" } }, - "node_modules/jest-snapshot": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.4.1.tgz", - "integrity": "sha512-tEOkkfOMppUyeiHwjZswOQ3lcnoTnws/q5FnGIaeIh/jmoU0ZlgMYRR8sTlTj+nNGCoJ0RDq6SfxGxCsyMTPmw==", - "dev": true, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "devOptional": true, "license": "MIT", - "dependencies": { - "@babel/core": "^7.27.4", - "@babel/generator": "^7.27.5", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1", - "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.4.1", - "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.4.1", - "@jest/transform": "30.4.1", - "@jest/types": "30.4.1", - "babel-preset-current-node-syntax": "^1.2.0", - "chalk": "^4.1.2", - "expect": "30.4.1", - "graceful-fs": "^4.2.11", - "jest-diff": "30.4.1", - "jest-matcher-utils": "30.4.1", - "jest-message-util": "30.4.1", - "jest-util": "30.4.1", - "pretty-format": "30.4.1", - "semver": "^7.7.2", - "synckit": "^0.11.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, "engines": { - "node": ">=10" + "node": ">=0.8.19" } }, - "node_modules/jest-util": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.4.1.tgz", - "integrity": "sha512-vjQb1sACEiv13DKJMDToJpzVW0joCsIQrmbg0fi7CyOOt+g9jTuQl2A216pWRBYhOVt53XbL/2LbMKg1BECWOw==", - "dev": true, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "license": "MIT", - "dependencies": { - "@jest/types": "30.4.1", - "@types/node": "*", - "chalk": "^4.1.2", - "ci-info": "^4.2.0", - "graceful-fs": "^4.2.11", - "picomatch": "^4.0.3" - }, + "peer": true, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">= 0.10" } }, - "node_modules/jest-validate": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.4.1.tgz", - "integrity": "sha512-PDWi4SOwLnwqNDfHZjOcsEFyZ4fc/2W2gVL3DEoyqnB6jCQMLRtfBong8s6omIw3lI0HWOus12xfnFmQtjW3fw==", - "dev": true, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "devOptional": true, "license": "MIT", - "dependencies": { - "@jest/get-type": "30.1.0", - "@jest/types": "30.4.1", - "camelcase": "^6.3.0", - "chalk": "^4.1.2", - "leven": "^3.1.0", - "pretty-format": "30.4.1" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=0.10.0" } }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/jest-watcher": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.4.1.tgz", - "integrity": "sha512-/l9UonmvCwjHH7d2h3iAwIloLc1H0S8mJZ/LNK3i86hqwPAz8otUJjP9MfYtz9Tt77Su5FD2xGjZn8d31IZHlw==", - "dev": true, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "devOptional": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.4.1", - "@jest/types": "30.4.1", - "@types/node": "*", - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "emittery": "^0.13.1", - "jest-util": "30.4.1", - "string-length": "^4.0.2" + "is-extglob": "^2.1.1" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=0.10.0" } }, - "node_modules/jest-worker": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.4.1.tgz", - "integrity": "sha512-SHynN/q/QD++iNyvMdy+WMmbCGk8jIsNcRxycXbWubSOhvo6T+j2afcfUSl+3hYsiBebOTo0cT7c2H7CXugu1g==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", - "dependencies": { - "@types/node": "*", - "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.4.1", - "merge-stream": "^2.0.0", - "supports-color": "^8.1.1" - }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + "node": ">=0.12.0" } }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT", + "peer": true + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "devOptional": true, + "license": "ISC" + }, "node_modules/jks-js": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/jks-js/-/jks-js-1.1.7.tgz", @@ -6030,40 +3565,6 @@ "node-rsa": "^1.1.1" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "3.15.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", - "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -6071,13 +3572,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -6092,19 +3586,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/jsonwebtoken": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", @@ -6191,16 +3672,6 @@ "dev": true, "license": "MIT" }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6215,13 +3686,279 @@ "node": ">= 0.8.0" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -6319,53 +4056,14 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tmpl": "1.0.5" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/math-intrinsics": { @@ -6404,13 +4102,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -6465,16 +4156,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -6501,16 +4182,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/module-details-from-path": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", @@ -6523,20 +4194,23 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/napi-postinstall": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "node_modules/nanoid": { + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "bin": { - "napi-postinstall": "lib/cli.js" + "nanoid": "bin/nanoid.cjs" }, "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, "node_modules/natural-compare": { @@ -6593,16 +4267,6 @@ "dev": true, "license": "MIT" }, - "node_modules/node-releases": { - "version": "2.0.52", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.52.tgz", - "integrity": "sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/node-rsa": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-1.1.1.tgz", @@ -6613,29 +4277,6 @@ "asn1": "^0.2.4" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -6649,6 +4290,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -6667,6 +4322,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", + "peer": true, "dependencies": { "wrappy": "1" } @@ -6681,22 +4337,6 @@ "fn.name": "1.x.x" } }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/opossum": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/opossum/-/opossum-10.0.0.tgz", @@ -6757,42 +4397,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -6813,16 +4417,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6833,30 +4427,6 @@ "node": ">=8" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, "node_modules/path-to-regexp": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", @@ -6868,6 +4438,13 @@ "url": "https://opencollective.com/express" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6888,83 +4465,33 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pirates": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", - "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pkg-dir/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/pkg-dir/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=8" + "node": "^10 || ^12 || >=14" } }, "node_modules/prelude-ls": { @@ -6977,35 +4504,6 @@ "node": ">= 0.8.0" } }, - "node_modules/pretty-format": { - "version": "30.4.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.4.1.tgz", - "integrity": "sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "30.4.1", - "ansi-styles": "^5.2.0", - "react-is-18": "npm:react-is@^18.3.1", - "react-is-19": "npm:react-is@^19.2.5" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/protobufjs": { "version": "7.6.5", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", @@ -7064,23 +4562,6 @@ "node": ">=6" } }, - "node_modules/pure-rand": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", - "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, "node_modules/qs": { "version": "6.15.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", @@ -7128,22 +4609,6 @@ "node": ">= 0.10" } }, - "node_modules/react-is-18": { - "name": "react-is", - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-is-19": { - "name": "react-is", - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", - "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", - "dev": true, - "license": "MIT" - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -7182,29 +4647,6 @@ "node": ">=9.3.0 || >=8.10.0 <9.0.0" } }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/retry": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", @@ -7212,7 +4654,40 @@ "dev": true, "license": "MIT", "engines": { - "node": ">= 4" + "node": ">= 4" + } + }, + "node_modules/rolldown": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.143.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.3", + "@rolldown/binding-darwin-arm64": "1.2.3", + "@rolldown/binding-darwin-x64": "1.2.3", + "@rolldown/binding-freebsd-x64": "1.2.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", + "@rolldown/binding-linux-arm64-gnu": "1.2.3", + "@rolldown/binding-linux-arm64-musl": "1.2.3", + "@rolldown/binding-linux-ppc64-gnu": "1.2.3", + "@rolldown/binding-linux-s390x-gnu": "1.2.3", + "@rolldown/binding-linux-x64-gnu": "1.2.3", + "@rolldown/binding-linux-x64-musl": "1.2.3", + "@rolldown/binding-openharmony-arm64": "1.2.3", + "@rolldown/binding-win32-arm64-msvc": "1.2.3", + "@rolldown/binding-win32-x64-msvc": "1.2.3" } }, "node_modules/router": { @@ -7269,16 +4744,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -7432,28 +4897,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "ISC" }, "node_modules/source-map": { "version": "0.6.1", @@ -7465,24 +4914,16 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.13", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", - "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true, - "license": "BSD-3-Clause" - }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -7493,28 +4934,12 @@ "node": "*" } }, - "node_modules/stack-utils": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", - "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "MIT" }, "node_modules/statuses": { "version": "2.0.2", @@ -7526,6 +4951,13 @@ "node": ">= 0.8" } }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -7536,209 +4968,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/string-length/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-length/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/synckit": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", - "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@pkgr/core": "^0.3.6" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/synckit" - } - }, "node_modules/systeminformation": { "version": "5.33.1", "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.1.tgz", @@ -7766,87 +4995,56 @@ "url": "https://www.buymeacoffee.com/systeminfo" } }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/test-exclude/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, "license": "MIT" }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=18" } }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { - "node": "*" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", - "dev": true, - "license": "MIT" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, - "license": "BSD-3-Clause" + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -7881,14 +5079,6 @@ "node": ">= 14.0.0" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7902,29 +5092,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", @@ -7989,75 +5156,6 @@ "node": ">= 0.8" } }, - "node_modules/unrs-resolver": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", - "integrity": "sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "napi-postinstall": "^0.3.4" - }, - "funding": { - "url": "https://opencollective.com/unrs-resolver" - }, - "optionalDependencies": { - "@unrs/resolver-binding-android-arm-eabi": "1.12.2", - "@unrs/resolver-binding-android-arm64": "1.12.2", - "@unrs/resolver-binding-darwin-arm64": "1.12.2", - "@unrs/resolver-binding-darwin-x64": "1.12.2", - "@unrs/resolver-binding-freebsd-x64": "1.12.2", - "@unrs/resolver-binding-linux-arm-gnueabihf": "1.12.2", - "@unrs/resolver-binding-linux-arm-musleabihf": "1.12.2", - "@unrs/resolver-binding-linux-arm64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-arm64-musl": "1.12.2", - "@unrs/resolver-binding-linux-loong64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-loong64-musl": "1.12.2", - "@unrs/resolver-binding-linux-ppc64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-riscv64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-riscv64-musl": "1.12.2", - "@unrs/resolver-binding-linux-s390x-gnu": "1.12.2", - "@unrs/resolver-binding-linux-x64-gnu": "1.12.2", - "@unrs/resolver-binding-linux-x64-musl": "1.12.2", - "@unrs/resolver-binding-openharmony-arm64": "1.12.2", - "@unrs/resolver-binding-wasm32-wasi": "1.12.2", - "@unrs/resolver-binding-win32-arm64-msvc": "1.12.2", - "@unrs/resolver-binding-win32-ia32-msvc": "1.12.2", - "@unrs/resolver-binding-win32-x64-msvc": "1.12.2" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -8075,21 +5173,6 @@ "dev": true, "license": "MIT" }, - "node_modules/v8-to-istanbul": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", - "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", - "dev": true, - "license": "ISC", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.12", - "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^2.0.0" - }, - "engines": { - "node": ">=10.12.0" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -8115,6 +5198,174 @@ "node": ">=0.6.0" } }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "node_modules/voca": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/voca/-/voca-1.4.1.tgz", @@ -8122,16 +5373,6 @@ "dev": true, "license": "MIT" }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "makeerror": "1.0.12" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8148,6 +5389,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/winston": { "version": "3.19.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", @@ -8203,120 +5461,12 @@ "dev": true, "license": "MIT" }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", - "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", - "dev": true, "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } + "peer": true }, "node_modules/y18n": { "version": "5.0.8", @@ -8328,13 +5478,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", diff --git a/package.json b/package.json index d9c2d5a2..22f2968f 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ ], "scripts": { "lint": "npx eslint . --max-warnings=0", - "test": "npx jest --silent" + "test": "vitest run --silent" }, "dependencies": { "@opentelemetry/api": "^1.9", @@ -47,7 +47,7 @@ "@sap/cds-mtxs": "^4", "axios": "^1.6.7", "eslint": "^10", - "jest": "^30.4.2" + "vitest": "^4" }, "cds": { "requires": { diff --git a/test/bookshop/package.json b/test/bookshop/package.json index bf574fe9..7cabf2be 100644 --- a/test/bookshop/package.json +++ b/test/bookshop/package.json @@ -22,7 +22,9 @@ "instrumentations": { "http": { "config": { - "ignoreIncomingRequestHook": "./lib/MyIgnoreIncomingRequestHook.js" + "ignoreIncomingRequestHook": "./lib/MyIgnoreIncomingRequestHook.js", + "disableIncomingRequestInstrumentation": true, + "disableOutgoingRequestInstrumentation": true } } }, diff --git a/test/logging.test.js b/test/logging.test.js index 614b83dc..db8d1298 100644 --- a/test/logging.test.js +++ b/test/logging.test.js @@ -6,12 +6,19 @@ process.env.cds_log = JSON.stringify({ cls_custom_fields: ['foo'] }) const cds = require('@sap/cds') const { expect, GET } = cds.test(__dirname + '/bookshop', '--profile', 'logging') +const wait = require('node:timers/promises').setTimeout + describe('logging', () => { const admin = { auth: { username: 'alice' } } const { dir } = console + // The queue's SchedulingService runs an initial outbox scan on server "listening"; its + // telemetry "elapsed times:" trace primer is exported asynchronously and would otherwise + // land in the spy window below. Drain it once up front before installing the spy. + // REVISIT: replace this fixed wait by polling for the primer / an in-memory exporter (see #478). + beforeAll(() => wait(500)) beforeEach(() => { - console.dir = jest.fn() + console.dir = vi.fn() }) afterAll(() => { console.dir = dir diff --git a/test/metrics-outbox-disabled.test.js b/test/metrics-outbox-disabled.test.js index 6864cc64..d1fe5bb1 100644 --- a/test/metrics-outbox-disabled.test.js +++ b/test/metrics-outbox-disabled.test.js @@ -1,6 +1,7 @@ +import { vi } from 'vitest' // Mock console.dir to capture logs ConsoleMetricExporter writes const consoleDirLogs = [] -jest.spyOn(console, 'dir').mockImplementation((...args) => { +vi.spyOn(console, 'dir').mockImplementation((...args) => { consoleDirLogs.push(args) }) @@ -49,4 +50,4 @@ describe('queue metrics is disabled', () => { expect(metricValue('med_storage_time_in_seconds')).to.eq(null) expect(metricValue('max_storage_time_in_seconds')).to.eq(null) }) -}) +}) \ No newline at end of file diff --git a/test/metrics-outbox-multitenant.test.js b/test/metrics-outbox-multitenant.test.js index 41b46ee0..485a3380 100644 --- a/test/metrics-outbox-multitenant.test.js +++ b/test/metrics-outbox-multitenant.test.js @@ -1,6 +1,7 @@ +import { vi } from 'vitest' // Mock console.dir to capture logs ConsoleMetricExporter writes const consoleDirLogs = [] -jest.spyOn(console, 'dir').mockImplementation((...args) => { +vi.spyOn(console, 'dir').mockImplementation((...args) => { consoleDirLogs.push(args) }) @@ -221,4 +222,4 @@ describe('queue metrics for multi tenant service', () => { expect(metricValue(T2, 'remaining_entries')).to.eq(0) }) }) -}) +}) \ No newline at end of file diff --git a/test/metrics-outbox.test.js b/test/metrics-outbox.test.js index f87c3c67..68b9e66c 100644 --- a/test/metrics-outbox.test.js +++ b/test/metrics-outbox.test.js @@ -1,6 +1,7 @@ +import { vi } from 'vitest' // Mock console.dir to capture logs ConsoleMetricExporter writes const consoleDirLogs = [] -jest.spyOn(console, 'dir').mockImplementation((...args) => { +vi.spyOn(console, 'dir').mockImplementation((...args) => { consoleDirLogs.push(args) }) @@ -27,7 +28,7 @@ function metricValue(metric, queuedServiceName) { return mestRecentQueueMetricData.value } -const debugLog = (cds.log('telemetry').debug = jest.fn(() => {})) +const debugLog = (cds.log('telemetry').debug = vi.fn(() => {})) describe('queue metrics for single tenant service', () => { let totalInc = { [E1]: 0, [E2]: 0 } @@ -293,4 +294,4 @@ describe('queue metrics for single tenant service', () => { }) }) }) -}) +}) \ No newline at end of file diff --git a/test/tracing-attributes.test.js b/test/tracing-attributes.test.js index 03204b13..c1497b06 100644 --- a/test/tracing-attributes.test.js +++ b/test/tracing-attributes.test.js @@ -24,20 +24,21 @@ describe('tracing attributes', () => { describe('remote', () => { let server, port - beforeAll(done => { - server = http.createServer((req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }) - res.end(JSON.stringify({ value: [] })) - }) - server.listen(0, () => { - port = server.address().port - done() - }) - }) - - afterAll(done => { - server.close(done) - }) + beforeAll( + () => + new Promise(resolve => { + server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ value: [] })) + }) + server.listen(0, () => { + port = server.address().port + resolve() + }) + }) + ) + + afterAll(() => new Promise(resolve => server.close(resolve))) test('HTTP client attributes are set on remote service span', async () => { // skip for cds 8 due to Cloud SDK resilience module resolution issues in test environment diff --git a/test/tracing-remote-cloudsdk.test.js b/test/tracing-remote-cloudsdk.test.js index 976ee348..3708cecf 100644 --- a/test/tracing-remote-cloudsdk.test.js +++ b/test/tracing-remote-cloudsdk.test.js @@ -11,7 +11,7 @@ describe('tracing remote via cloud sdk', () => { // cloud-sdk resilience module resolution has issues on cds 8 if (Number(cds.version.split('.')[0]) < 9) return - const log = jest.spyOn(console, 'dir') + const log = vi.spyOn(console, 'dir') beforeEach(log.mockClear) const getSpans = () => log.mock.calls.map(c => c[0]).filter(Boolean) @@ -19,16 +19,19 @@ describe('tracing remote via cloud sdk', () => { let server, port - beforeAll(done => { - server = http.createServer((req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }) - res.end(JSON.stringify({ value: [] })) - }) - server.listen(0, () => { - port = server.address().port - done() - }) - }) + beforeAll( + () => + new Promise(resolve => { + server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ value: [] })) + }) + server.listen(0, () => { + port = server.address().port + resolve() + }) + }) + ) afterAll(() => new Promise(resolve => server.close(resolve))) diff --git a/test/tracing-remote-native.test.js b/test/tracing-remote-native.test.js index 5a6947cc..fe8a0a2e 100644 --- a/test/tracing-remote-native.test.js +++ b/test/tracing-remote-native.test.js @@ -15,23 +15,26 @@ describe('tracing remote via native fetch', () => { // cloud-sdk resilience module resolution has issues on cds 8 if (Number(cds.version.split('.')[0]) < 9) return - const log = jest.spyOn(console, 'dir') + const log = vi.spyOn(console, 'dir') beforeEach(log.mockClear) const getSpans = () => log.mock.calls.map(c => c[0]).filter(Boolean) let server, port - beforeAll(done => { - server = http.createServer((req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }) - res.end(JSON.stringify({ value: [] })) - }) - server.listen(0, () => { - port = server.address().port - done() - }) - }) + beforeAll( + () => + new Promise(resolve => { + server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ value: [] })) + }) + server.listen(0, () => { + port = server.address().port + resolve() + }) + }) + ) afterAll(() => new Promise(resolve => server.close(resolve))) diff --git a/test/tracing-span-names.test.js b/test/tracing-span-names.test.js index 027579f3..8ff39d39 100644 --- a/test/tracing-span-names.test.js +++ b/test/tracing-span-names.test.js @@ -5,7 +5,7 @@ const http = require('http') describe('span names', () => { beforeEach(data.reset) - const log = jest.spyOn(console, 'dir') + const log = vi.spyOn(console, 'dir') beforeEach(log.mockClear) const getSpans = () => log.mock.calls.map(c => c[0]).filter(Boolean) @@ -63,16 +63,19 @@ describe('span names', () => { describe('cloud sdk', () => { let server, port - beforeAll(done => { - server = http.createServer((req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }) - res.end(JSON.stringify({ value: [] })) - }) - server.listen(0, () => { - port = server.address().port - done() - }) - }) + beforeAll( + () => + new Promise(resolve => { + server = http.createServer((req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ value: [] })) + }) + server.listen(0, () => { + port = server.address().port + resolve() + }) + }) + ) afterAll(() => new Promise(resolve => server.close(resolve))) diff --git a/vitest.config.mjs b/vitest.config.mjs new file mode 100644 index 00000000..49b75cf5 --- /dev/null +++ b/vitest.config.mjs @@ -0,0 +1,37 @@ +import { defineConfig } from 'vitest/config' + +// Default: 42s timeout, run every *.test.js file. +let testTimeout = 42000 +let include = ['test/**/*.test.js'] + +// HANA CI runs only a small subset with a 10x timeout (ported from the old +// jest.config.js). The `cds_requires_telemetry_tracing` env has to be set here, +// before any test file requires @sap/cds, so keep it in the config module. +if (process.env.CI && process.env.HANA_DRIVER) { + testTimeout *= 10 + include = ['test/**/tracing-attributes.test.js', 'test/**/passport.test.js'] + + if (process.env.HANA_PROM) + process.env.cds_requires_telemetry_tracing = JSON.stringify({ _hana_prom: process.env.HANA_PROM === 'true' }) +} + +export default defineConfig({ + test: { + // globals:true keeps describe/test/beforeEach/... available without importing + // them in every test file (smallest diff to the existing jest suite). + globals: true, + include, + testTimeout, + // The OTLP exporters (and CAP's telemetry SDK) can leave open handles/timers + // alive. Run each test file in its own forked child process so that, once a + // file finishes, its process is torn down and the handles die with it. This + // is what makes the suite EXIT CLEANLY where jest needed --forceExit. + // (In Vitest 4 the former poolOptions.forks.* are top-level options.) + pool: 'forks', + // fresh child per file: matches jest's per-file isolation and preserves the + // top-of-module process.env mutations some test files rely on. + isolate: true, + // don't hang the run waiting on lingering handles at teardown. + teardownTimeout: 5000 + } +}) From 205e449efb0498d4b5eb1f32795360d0c6d9120a Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:12:10 +0200 Subject: [PATCH 07/31] chore: adopt oxfmt for code formatting (#476) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/workflows/ci.yml | 1 + .oxfmtrc.jsonc | 21 ++ lib/exporter/ConsoleMetricExporter.js | 4 +- lib/logging/index.js | 5 +- lib/metrics/index.js | 8 +- lib/tracing/cloud_sdk.js | 12 +- lib/tracing/index.js | 8 +- package-lock.json | 410 ++++++++++++++++++++++++ package.json | 21 +- test/bookshop/srv/admin-service.js | 6 +- test/console-span-exporter.test.js | 72 ++++- test/metrics-outbox-disabled.test.js | 2 +- test/metrics-outbox-multitenant.test.js | 2 +- test/metrics-outbox.test.js | 2 +- test/tracing-attributes.test.js | 16 +- test/tracing-outboxed-batch.test.js | 24 +- test/tracing-remote-native.test.js | 4 +- 17 files changed, 574 insertions(+), 44 deletions(-) create mode 100644 .oxfmtrc.jsonc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 79da765d..d9dd4939 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - run: npm i - run: npm run lint + - run: npm run format:check test: runs-on: ubuntu-latest strategy: diff --git a/.oxfmtrc.jsonc b/.oxfmtrc.jsonc new file mode 100644 index 00000000..413968f7 --- /dev/null +++ b/.oxfmtrc.jsonc @@ -0,0 +1,21 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "arrowParens": "avoid", + "bracketSpacing": true, + "embeddedLanguageFormatting": "auto", + "htmlWhitespaceSensitivity": "css", + "insertPragma": false, + "jsxSingleQuote": false, + "printWidth": 120, + "proseWrap": "preserve", + "quoteProps": "as-needed", + "requirePragma": false, + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": false, + "vueIndentScriptAndStyle": false, + "sortPackageJson": false, + "ignorePatterns": ["*.md", "node_modules/**", "package-lock.json", "CHANGELOG.md", "jest.config.js"] +} diff --git a/lib/exporter/ConsoleMetricExporter.js b/lib/exporter/ConsoleMetricExporter.js index 21493a3d..402fe6c0 100644 --- a/lib/exporter/ConsoleMetricExporter.js +++ b/lib/exporter/ConsoleMetricExporter.js @@ -119,7 +119,9 @@ class ConsoleMetricExporter extends StandardConsoleMetricExporter { // export other metrics for (const tenant of Object.keys(other)) { for (const [k, v] of Object.entries(other[tenant])) { - LOG.info(`${k}${tenant !== 'undefined' ? ` of tenant "${tenant}"` : ''}: ${inspect(v.length === 1 ? v[0] : v)}`) + LOG.info( + `${k}${tenant !== 'undefined' ? ` of tenant "${tenant}"` : ''}: ${inspect(v.length === 1 ? v[0] : v)}` + ) } } } diff --git a/lib/logging/index.js b/lib/logging/index.js index 08840201..45a1ea36 100644 --- a/lib/logging/index.js +++ b/lib/logging/index.js @@ -40,7 +40,10 @@ function _getExporter() { if (kind.match(/to-cloud-logging$/)) { if (!credentials) credentials = getCredsForCLSAsUPS() - if (!credentials) throw new Error('No SAP Cloud Logging credentials found. Make sure the bound service instance uses the tag "Cloud Logging".') + if (!credentials) + throw new Error( + 'No SAP Cloud Logging credentials found. Make sure the bound service instance uses the tag "Cloud Logging".' + ) augmentCLCreds(credentials) config.url ??= credentials.url config.credentials ??= credentials.credentials diff --git a/lib/metrics/index.js b/lib/metrics/index.js index bd9be759..f876433f 100644 --- a/lib/metrics/index.js +++ b/lib/metrics/index.js @@ -50,7 +50,8 @@ function _getExporter() { // Augment configuration depending on 'kind' of telemetry if (kind.match(/to-dynatrace$/)) { if (!credentials) credentials = getCredsForDTAsUPS() - if (!credentials) throw new Error('No Dynatrace credentials found. Make sure the bound service instance uses the tag "dynatrace".') + if (!credentials) + throw new Error('No Dynatrace credentials found. Make sure the bound service instance uses the tag "dynatrace".') config.url ??= `${credentials.apiurl}/v2/otlp/v1/metrics` config.headers ??= {} @@ -67,7 +68,10 @@ function _getExporter() { if (kind.match(/to-cloud-logging$/)) { if (!credentials) credentials = getCredsForCLSAsUPS() - if (!credentials) throw new Error('No SAP Cloud Logging credentials found. Make sure the bound service instance uses the tag "Cloud Logging".') + if (!credentials) + throw new Error( + 'No SAP Cloud Logging credentials found. Make sure the bound service instance uses the tag "Cloud Logging".' + ) augmentCLCreds(credentials) config.url ??= credentials.url diff --git a/lib/tracing/cloud_sdk.js b/lib/tracing/cloud_sdk.js index adc465fe..c19737c4 100644 --- a/lib/tracing/cloud_sdk.js +++ b/lib/tracing/cloud_sdk.js @@ -36,7 +36,11 @@ module.exports = () => { }) } }) - Object.defineProperty(cloudSDK, 'executeHttpRequest', { value: _executeHttpRequest, writable: true, configurable: true }) + Object.defineProperty(cloudSDK, 'executeHttpRequest', { + value: _executeHttpRequest, + writable: true, + configurable: true + }) const _executeHttpRequestWithOrigin = wrap(_executeWithOrigin, { wrapper: function executeHttpRequestWithOrigin(destination, requestConfig) { return trace(_cloudSdkSpanName(destination, requestConfig), _executeWithOrigin, this, arguments, { @@ -45,5 +49,9 @@ module.exports = () => { }) } }) - Object.defineProperty(cloudSDK, 'executeHttpRequestWithOrigin', { value: _executeHttpRequestWithOrigin, writable: true, configurable: true }) + Object.defineProperty(cloudSDK, 'executeHttpRequestWithOrigin', { + value: _executeHttpRequestWithOrigin, + writable: true, + configurable: true + }) } diff --git a/lib/tracing/index.js b/lib/tracing/index.js index 8a887c3a..bbdef34e 100644 --- a/lib/tracing/index.js +++ b/lib/tracing/index.js @@ -106,7 +106,8 @@ function _getExporter() { if (kind.match(/to-dynatrace$/)) { if (!credentials) credentials = getCredsForDTAsUPS() - if (!credentials) throw new Error('No Dynatrace credentials found. Make sure the bound service instance uses the tag "dynatrace".') + if (!credentials) + throw new Error('No Dynatrace credentials found. Make sure the bound service instance uses the tag "dynatrace".') config.url ??= `${credentials.apiurl}/v2/otlp/v1/traces` config.headers ??= {} // credentials.rest_apitoken?.token is deprecated and only supported for compatibility reasons @@ -119,7 +120,10 @@ function _getExporter() { if (kind.match(/to-cloud-logging$/)) { if (!credentials) credentials = getCredsForCLSAsUPS() - if (!credentials) throw new Error('No SAP Cloud Logging credentials found. Make sure the bound service instance uses the tag "Cloud Logging".') + if (!credentials) + throw new Error( + 'No SAP Cloud Logging credentials found. Make sure the bound service instance uses the tag "Cloud Logging".' + ) augmentCLCreds(credentials) config.url ??= credentials.url config.credentials ??= credentials.credentials diff --git a/package-lock.json b/package-lock.json index 759d61b4..ef4c98c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "@sap/cds-mtxs": "^4", "axios": "^1.6.7", "eslint": "^10", + "oxfmt": "0.63.0", "vitest": "^4" }, "peerDependencies": { @@ -1265,6 +1266,353 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.63.0.tgz", + "integrity": "sha512-YmRth4ZPGgEXcgmkhvANbC9uD67dxmSobW7DQuyt5tOBOKvPnIpk5SVHBj88E+7wMNRI2FhqaDbOhQFBix+b8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.63.0.tgz", + "integrity": "sha512-icbahX8X2X3sRamOMecvdYeZXWjPDazRDIfvWfy7Ca1nc/ZDT2Y9k5Nt7s46EqFd7NQPdgk+CM3/SgIT5LPCaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.63.0.tgz", + "integrity": "sha512-WV+Ze5v5gI2qoj8jpAovt8KBTW8pjEz/AiMXXjeTQS+Bmf/MmZXTS40S8xNPDszX+W8WDv2Bbk6qKrMTtUGu1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.63.0.tgz", + "integrity": "sha512-CJGSBdDxXOWIpoFXHpverimCvz084KA7L483rqJ44c3jDtzv6d4qOSoR/V9ywSHfV+Ks1lwIj2P49BFhunLNAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.63.0.tgz", + "integrity": "sha512-BDfKY+KhL2078cgswBBFQPAYuxCy93bS/iC5frdSeSbTLcGrR6VC2hsuPTanoJmg84+wSyWl0wWC1eR+uTnkRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.63.0.tgz", + "integrity": "sha512-Ov1cQEXT4mj7cojAokWSS1eoxkoyvbDfAbxNsGIKY2o36kvdAaFzPxRN6NxFRk9fD72B8oCoTTX/NuYTUWlpsg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.63.0.tgz", + "integrity": "sha512-0LE7ro3+6L79jcMANycAZfRaC7zxr9YZ2+vEL5uMD9QlEep+rS/r1kSJsnuLl991NXJZD60euh0PC1GHrR20vw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.63.0.tgz", + "integrity": "sha512-izPk+2Z4gjuZK32Fqh5qXoMpT/2NXzLh++ob57HiEiVSQZ1iYXu8EKMzb+K5AvWyIEXhdDIt7ADjGGtFhkT9Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.63.0.tgz", + "integrity": "sha512-alPmbOuWXFXiSo+lOtv6X71C7SYMEDW2WVvywOvf9BwKgEhSNGhMTLeFVSjKUMCamcjbbgVdsWF8GN1uy8xshg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.63.0.tgz", + "integrity": "sha512-BdzCPvolJc4AWZ+YMzgUDJcDzbQWrFjYuqBHoNHNqP1aCaluQRJNs4k3vNU5IG7vTpjf9zeD73D7MFM1TecZpg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.63.0.tgz", + "integrity": "sha512-7sIgfLzqtNKSkMGsGVyRpHwpjNezRg2XONvUOheFZs95TSZpM0JAuPpA8KrQFsWc4wPU95roX2O69JgH8igOgw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.63.0.tgz", + "integrity": "sha512-9Tcg0y0WcVa6Mm9AgcgFMseDS+VkFJZpKZ8We9SpDY4gg5jewSwln+0sO04QLcTS1BtfDl9MwR+NfID8L7PUTg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.63.0.tgz", + "integrity": "sha512-qWKC1pEOpx1qYhXaugPhHUeXwSfqEOk2wJH2LqVXGPV5iQYfdAZdt+d2XDiX4DTSWA2QDMUcFB+wEORh3Xn/sA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.63.0.tgz", + "integrity": "sha512-S9wXYOiGSqYGS4Fx/TFsY+xDd/7dE5s+rUgbA4TsHiVF9e8J3ZcKmP7dsP/7iqLI9Wz7Ic7TzEr3mdthRCTdrA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.63.0.tgz", + "integrity": "sha512-5eGyTJuMZNwBSHCivXt8Yuta6GeTYksOPXRk2MIhajiyFGQx7bjaHIwY+ZusAoFHhT157A9x6sktLjYo9D5oMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.63.0.tgz", + "integrity": "sha512-Rz7hx+Dv3DoW/S6pwVAyjfFXp7/trdQ1zg+vNmsdsdDNlUccugp4XNqambSuEAeP0DaG9k72AtNyfDXCEg0AGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.63.0.tgz", + "integrity": "sha512-T/IuizKN9mr4Xw6YYnptkXRNdLkyIlUZ7c8zfTOBpoytZyJ1BAsMUvsMDEx0X4YvSMpaivm+DR8112rQfzC25g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.63.0.tgz", + "integrity": "sha512-XjrO5FJ5Wl9vsAxtCP1G/eaeT6y1K2s9CICUHGE42cEjou32/J6S+B1KnrOAboj6E7uhJnwPbRSvznWcxNdA0g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.63.0.tgz", + "integrity": "sha512-sgsHCQy432OTQH4Ikk3tZptp3GqwnhwUDuY0loBH41zyHWfMZY9v8Dy78wsnSofHejvFozZGgJgBB1A0LQRwMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -4365,6 +4713,58 @@ "node": ">= 0.8.0" } }, + "node_modules/oxfmt": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.63.0.tgz", + "integrity": "sha512-kgdDwv35wvVf6554U2Ab8Jnd0zTM+TsEQWwaB70RAjK3gICFAFGO+2Hd3Be27GMoXj3XRL9IKSNRVl7KBQL6iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinypool": "2.1.0" + }, + "bin": { + "oxfmt": "bin/oxfmt" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxfmt/binding-android-arm-eabi": "0.63.0", + "@oxfmt/binding-android-arm64": "0.63.0", + "@oxfmt/binding-darwin-arm64": "0.63.0", + "@oxfmt/binding-darwin-x64": "0.63.0", + "@oxfmt/binding-freebsd-x64": "0.63.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.63.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.63.0", + "@oxfmt/binding-linux-arm64-gnu": "0.63.0", + "@oxfmt/binding-linux-arm64-musl": "0.63.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.63.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.63.0", + "@oxfmt/binding-linux-riscv64-musl": "0.63.0", + "@oxfmt/binding-linux-s390x-gnu": "0.63.0", + "@oxfmt/binding-linux-x64-gnu": "0.63.0", + "@oxfmt/binding-linux-x64-musl": "0.63.0", + "@oxfmt/binding-openharmony-arm64": "0.63.0", + "@oxfmt/binding-win32-arm64-msvc": "0.63.0", + "@oxfmt/binding-win32-ia32-msvc": "0.63.0", + "@oxfmt/binding-win32-x64-msvc": "0.63.0" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5036,6 +5436,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", + "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, "node_modules/tinyrainbow": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", diff --git a/package.json b/package.json index 22f2968f..14c10c4c 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,9 @@ ], "scripts": { "lint": "npx eslint . --max-warnings=0", - "test": "vitest run --silent" + "test": "vitest run --silent", + "format": "npx oxfmt", + "format:check": "npx oxfmt --check" }, "dependencies": { "@opentelemetry/api": "^1.9", @@ -47,6 +49,7 @@ "@sap/cds-mtxs": "^4", "axios": "^1.6.7", "eslint": "^10", + "oxfmt": "0.63.0", "vitest": "^4" }, "cds": { @@ -108,8 +111,12 @@ }, "telemetry-to-dynatrace": { "vcap": [ - { "label": "dynatrace" }, - { "tag": "dynatrace" } + { + "label": "dynatrace" + }, + { + "tag": "dynatrace" + } ], "tracing": { "exporter": { @@ -127,8 +134,12 @@ }, "telemetry-to-cloud-logging": { "vcap": [ - { "label": "cloud-logging" }, - { "tag": "Cloud Logging" } + { + "label": "cloud-logging" + }, + { + "tag": "Cloud Logging" + } ], "tracing": { "exporter": { diff --git a/test/bookshop/srv/admin-service.js b/test/bookshop/srv/admin-service.js index 9cb186fe..f8f4f3bd 100644 --- a/test/bookshop/srv/admin-service.js +++ b/test/bookshop/srv/admin-service.js @@ -43,11 +43,7 @@ module.exports = class AdminService extends cds.ApplicationService { this.on('test_outboxed_send_batch', async () => { const externalOne = await cds.connect.to('ExternalServiceOne') const queued = cds.queued(externalOne) - await Promise.all([ - queued.send('call', {}), - queued.send('call', {}), - queued.send('call', {}) - ]) + await Promise.all([queued.send('call', {}), queued.send('call', {}), queued.send('call', {})]) }) // test_scheduled: schedules a one-shot task to fire after a short delay. diff --git a/test/console-span-exporter.test.js b/test/console-span-exporter.test.js index 4c83e4e8..3fbee4fc 100644 --- a/test/console-span-exporter.test.js +++ b/test/console-span-exporter.test.js @@ -80,9 +80,30 @@ describe('ConsoleSpanExporter', () => { // childB (5 → 9 ms) const TRACE = 'a'.repeat(32) const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 10 }) - const childA = span({ name: 'childA', traceId: TRACE, spanId: 'cA', parentSpanId: 'r0', startMs: 1, durationMs: 3 }) - const grand = span({ name: 'grandchild', traceId: TRACE, spanId: 'g0', parentSpanId: 'cA', startMs: 2, durationMs: 1 }) - const childB = span({ name: 'childB', traceId: TRACE, spanId: 'cB', parentSpanId: 'r0', startMs: 5, durationMs: 4 }) + const childA = span({ + name: 'childA', + traceId: TRACE, + spanId: 'cA', + parentSpanId: 'r0', + startMs: 1, + durationMs: 3 + }) + const grand = span({ + name: 'grandchild', + traceId: TRACE, + spanId: 'g0', + parentSpanId: 'cA', + startMs: 2, + durationMs: 1 + }) + const childB = span({ + name: 'childB', + traceId: TRACE, + spanId: 'cB', + parentSpanId: 'r0', + startMs: 5, + durationMs: 4 + }) // Order matters: children must arrive BEFORE the root for the exporter's // temporaryStorage flush logic to merge them under the same primer. @@ -111,7 +132,14 @@ describe('ConsoleSpanExporter', () => { // Root starts at 100 ms wallclock; child at 105 ms. Child should display as 5.00 → ... const TRACE = 'b'.repeat(32) const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 100, durationMs: 20 }) - const child = span({ name: 'child', traceId: TRACE, spanId: 'c0', parentSpanId: 'r0', startMs: 105, durationMs: 10 }) + const child = span({ + name: 'child', + traceId: TRACE, + spanId: 'c0', + parentSpanId: 'r0', + startMs: 105, + durationMs: 10 + }) const [primer] = exportAndCapture([child, root]) @@ -123,8 +151,22 @@ describe('ConsoleSpanExporter', () => { const TRACE = 'c'.repeat(32) const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 50 }) const late = span({ name: 'late', traceId: TRACE, spanId: 's3', parentSpanId: 'r0', startMs: 10, durationMs: 1 }) - const earlyLong = span({ name: 'earlyLong', traceId: TRACE, spanId: 's1', parentSpanId: 'r0', startMs: 0, durationMs: 30 }) - const earlyShort = span({ name: 'earlyShort', traceId: TRACE, spanId: 's2', parentSpanId: 'r0', startMs: 0, durationMs: 5 }) + const earlyLong = span({ + name: 'earlyLong', + traceId: TRACE, + spanId: 's1', + parentSpanId: 'r0', + startMs: 0, + durationMs: 30 + }) + const earlyShort = span({ + name: 'earlyShort', + traceId: TRACE, + spanId: 's2', + parentSpanId: 'r0', + startMs: 0, + durationMs: 5 + }) const [primer] = exportAndCapture([late, earlyShort, earlyLong, root]) @@ -154,7 +196,14 @@ describe('ConsoleSpanExporter', () => { const TRACE = 'f'.repeat(32) const root = span({ name: 'root', traceId: TRACE, spanId: 'r0', startMs: 0, durationMs: 10 }) // The skip regex is /^[A-Z]+ \/\${0,1}\w+$/ — single path segment, no slashes after the first. - const noisy = span({ name: 'GET /catalog', traceId: TRACE, spanId: 'h0', parentSpanId: 'r0', startMs: 1, durationMs: 5 }) + const noisy = span({ + name: 'GET /catalog', + traceId: TRACE, + spanId: 'h0', + parentSpanId: 'r0', + startMs: 1, + durationMs: 5 + }) const [primer] = exportAndCapture([noisy, root]) @@ -215,7 +264,14 @@ describe('ConsoleSpanExporter', () => { it('does not throw when a child arrives without its parent (orphan trace)', () => { // No root provided for this trace — the exporter should buffer the child and not flush. const TRACE = '5'.repeat(32) - const orphan = span({ name: 'orphan', traceId: TRACE, spanId: 'o0', parentSpanId: 'r-missing', startMs: 0, durationMs: 1 }) + const orphan = span({ + name: 'orphan', + traceId: TRACE, + spanId: 'o0', + parentSpanId: 'r-missing', + startMs: 0, + durationMs: 1 + }) expect(() => exportAndCapture([orphan])).not.to.throw() expect(infoCalls.length).to.equal(0) diff --git a/test/metrics-outbox-disabled.test.js b/test/metrics-outbox-disabled.test.js index d1fe5bb1..2616a09e 100644 --- a/test/metrics-outbox-disabled.test.js +++ b/test/metrics-outbox-disabled.test.js @@ -50,4 +50,4 @@ describe('queue metrics is disabled', () => { expect(metricValue('med_storage_time_in_seconds')).to.eq(null) expect(metricValue('max_storage_time_in_seconds')).to.eq(null) }) -}) \ No newline at end of file +}) diff --git a/test/metrics-outbox-multitenant.test.js b/test/metrics-outbox-multitenant.test.js index 485a3380..0f0b459b 100644 --- a/test/metrics-outbox-multitenant.test.js +++ b/test/metrics-outbox-multitenant.test.js @@ -222,4 +222,4 @@ describe('queue metrics for multi tenant service', () => { expect(metricValue(T2, 'remaining_entries')).to.eq(0) }) }) -}) \ No newline at end of file +}) diff --git a/test/metrics-outbox.test.js b/test/metrics-outbox.test.js index 68b9e66c..113a76ee 100644 --- a/test/metrics-outbox.test.js +++ b/test/metrics-outbox.test.js @@ -294,4 +294,4 @@ describe('queue metrics for single tenant service', () => { }) }) }) -}) \ No newline at end of file +}) diff --git a/test/tracing-attributes.test.js b/test/tracing-attributes.test.js index c1497b06..5972584f 100644 --- a/test/tracing-attributes.test.js +++ b/test/tracing-attributes.test.js @@ -79,14 +79,18 @@ describe('tracing attributes', () => { test('SELECT', async () => { await SELECT.from('sap.capire.bookshop.Books').where('title !=', 'DUMMY') - const rowCounts = dbSpans().map(s => s.attributes['db.client.response.returned_rows']).filter(v => v != null) + const rowCounts = dbSpans() + .map(s => s.attributes['db.client.response.returned_rows']) + .filter(v => v != null) expect(rowCounts).to.include(5) _match_db_spans('SELECT') }) test('INSERT', async () => { await INSERT.into('sap.capire.bookshop.Books').entries([{ ID: 1 }, { ID: 2 }]) - const rowCounts = dbSpans().map(s => s.attributes['db.client.response.returned_rows']).filter(v => v != null) + const rowCounts = dbSpans() + .map(s => s.attributes['db.client.response.returned_rows']) + .filter(v => v != null) expect(rowCounts).to.include(2) // TODO // _match_db_spans('INSERT') @@ -94,7 +98,9 @@ describe('tracing attributes', () => { test('UPDATE', async () => { await UPDATE('sap.capire.bookshop.Books').set({ stock: 42 }).where('ID > 250') - const rowCounts = dbSpans().map(s => s.attributes['db.client.response.returned_rows']).filter(v => v != null) + const rowCounts = dbSpans() + .map(s => s.attributes['db.client.response.returned_rows']) + .filter(v => v != null) expect(rowCounts).to.include(3) // TODO // _match_db_spans('UPDATE') @@ -102,7 +108,9 @@ describe('tracing attributes', () => { test('DELETE', async () => { await DELETE.from('sap.capire.bookshop.Books') - const rowCounts = dbSpans().map(s => s.attributes['db.client.response.returned_rows']).filter(v => v != null) + const rowCounts = dbSpans() + .map(s => s.attributes['db.client.response.returned_rows']) + .filter(v => v != null) expect(rowCounts).to.include(0) // texts expect(rowCounts).to.include(5) // TODO diff --git a/test/tracing-outboxed-batch.test.js b/test/tracing-outboxed-batch.test.js index 640689ea..d55993ec 100644 --- a/test/tracing-outboxed-batch.test.js +++ b/test/tracing-outboxed-batch.test.js @@ -36,16 +36,18 @@ describe('tracing for outboxed batch (chunk-size fan-out)', () => { expect(upserts.length, 'expected three producer outbox UPSERTs').to.be.gte(3) // Look for a queue worker root containing multiple dispatch tx spans. - const workerTrace = groupedByTrace().find(g => - g.root.name === 'cds.spawn - run task' && - g.all.filter(s => s.name === 'ExternalServiceOne - tx').length >= 2 + const workerTrace = groupedByTrace().find( + g => g.root.name === 'cds.spawn - run task' && g.all.filter(s => s.name === 'ExternalServiceOne - tx').length >= 2 ) expect(workerTrace, 'expected a worker trace with multiple ExternalServiceOne - tx children').to.exist // The worker root must have exactly one lock tx (db - tx with READ + UPDATE)… - const lockTxs = workerTrace.all.filter(s => - s.name === 'db - tx' && - workerTrace.all.some(c => c.parentSpanContext?.spanId === s.spanContext().spanId && c.name === 'db - READ cds.outbox.Messages') + const lockTxs = workerTrace.all.filter( + s => + s.name === 'db - tx' && + workerTrace.all.some( + c => c.parentSpanContext?.spanId === s.spanContext().spanId && c.name === 'db - READ cds.outbox.Messages' + ) ) expect(lockTxs, 'expected one lock tx (db - tx with READ + UPDATE)').to.have.lengthOf(1) @@ -54,8 +56,14 @@ describe('tracing for outboxed batch (chunk-size fan-out)', () => { expect(dispatchTxs.length, 'expected multiple dispatch txs (chunk-size fan-out)').to.be.gte(2) for (const tx of dispatchTxs) { const kids = workerTrace.all.filter(k => k.parentSpanContext?.spanId === tx.spanContext().spanId) - expect(kids.some(k => k.name.match(/ExternalServiceOne - handle/)), 'dispatch tx should contain handle call').to.be.true - expect(kids.some(k => k.name === 'db - DELETE cds.outbox.Messages'), 'dispatch tx should contain DELETE').to.be.true + expect( + kids.some(k => k.name.match(/ExternalServiceOne - handle/)), + 'dispatch tx should contain handle call' + ).to.be.true + expect( + kids.some(k => k.name === 'db - DELETE cds.outbox.Messages'), + 'dispatch tx should contain DELETE' + ).to.be.true } // The dispatch txs should overlap in time (parallel), not be strictly sequential. diff --git a/test/tracing-remote-native.test.js b/test/tracing-remote-native.test.js index fe8a0a2e..36c00beb 100644 --- a/test/tracing-remote-native.test.js +++ b/test/tracing-remote-native.test.js @@ -47,9 +47,7 @@ describe('tracing remote via native fetch', () => { // no mock handler - let it make the actual HTTP call via native fetch await remote.send({ method: 'GET', path: '/test' }) - const undiciSpan = getSpans().find( - s => s.instrumentationScope?.name === '@opentelemetry/instrumentation-undici' - ) + const undiciSpan = getSpans().find(s => s.instrumentationScope?.name === '@opentelemetry/instrumentation-undici') expect(undiciSpan, 'no span from @opentelemetry/instrumentation-undici').to.exist expect(undiciSpan.attributes['http.request.method']).to.equal('GET') expect(undiciSpan.attributes['http.response.status_code']).to.equal(200) From d8f3dd11b1743246c0c862e4947a313544ec1a1f Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:06:36 +0200 Subject: [PATCH 08/31] test: capture outbox+console metrics via in-memory reader & unit-test ConsoleMetricExporter (#479) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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. --- test/bookshop/.cdsrc.json | 12 +- test/bookshop/lib/MyInMemoryMetricReader.js | 189 +++++++++++++ test/console-metric-exporter.test.js | 255 +++++++++++++++++ test/metrics-outbox-disabled.test.js | 27 +- test/metrics-outbox-multitenant.test.js | 224 ++++++++------- test/metrics-outbox.test.js | 290 +++++++++++--------- test/metrics.test.js | 86 ++++-- 7 files changed, 807 insertions(+), 276 deletions(-) create mode 100644 test/bookshop/lib/MyInMemoryMetricReader.js create mode 100644 test/console-metric-exporter.test.js diff --git a/test/bookshop/.cdsrc.json b/test/bookshop/.cdsrc.json index 6571cbe7..606f66f5 100644 --- a/test/bookshop/.cdsrc.json +++ b/test/bookshop/.cdsrc.json @@ -30,6 +30,10 @@ "metrics": { "config": { "exportIntervalMillis": 100 + }, + "exporter": { + "module": "./lib/MyInMemoryMetricReader.js", + "class": "MyInMemoryMetricReader" } } } @@ -46,8 +50,8 @@ "_db_pool": false, "_queue": true, "exporter": { - "module": "@opentelemetry/sdk-metrics", - "class": "ConsoleMetricExporter" + "module": "./lib/MyInMemoryMetricReader.js", + "class": "MyInMemoryMetricReader" } } } @@ -64,8 +68,8 @@ "_db_pool": false, "_queue": false, "exporter": { - "module": "@opentelemetry/sdk-metrics", - "class": "ConsoleMetricExporter" + "module": "./lib/MyInMemoryMetricReader.js", + "class": "MyInMemoryMetricReader" } } } diff --git a/test/bookshop/lib/MyInMemoryMetricReader.js b/test/bookshop/lib/MyInMemoryMetricReader.js new file mode 100644 index 00000000..5787ee01 --- /dev/null +++ b/test/bookshop/lib/MyInMemoryMetricReader.js @@ -0,0 +1,189 @@ +// In-memory metric reader for tests. Exported metrics are accumulated in a module-level array +// that tests can import directly via `require('./lib/MyInMemoryMetricReader').captured`. +// Wired into the meter provider via .cdsrc.json profile config (no provider-poking from tests): +// the class is exporter-shaped (has `export()`), so lib/metrics/index.js wraps it in a +// PeriodicExportingMetricReader — keeping the configured `exportIntervalMillis` working. +// +// Kept dependency-light on purpose: it does NOT `require('@sap/cds')` at module top (doing so +// broke span capture once for the sibling span exporter). Only @opentelemetry primitives. +// +// TEMPORALITY: mirrors production. lib/metrics/index.js configures the real exporter with +// `temporalityPreference: AggregationTemporality.DELTA`, so the tests must validate what a real +// DELTA export produces — the reader honors that preference rather than forcing CUMULATIVE. +// +// Under DELTA each export reports only the *increment* since the previous collection, and +// `expectEventually` force-flushes repeatedly, so a naive "latest datapoint" read of a counter +// would drop to 0 after the first flush. We therefore split handling by datapoint type: +// * SUM datapoints (the 3 counters: incoming_messages, outgoing_messages, processing_failures) +// are summed into a running total per counter series (metric name + full attribute set) — +// reconstructing the cumulative value the tests assert against (totalInc/totalOut/totalFailed). +// * GAUGE datapoints (cold_entries, remaining_entries, *_storage_time_in_seconds) are absolute +// point-in-time observations; for those we keep the latest exported value, never a sum. + +const { ExportResultCode } = require('@opentelemetry/core') +const { AggregationTemporality, DataPointType } = require('@opentelemetry/sdk-metrics') +const { metrics } = require('@opentelemetry/api') + +// Raw ResourceMetrics objects, one per collection/flush. Drives the GAUGE latest-value lookup. +const captured = [] + +// Running totals for SUM (counter) series. Keyed by the fully-qualified series identity +// (metric name + every attribute on the datapoint) so distinct (queue.name, tenant) series never +// collide; each entry keeps the original attributes so lookups can match by attribute subset the +// same way the gauge path does. Under DELTA the SDK reports the increment since its last +// collection; summing every increment a series receives reconstructs its cumulative value — which +// is what the tests track (totalInc/totalOut/totalFailed grow monotonically, never reset per case). +// +// NOTE: `captured` and `counterSeries` are process-level singletons. Cross-file correctness relies +// on Vitest isolating each test file in its own worker process (vitest.config.mjs: pool:'forks' + +// isolate:true). Two files sharing this module in one process would bleed counter totals together. +const counterSeries = new Map() + +function seriesKey(metricName, attributes) { + const sorted = Object.keys(attributes) + .sort() + .map(k => `${k}=${attributes[k]}`) + .join('&') + return `${metricName} ${sorted}` +} + +// True when `sub` is an attribute subset of `full` (all keys present with equal values). +function attributesMatch(full, sub) { + return Object.entries(sub).every(([key, value]) => full[key] === value) +} + +class MyInMemoryMetricReader { + constructor(config = {}) { + // Honor the temporality the plugin config sets (DELTA in production) so the tests exercise the + // real export shape. Defaults to DELTA to match lib/metrics/index.js when no config is passed. + this._temporality = config.temporalityPreference ?? AggregationTemporality.DELTA + } + + // Invoked by PeriodicExportingMetricReader for each instrument type. + selectAggregationTemporality() { + return this._temporality + } + + export(resourceMetrics, resultCallback) { + captured.push(resourceMetrics) + + // Fold DELTA increments of SUM (counter) datapoints into the running totals. + for (const scopeMetrics of resourceMetrics.scopeMetrics) { + for (const metric of scopeMetrics.metrics) { + if (metric.dataPointType !== DataPointType.SUM) continue + for (const dp of metric.dataPoints) { + const key = seriesKey(metric.descriptor.name, dp.attributes) + const entry = counterSeries.get(key) + if (entry) entry.total += dp.value + else counterSeries.set(key, { name: metric.descriptor.name, attributes: dp.attributes, total: dp.value }) + } + } + } + + resultCallback({ code: ExportResultCode.SUCCESS }) + } + + shutdown() { + return Promise.resolve() + } + + forceFlush() { + return Promise.resolve() + } +} + +// Most recent GAUGE MetricData for `queue.` that carries datapoints, scanning captured +// exports newest-first (mirrors the old `consoleDirLogs.findLast(... && dataPoints?.length)`). +function latestGaugeMetric(metricName) { + const name = `queue.${metricName}` + for (let i = captured.length - 1; i >= 0; i--) { + for (const scopeMetrics of captured[i].scopeMetrics) { + for (const metric of scopeMetrics.metrics) { + if ( + metric.descriptor.name === name && + metric.dataPointType === DataPointType.GAUGE && + metric.dataPoints?.length + ) + return metric + } + } + } + return null +} + +// Accumulated counter total for `queue.` across all series whose attributes match the +// given filter (subset match, like the gauge lookup). Returns null when no counter series exists +// for that name — i.e. the metric was never exported as a counter (queue metrics disabled) or the +// filter matches nothing. +function counterTotal(metricName, attributes) { + const name = `queue.${metricName}` + let found = false + let total = 0 + for (const entry of counterSeries.values()) { + if (entry.name === name && attributesMatch(entry.attributes, attributes)) { + found = true + total += entry.total + } + } + return found ? total : null +} + +// Names of metrics that are SUM (counter) instruments — the three counters the queue plugin +// registers. Dispatches latestDataPointValue explicitly, rather than relying on counterSeries +// happening to be populated (which is empty on the first poll after reset()). +const COUNTER_METRIC_NAMES = new Set([ + 'queue.incoming_messages', + 'queue.outgoing_messages', + 'queue.processing_failures' +]) + +function isCounter(metricName) { + return COUNTER_METRIC_NAMES.has(`queue.${metricName}`) +} + +// Value of `queue.` for the datapoint(s) matching all given attributes +// (e.g. { 'queue.name': ... } and/or { 'sap.tenancy.tenant_id': ... }). For counters this is the +// accumulated running total (cumulative, reconstructed from DELTA increments); for gauges it is +// the latest absolute observation. Returns null when the metric was never exported (queue metrics +// disabled) or no datapoint matches the filter. +function latestDataPointValue(metricName, attributes = {}) { + if (isCounter(metricName)) return counterTotal(metricName, attributes) + + const metric = latestGaugeMetric(metricName) + if (!metric) return null + const dp = metric.dataPoints.find(dp => attributesMatch(dp.attributes, attributes)) + return dp ? dp.value : null +} + +// Force the wired meter provider to collect + export now, so the reader reflects the latest state. +// Fails fast if the provider isn't the real (wired) one — a NoopMeterProvider has no forceFlush, +// which would otherwise silently no-op and let a polling helper busy-spin its whole timeout. +async function forceFlush() { + const provider = metrics.getMeterProvider() + if (typeof provider.forceFlush !== 'function') { + throw new Error( + 'MyInMemoryMetricReader.forceFlush: meter provider is not wired up (no forceFlush) — ' + + 'is the metrics-outbox profile active and the reader configured?' + ) + } + await provider.forceFlush() +} + +// Clears the per-test GAUGE state (captured exports) so a stale point-in-time value from a previous +// case cannot leak. The counter running totals are intentionally NOT cleared: the suites' counter +// assertions (totalInc/totalOut/totalFailed) and the plugin's underlying counters are cumulative +// across the whole file, and the SDK's DELTA baseline likewise persists across flushes — zeroing +// only our side would desync it and under-count. See the module header for the full rationale. +function reset() { + captured.length = 0 +} + +module.exports = { + MyInMemoryMetricReader, + captured, + counterSeries, + latestGaugeMetric, + latestDataPointValue, + forceFlush, + reset +} diff --git a/test/console-metric-exporter.test.js b/test/console-metric-exporter.test.js new file mode 100644 index 00000000..ab13af1f --- /dev/null +++ b/test/console-metric-exporter.test.js @@ -0,0 +1,255 @@ +// Unit tests for ConsoleMetricExporter — verifies the user-friendly formatting of the three +// output branches (db.pool table, queue table, "other" metrics) plus the aggregated host-metrics +// block, by feeding the exporter crafted ResourceMetrics-shaped fixtures and inspecting the +// formatted strings passed to LOG.info. +// +// This is a pure unit test: no cds.test server, no real OTel SDK, no console spying. + +const cds = require('@sap/cds') + +// Hook LOG.info BEFORE requiring the exporter so the exporter's module-level +// `cds.log('telemetry')` resolves to a logger whose .info we control. +const infoCalls = [] +const telemetryLog = cds.log('telemetry') +const originalInfo = telemetryLog.info +telemetryLog.info = (...args) => infoCalls.push(args) + +const ConsoleMetricExporter = require('../lib/exporter/ConsoleMetricExporter') + +afterAll(() => { + telemetryLog.info = originalInfo +}) + +beforeEach(() => { + infoCalls.length = 0 +}) + +// --- helpers --------------------------------------------------------------- + +// Builds a minimal ScopeMetrics-shaped object. +function scopeMetrics(name, metrics) { + return { scope: { name }, metrics } +} + +// Builds a minimal MetricData-shaped object. `dataPoints` are `{ attributes, value }`. +function metric(name, dataPoints, description = name) { + return { descriptor: { name, description }, dataPoints } +} + +// Drives the exporter and returns the lines logged. Asserts the result callback got SUCCESS. +function exportAndCapture(scopes) { + const exporter = new ConsoleMetricExporter() + let result + exporter.export({ scopeMetrics: scopes }, r => (result = r)) + expect(result).to.deep.equal({ code: 0 /* ExportResultCode.SUCCESS */ }) + return infoCalls.map(args => args[0]) +} + +// --- assertions ------------------------------------------------------------ + +const { expect } = require('@cap-js/cds-test') + +const APP_SCOPE = '@cap-js/telemetry' +const HOST_SCOPE = '@opentelemetry/instrumentation-host-metrics' + +describe('ConsoleMetricExporter', () => { + describe('db.pool table', () => { + it('renders a "db.pool:" header and the size/available/pending table row', () => { + const scopes = [ + scopeMetrics(APP_SCOPE, [ + metric('db.pool.size', [{ attributes: {}, value: 3 }]), + metric('db.pool.max', [{ attributes: {}, value: 10 }]), + metric('db.pool.available', [{ attributes: {}, value: 2 }]), + metric('db.pool.pending', [{ attributes: {}, value: 1 }]) + ]) + ] + + const [line] = exportAndCapture(scopes) + + expect(infoCalls.length).to.equal(1) + expect(line).to.match(/^db\.pool:/) + // Column header + expect(line).to.include('size | available | pending') + // size/max, available/size, pending — padded into the row + expect(line).to.match(/3\/10 \| +2\/3 \| +1/) + }) + + it('labels the table with the tenant id when a datapoint carries sap.tenancy.tenant_id', () => { + const attributes = { 'sap.tenancy.tenant_id': 't1' } + const scopes = [ + scopeMetrics(APP_SCOPE, [ + metric('db.pool.size', [{ attributes, value: 5 }]), + metric('db.pool.max', [{ attributes, value: 8 }]), + metric('db.pool.available', [{ attributes, value: 4 }]), + metric('db.pool.pending', [{ attributes, value: 0 }]) + ]) + ] + + const [line] = exportAndCapture(scopes) + + expect(line).to.match(/^db\.pool of tenant "t1":/) + expect(line).to.match(/5\/8 \| +4\/5 \| +0/) + }) + }) + + describe('queue table', () => { + it('renders a "queue:" header, the wide column header, and lands the values', () => { + const dp = value => [{ attributes: {}, value }] + const scopes = [ + scopeMetrics(APP_SCOPE, [ + metric('queue.cold_entries', dp(1)), + metric('queue.remaining_entries', dp(2)), + metric('queue.min_storage_time_in_seconds', dp(3)), + metric('queue.med_storage_time_in_seconds', dp(4)), + metric('queue.max_storage_time_in_seconds', dp(5)), + metric('queue.incoming_messages', dp(6)), + metric('queue.outgoing_messages', dp(7)), + metric('queue.processing_failures', dp(8)) + ]) + ] + + const [line] = exportAndCapture(scopes) + + expect(infoCalls.length).to.equal(1) + expect(line).to.match(/^queue:/) + // Column header (all eight columns) + expect(line).to.include( + 'cold | remaining | min storage time | med storage time | max storage time | incoming | outgoing | failed' + ) + // The eight values land in the padded row, in column order. + const row = line.split('\n').at(-1) + expect(row.split('|').map(c => c.trim())).to.deep.equal(['1', '2', '3', '4', '5', '6', '7', '8']) + }) + + it('labels the queue table with the tenant id when present', () => { + const attributes = { 'sap.tenancy.tenant_id': 't2' } + const dp = value => [{ attributes, value }] + const scopes = [ + scopeMetrics(APP_SCOPE, [ + metric('queue.cold_entries', dp(0)), + metric('queue.remaining_entries', dp(0)), + metric('queue.min_storage_time_in_seconds', dp(0)), + metric('queue.med_storage_time_in_seconds', dp(0)), + metric('queue.max_storage_time_in_seconds', dp(0)), + metric('queue.incoming_messages', dp(0)), + metric('queue.outgoing_messages', dp(0)), + metric('queue.processing_failures', dp(0)) + ]) + ] + + const [line] = exportAndCapture(scopes) + + expect(line).to.match(/^queue of tenant "t2":/) + }) + }) + + describe('other metrics', () => { + it('logs a single-datapoint metric unwrapped (inspect of the datapoint object)', () => { + const scopes = [ + scopeMetrics(APP_SCOPE, [metric('nodejs.eventloop.utilization', [{ attributes: {}, value: 0.42 }])]) + ] + + const [line] = exportAndCapture(scopes) + + expect(infoCalls.length).to.equal(1) + // Unwrapped: inspect(v[0]) of a single datapoint object → starts with "{" + expect(line).to.match(/^nodejs\.eventloop\.utilization: \{/) + expect(line).to.include('value: 0.42') + expect(line).not.to.match(/^nodejs\.eventloop\.utilization: \[/) + }) + + it('logs a multi-datapoint metric as an array (inspect of the datapoints array)', () => { + const scopes = [ + scopeMetrics(APP_SCOPE, [ + metric('nodejs.eventloop.time', [ + { attributes: { 'nodejs.eventloop.state': 'active' }, value: 100 }, + { attributes: { 'nodejs.eventloop.state': 'idle' }, value: 200 } + ]) + ]) + ] + + const [line] = exportAndCapture(scopes) + + expect(infoCalls.length).to.equal(1) + // Wrapped: inspect(v) of the datapoints array → starts with "[" + expect(line).to.match(/^nodejs\.eventloop\.time: \[/) + expect(line).to.include('value: 100') + expect(line).to.include('value: 200') + }) + + it('labels other metrics with the tenant id when present', () => { + const scopes = [ + scopeMetrics(APP_SCOPE, [metric('some.metric', [{ attributes: { 'sap.tenancy.tenant_id': 't3' }, value: 1 }])]) + ] + + const [line] = exportAndCapture(scopes) + + expect(line).to.match(/^some\.metric of tenant "t3": \{/) + }) + }) + + describe('host metrics', () => { + const original = process.env.HOST_METRICS_LOG_SYSTEM + + afterEach(() => { + if (original === undefined) delete process.env.HOST_METRICS_LOG_SYSTEM + else process.env.HOST_METRICS_LOG_SYSTEM = original + }) + + // process.* metrics are always aggregated; a system.network.* metric is only aggregated when + // HOST_METRICS_LOG_SYSTEM is set. + function hostScope() { + return [ + scopeMetrics(HOST_SCOPE, [ + metric('process.cpu.time', [{ attributes: { 'process.cpu.state': 'user' }, value: 1.5 }], 'process cpu time'), + metric('process.memory.usage', [{ attributes: {}, value: 123456 }], 'process memory usage'), + metric( + 'system.network.io', + [{ attributes: { device: 'eth0', direction: 'receive' }, value: 999 }], + 'system network io' + ) + ]) + ] + } + + it('aggregates only process.* into a "host metrics:" block when HOST_METRICS_LOG_SYSTEM is unset', () => { + delete process.env.HOST_METRICS_LOG_SYSTEM + + const [line] = exportAndCapture(hostScope()) + + expect(infoCalls.length).to.equal(1) + expect(line).to.match(/^host metrics:/) + expect(line).to.include('process cpu time') + expect(line).to.include('process memory usage') + // system.* excluded when the flag is unset + expect(line).not.to.include('system network io') + }) + + it('additionally aggregates system.* when HOST_METRICS_LOG_SYSTEM is set', () => { + process.env.HOST_METRICS_LOG_SYSTEM = 'true' + + const [line] = exportAndCapture(hostScope()) + + expect(infoCalls.length).to.equal(1) + expect(line).to.match(/^host metrics:/) + expect(line).to.include('process cpu time') + expect(line).to.include('process memory usage') + expect(line).to.include('system network io') + }) + }) + + describe('shutdown', () => { + it('returns FAILED via setImmediate when the exporter is shutting down', () => { + const exporter = new ConsoleMetricExporter() + exporter._shutdown = true + + return new Promise(resolve => { + exporter.export({ scopeMetrics: [] }, result => { + expect(result).to.deep.equal({ code: 1 /* ExportResultCode.FAILED */ }) + expect(infoCalls.length).to.equal(0) + resolve() + }) + }) + }) + }) +}) diff --git a/test/metrics-outbox-disabled.test.js b/test/metrics-outbox-disabled.test.js index 2616a09e..6b6e33a8 100644 --- a/test/metrics-outbox-disabled.test.js +++ b/test/metrics-outbox-disabled.test.js @@ -1,23 +1,13 @@ -import { vi } from 'vitest' -// Mock console.dir to capture logs ConsoleMetricExporter writes -const consoleDirLogs = [] -vi.spyOn(console, 'dir').mockImplementation((...args) => { - consoleDirLogs.push(args) -}) - const cds = require('@sap/cds') -const { setTimeout: wait } = require('node:timers/promises') + +// With queue metrics disabled (_queue: false in the metrics-outbox-disabled profile) the +// in-memory reader should never capture any `queue.*` datapoints. +const { latestDataPointValue, forceFlush, reset } = require('./bookshop/lib/MyInMemoryMetricReader') const { expect, GET } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'metrics-outbox-disabled') function metricValue(metric) { - const mostRecentMetricLog = consoleDirLogs.findLast( - metricLog => metricLog[0].descriptor.name === `queue.${metric}` - )?.[0] - - if (!mostRecentMetricLog) return null - - return mostRecentMetricLog.dataPoints[0].value + return latestDataPointValue(metric) } describe('queue metrics is disabled', () => { @@ -35,12 +25,15 @@ describe('queue metrics is disabled', () => { externalServiceOne.before('*', () => {}) }) - beforeEach(() => (consoleDirLogs.length = 0)) + beforeEach(() => reset()) test('metrics are not collected', async () => { await GET('/odata/v4/proxy/proxyCallToExternalServiceOne', admin) - await wait(150) // Wait for metrics to be collected + // Assert absence: with _queue disabled no queue.* instrument is ever registered, so nothing can + // be exported. Force a few export cycles (rather than a fixed sleep) to give the app every chance + // to emit a queue metric — none must appear. + for (let i = 0; i < 5; i++) await forceFlush() expect(metricValue('cold_entries')).to.eq(null) expect(metricValue('remaining_entries')).to.eq(null) diff --git a/test/metrics-outbox-multitenant.test.js b/test/metrics-outbox-multitenant.test.js index 0f0b459b..d97bc09f 100644 --- a/test/metrics-outbox-multitenant.test.js +++ b/test/metrics-outbox-multitenant.test.js @@ -1,13 +1,10 @@ -import { vi } from 'vitest' -// Mock console.dir to capture logs ConsoleMetricExporter writes -const consoleDirLogs = [] -vi.spyOn(console, 'dir').mockImplementation((...args) => { - consoleDirLogs.push(args) -}) - const cds = require('@sap/cds') const { setTimeout: wait } = require('node:timers/promises') +// Exported metric data is captured in-memory by MyInMemoryMetricReader (wired via the +// metrics-outbox profile in .cdsrc.json) instead of scraping ConsoleMetricExporter's console.dir. +const { latestDataPointValue, forceFlush, reset } = require('./bookshop/lib/MyInMemoryMetricReader') + const { expect, GET, axios } = cds.test( __dirname + '/bookshop', '--with-mocks', @@ -17,19 +14,36 @@ const { expect, GET, axios } = cds.test( axios.defaults.validateStatus = () => true function metricValue(tenant, metric) { - const mostRecentMetricLog = consoleDirLogs.findLast( - metricLog => metricLog[0].descriptor.name === `queue.${metric}` - )?.[0] - - if (!mostRecentMetricLog) return null + return latestDataPointValue(metric, { 'sap.tenancy.tenant_id': tenant }) +} - const mostRecentTenantDataPoint = mostRecentMetricLog.dataPoints.find( - dp => dp.attributes['sap.tenancy.tenant_id'] === tenant - ) - return mostRecentTenantDataPoint ? mostRecentTenantDataPoint.value : null +// State-based wait: force the wired meter provider to collect + export, then re-run the assertion +// block. Replaces all fixed-time `wait(…)` sleeps — the loop completes the instant the in-memory +// per-tenant queue statistics (kept fresh by the existing cds.spawn poller) reflect the asserted +// state. forceFlush() throws fast if the provider isn't wired, so a misconfigured profile fails +// loudly instead of busy-spinning the full timeout. +async function expectEventually(assertion, { timeout = 10000, interval = 25 } = {}) { + const start = Date.now() + let lastError + while (true) { + await forceFlush() + try { + assertion() + return + } catch (err) { + lastError = err + if (Date.now() - start >= timeout) throw lastError + await wait(interval) + } + } } describe('queue metrics for multi tenant service', () => { + if (cds.version.split('.')[0] < 9) { + test.skip('skipping tests for cds version < 9', () => {}) + return + } + const T1 = 'tenant_1' const T2 = 'tenant_2' @@ -67,7 +81,7 @@ describe('queue metrics for multi tenant service', () => { beforeEach(async () => { await cds.tx({ tenant: T1 }, () => DELETE.from('cds.outbox.Messages')) await cds.tx({ tenant: T2 }, () => DELETE.from('cds.outbox.Messages')) - consoleDirLogs.length = 0 + reset() }) describe('given the target service succeeds immediately', () => { @@ -77,34 +91,39 @@ describe('queue metrics for multi tenant service', () => { GET('/odata/v4/proxy/proxyCallToExternalServiceOne', user[T2]) ]) - await wait(150) // Wait for metrics to be collected - - expect(metricValue(T1, 'cold_entries')).to.eq(0) - expect(metricValue(T1, 'incoming_messages')).to.eq(totalInc[T1]) - expect(metricValue(T1, 'outgoing_messages')).to.eq(totalOut[T1]) - expect(metricValue(T1, 'remaining_entries')).to.eq(0) - expect(metricValue(T1, 'min_storage_time_in_seconds')).to.eq(0) - expect(metricValue(T1, 'med_storage_time_in_seconds')).to.eq(0) - expect(metricValue(T1, 'max_storage_time_in_seconds')).to.eq(0) - - expect(metricValue(T2, 'cold_entries')).to.eq(0) - expect(metricValue(T2, 'incoming_messages')).to.eq(totalInc[T2]) - expect(metricValue(T2, 'outgoing_messages')).to.eq(totalOut[T2]) - expect(metricValue(T2, 'remaining_entries')).to.eq(0) - expect(metricValue(T2, 'min_storage_time_in_seconds')).to.eq(0) - expect(metricValue(T2, 'med_storage_time_in_seconds')).to.eq(0) - expect(metricValue(T2, 'max_storage_time_in_seconds')).to.eq(0) + await expectEventually(() => { + expect(metricValue(T1, 'cold_entries')).to.eq(0) + expect(metricValue(T1, 'incoming_messages')).to.eq(totalInc[T1]) + expect(metricValue(T1, 'outgoing_messages')).to.eq(totalOut[T1]) + expect(metricValue(T1, 'remaining_entries')).to.eq(0) + expect(metricValue(T1, 'min_storage_time_in_seconds')).to.eq(0) + expect(metricValue(T1, 'med_storage_time_in_seconds')).to.eq(0) + expect(metricValue(T1, 'max_storage_time_in_seconds')).to.eq(0) + + expect(metricValue(T2, 'cold_entries')).to.eq(0) + expect(metricValue(T2, 'incoming_messages')).to.eq(totalInc[T2]) + expect(metricValue(T2, 'outgoing_messages')).to.eq(totalOut[T2]) + expect(metricValue(T2, 'remaining_entries')).to.eq(0) + expect(metricValue(T2, 'min_storage_time_in_seconds')).to.eq(0) + expect(metricValue(T2, 'med_storage_time_in_seconds')).to.eq(0) + expect(metricValue(T2, 'max_storage_time_in_seconds')).to.eq(0) + }) }) }) describe('given a target service that requires retries', () => { let currentRetryCount, unboxedService + // Fail the first 3 attempts so the 4th delivers — the same widened window #445 introduced for + // the single-tenant suite: it opens a comfortable gap between "message has aged >=1s in the + // queue" and "message is delivered and removed", which is what made the wall-clock test flaky. + const ATTEMPTS_TO_FAIL = 3 + beforeAll(async () => { unboxedService = await cds.connect.to('ExternalServiceOne') unboxedService.before('call', req => { - if ((currentRetryCount[cds.context.tenant] += 1) <= 2) { + if ((currentRetryCount[cds.context.tenant] += 1) <= ATTEMPTS_TO_FAIL) { totalFailed[cds.context.tenant] += 1 return req.reject({ status: 503 }) } @@ -120,76 +139,75 @@ describe('queue metrics for multi tenant service', () => { }) test('storage time increases before message can be delivered', async () => { + // Reference time taken BEFORE the GETs so the queuing round-trip counts toward the wall-clock debounce below. const timeOfInitialCall = Date.now() await Promise.all([ GET('/odata/v4/proxy/proxyCallToExternalServiceOne', user[T1]), GET('/odata/v4/proxy/proxyCallToExternalServiceOne', user[T2]) ]) - // Wait for the first retry to be processed - while (currentRetryCount[T1] < 2) await wait(10) - while (currentRetryCount[T2] < 2) await wait(10) - - // Wait until at least 1 second has passed since the initial call - const timeAfterFirstRetry = Date.now() - if (timeAfterFirstRetry - timeOfInitialCall < 1000) { - await wait(1000 - (timeAfterFirstRetry - timeOfInitialCall)) - } - await wait(150) // ... for metrics to be collected - - expect(metricValue(T1, 'cold_entries')).to.eq(0) - expect(metricValue(T1, 'incoming_messages')).to.eq(totalInc[T1]) - expect(metricValue(T1, 'outgoing_messages')).to.eq(totalOut[T1]) - expect(metricValue(T1, 'processing_failures')).to.eq(totalFailed[T1]) - expect(metricValue(T1, 'remaining_entries')).to.eq(1) - expect(metricValue(T1, 'min_storage_time_in_seconds')).to.be.gte(1) - expect(metricValue(T1, 'med_storage_time_in_seconds')).to.be.gte(1) - expect(metricValue(T1, 'max_storage_time_in_seconds')).to.be.gte(1) - - expect(metricValue(T2, 'cold_entries')).to.eq(0) - expect(metricValue(T2, 'incoming_messages')).to.eq(totalInc[T2]) - expect(metricValue(T2, 'outgoing_messages')).to.eq(totalOut[T2]) - expect(metricValue(T2, 'processing_failures')).to.eq(totalFailed[T2]) - expect(metricValue(T2, 'remaining_entries')).to.eq(1) - expect(metricValue(T2, 'min_storage_time_in_seconds')).to.be.gte(1) - expect(metricValue(T2, 'med_storage_time_in_seconds')).to.be.gte(1) - expect(metricValue(T2, 'max_storage_time_in_seconds')).to.be.gte(1) - - // Wait for the second retry to be processd - while (currentRetryCount[T1] < 3) await wait(10) - while (currentRetryCount[T2] < 3) await wait(10) - await wait(600) // ... for metrics to be collected - - expect(metricValue(T1, 'cold_entries')).to.eq(0) - expect(metricValue(T1, 'incoming_messages')).to.eq(totalInc[T1]) - expect(metricValue(T1, 'outgoing_messages')).to.eq(totalOut[T1]) - expect(metricValue(T1, 'processing_failures')).to.eq(totalFailed[T1]) - expect(metricValue(T1, 'remaining_entries')).to.eq(0) - expect(metricValue(T1, 'min_storage_time_in_seconds')).to.eq(0) - expect(metricValue(T1, 'med_storage_time_in_seconds')).to.eq(0) - expect(metricValue(T1, 'max_storage_time_in_seconds')).to.eq(0) - - expect(metricValue(T2, 'cold_entries')).to.eq(0) - expect(metricValue(T2, 'incoming_messages')).to.eq(totalInc[T2]) - expect(metricValue(T2, 'outgoing_messages')).to.eq(totalOut[T2]) - expect(metricValue(T2, 'processing_failures')).to.eq(totalFailed[T2]) - expect(metricValue(T2, 'remaining_entries')).to.eq(0) - expect(metricValue(T2, 'min_storage_time_in_seconds')).to.eq(0) - expect(metricValue(T2, 'med_storage_time_in_seconds')).to.eq(0) - expect(metricValue(T2, 'max_storage_time_in_seconds')).to.eq(0) + // The storage_time gauges need a real second to elapse since the messages were enqueued — + // this is the one place the test fundamentally depends on wall-clock time. + const elapsed = Date.now() - timeOfInitialCall + if (elapsed < 1500) await wait(1500 - elapsed) + + await expectEventually(() => { + // Message is still being retried (>=1s aged) for both tenants. + expect(currentRetryCount[T1]).to.be.gte(2) + expect(currentRetryCount[T2]).to.be.gte(2) + + expect(metricValue(T1, 'cold_entries')).to.eq(0) + expect(metricValue(T1, 'incoming_messages')).to.eq(totalInc[T1]) + expect(metricValue(T1, 'outgoing_messages')).to.eq(totalOut[T1]) + expect(metricValue(T1, 'processing_failures')).to.eq(totalFailed[T1]) + expect(metricValue(T1, 'remaining_entries')).to.eq(1) + expect(metricValue(T1, 'min_storage_time_in_seconds')).to.be.gte(1) + expect(metricValue(T1, 'med_storage_time_in_seconds')).to.be.gte(1) + expect(metricValue(T1, 'max_storage_time_in_seconds')).to.be.gte(1) + + expect(metricValue(T2, 'cold_entries')).to.eq(0) + expect(metricValue(T2, 'incoming_messages')).to.eq(totalInc[T2]) + expect(metricValue(T2, 'outgoing_messages')).to.eq(totalOut[T2]) + expect(metricValue(T2, 'processing_failures')).to.eq(totalFailed[T2]) + expect(metricValue(T2, 'remaining_entries')).to.eq(1) + expect(metricValue(T2, 'min_storage_time_in_seconds')).to.be.gte(1) + expect(metricValue(T2, 'med_storage_time_in_seconds')).to.be.gte(1) + expect(metricValue(T2, 'max_storage_time_in_seconds')).to.be.gte(1) + }) + + // Final attempt — the message is delivered and removed from the outbox for both tenants. + await expectEventually(() => { + expect(currentRetryCount[T1]).to.be.gte(ATTEMPTS_TO_FAIL + 1) + expect(currentRetryCount[T2]).to.be.gte(ATTEMPTS_TO_FAIL + 1) + + expect(metricValue(T1, 'cold_entries')).to.eq(0) + expect(metricValue(T1, 'incoming_messages')).to.eq(totalInc[T1]) + expect(metricValue(T1, 'outgoing_messages')).to.eq(totalOut[T1]) + expect(metricValue(T1, 'processing_failures')).to.eq(totalFailed[T1]) + expect(metricValue(T1, 'remaining_entries')).to.eq(0) + expect(metricValue(T1, 'min_storage_time_in_seconds')).to.eq(0) + expect(metricValue(T1, 'med_storage_time_in_seconds')).to.eq(0) + expect(metricValue(T1, 'max_storage_time_in_seconds')).to.eq(0) + + expect(metricValue(T2, 'cold_entries')).to.eq(0) + expect(metricValue(T2, 'incoming_messages')).to.eq(totalInc[T2]) + expect(metricValue(T2, 'outgoing_messages')).to.eq(totalOut[T2]) + expect(metricValue(T2, 'processing_failures')).to.eq(totalFailed[T2]) + expect(metricValue(T2, 'remaining_entries')).to.eq(0) + expect(metricValue(T2, 'min_storage_time_in_seconds')).to.eq(0) + expect(metricValue(T2, 'med_storage_time_in_seconds')).to.eq(0) + expect(metricValue(T2, 'max_storage_time_in_seconds')).to.eq(0) + }) }) }) describe('given a taget service that fails unrecoverably', () => { let unboxedService - const didProcess = { [T1]: false, [T2]: false } - beforeAll(async () => { unboxedService = await cds.connect.to('ExternalServiceOne') unboxedService.before('call', req => { - didProcess[cds.context.tenant] = true totalFailed[cds.context.tenant] += 1 return req.reject({ status: 418, unrecoverable: true }) }) @@ -205,21 +223,19 @@ describe('queue metrics for multi tenant service', () => { GET('/odata/v4/proxy/proxyCallToExternalServiceOne', user[T2]) ]) - while (!didProcess[T1]) await wait(10) - while (!didProcess[T2]) await wait(10) - await wait(500) // ... for metrics to be collected - - expect(metricValue(T1, 'cold_entries')).to.eq(1) - expect(metricValue(T1, 'incoming_messages')).to.eq(totalInc[T1]) - expect(metricValue(T1, 'outgoing_messages')).to.eq(totalOut[T1]) - expect(metricValue(T1, 'processing_failures')).to.eq(totalFailed[T1]) - expect(metricValue(T1, 'remaining_entries')).to.eq(0) - - expect(metricValue(T2, 'cold_entries')).to.eq(1) - expect(metricValue(T2, 'incoming_messages')).to.eq(totalInc[T2]) - expect(metricValue(T2, 'outgoing_messages')).to.eq(totalOut[T2]) - expect(metricValue(T2, 'processing_failures')).to.eq(totalFailed[T2]) - expect(metricValue(T2, 'remaining_entries')).to.eq(0) + await expectEventually(() => { + expect(metricValue(T1, 'cold_entries')).to.eq(1) + expect(metricValue(T1, 'incoming_messages')).to.eq(totalInc[T1]) + expect(metricValue(T1, 'outgoing_messages')).to.eq(totalOut[T1]) + expect(metricValue(T1, 'processing_failures')).to.eq(totalFailed[T1]) + expect(metricValue(T1, 'remaining_entries')).to.eq(0) + + expect(metricValue(T2, 'cold_entries')).to.eq(1) + expect(metricValue(T2, 'incoming_messages')).to.eq(totalInc[T2]) + expect(metricValue(T2, 'outgoing_messages')).to.eq(totalOut[T2]) + expect(metricValue(T2, 'processing_failures')).to.eq(totalFailed[T2]) + expect(metricValue(T2, 'remaining_entries')).to.eq(0) + }) }) }) }) diff --git a/test/metrics-outbox.test.js b/test/metrics-outbox.test.js index 113a76ee..ab9f0d95 100644 --- a/test/metrics-outbox.test.js +++ b/test/metrics-outbox.test.js @@ -1,9 +1,4 @@ import { vi } from 'vitest' -// Mock console.dir to capture logs ConsoleMetricExporter writes -const consoleDirLogs = [] -vi.spyOn(console, 'dir').mockImplementation((...args) => { - consoleDirLogs.push(args) -}) const E1 = 'ExternalServiceOne' const E2 = 'ExternalServiceTwo' @@ -11,26 +6,46 @@ const E2 = 'ExternalServiceTwo' const cds = require('@sap/cds') const { setTimeout: wait } = require('node:timers/promises') +// Exported metric data is captured in-memory by MyInMemoryMetricReader (wired via the +// metrics-outbox profile in .cdsrc.json) instead of scraping ConsoleMetricExporter's console.dir. +const { latestDataPointValue, forceFlush, reset } = require('./bookshop/lib/MyInMemoryMetricReader') + const { expect, GET, axios } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'metrics-outbox') axios.defaults.validateStatus = () => true function metricValue(metric, queuedServiceName) { - const mostRecentMetricLog = consoleDirLogs.findLast( - metricLog => metricLog[0].descriptor.name === `queue.${metric}` && metricLog[0].dataPoints?.length - )?.[0] - - const mestRecentQueueMetricData = mostRecentMetricLog?.dataPoints.find( - dataPoint => dataPoint.attributes['queue.name'] === queuedServiceName - ) - - if (!mestRecentQueueMetricData) return null + return latestDataPointValue(metric, { 'queue.name': queuedServiceName }) +} - return mestRecentQueueMetricData.value +// State-based wait: force the wired meter provider to collect + export, then re-run the assertion +// block. Replaces all fixed-time `wait(150)` sleeps — the loop completes the instant the in-memory +// queue statistics (kept fresh by the existing cds.spawn poller) reflect the asserted state. +// forceFlush() throws fast if the provider isn't wired, so a misconfigured profile fails loudly +// instead of busy-spinning the full timeout. +async function expectEventually(assertion, { timeout = 10000, interval = 25 } = {}) { + const start = Date.now() + let lastError + while (true) { + await forceFlush() + try { + assertion() + return + } catch (err) { + lastError = err + if (Date.now() - start >= timeout) throw lastError + await wait(interval) + } + } } const debugLog = (cds.log('telemetry').debug = vi.fn(() => {})) describe('queue metrics for single tenant service', () => { + if (cds.version.split('.')[0] < 9) { + test.skip('skipping tests for cds version < 9', () => {}) + return + } + let totalInc = { [E1]: 0, [E2]: 0 } let totalOut = { [E1]: 0, [E2]: 0 } let totalFailed = { [E1]: 0, [E2]: 0 } @@ -74,7 +89,7 @@ describe('queue metrics for single tenant service', () => { beforeEach(async () => { await DELETE.from('cds.outbox.Messages') - consoleDirLogs.length = 0 + reset() debugLog.mockClear() }) @@ -82,37 +97,43 @@ describe('queue metrics for single tenant service', () => { test('metrics are collected', async () => { await GET('/odata/v4/proxy/proxyCallToExternalServiceOne', admin) - await wait(150) // Wait for metrics to be collected - - expect(metricValue('cold_entries', E1)).to.eq(0) - expect(metricValue('remaining_entries', E1)).to.eq(0) - expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) - expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) - expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) - expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) + await expectEventually(() => { + expect(metricValue('cold_entries', E1)).to.eq(0) + expect(metricValue('remaining_entries', E1)).to.eq(0) + expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) + expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) + expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) + expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) + expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) + expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) + }) await GET('/odata/v4/proxy/proxyCallToExternalServiceTwo', admin) - await wait(150) // Wait for metrics to be collected - - expect(metricValue('cold_entries', E2)).to.eq(0) - expect(metricValue('remaining_entries', E2)).to.eq(0) - expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) - expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) - expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) - expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) + await expectEventually(() => { + expect(metricValue('cold_entries', E2)).to.eq(0) + expect(metricValue('remaining_entries', E2)).to.eq(0) + expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) + expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) + expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) + expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) + expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) + expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) + }) }) }) describe('given a target service that requires retries', () => { let currentRetryCount, customizedHandler + // Fail the first 3 attempts so the 4th delivers. With the queue's exp-backoff schedule + // (0.5s, 1.25s, 2.375s, ...), this places the 4th attempt at ~t=4.1s after enqueue — + // giving a comfortable ~3s window between "message has aged 1s in the queue" and + // "message is finally delivered and removed". Tightening that window is what made the + // original wall-clock-based test flaky. + const ATTEMPTS_TO_FAIL = 3 const customizedHandlerFor = E => req => { - if ((currentRetryCount[E] += 1) <= 2) { + if ((currentRetryCount[E] += 1) <= ATTEMPTS_TO_FAIL) { totalFailed[E] += 1 return req.reject({ status: 503 }) } @@ -142,88 +163,87 @@ describe('queue metrics for single tenant service', () => { test('storage time increases before message can be delivered', async () => { await GET('/odata/v4/proxy/proxyCallToExternalServiceOne', admin) await GET('/odata/v4/proxy/proxyCallToExternalServiceTwo', admin) - + // Reference time taken after GETs return — i.e. after both messages are persisted in the outbox. const timeOfInitialCall = Date.now() - await wait(150) // ... for metrics to be collected - expect(currentRetryCount[E1]).to.eq(1) - expect(currentRetryCount[E2]).to.eq(1) - - expect(metricValue('cold_entries', E1)).to.eq(0) - expect(metricValue('remaining_entries', E1)).to.eq(1) - expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) - expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) - expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) - expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) - - expect(metricValue('cold_entries', E2)).to.eq(0) - expect(metricValue('remaining_entries', E2)).to.eq(1) - expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) - expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) - expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) - expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) - - // Wait for the first retry to be initiated - while (currentRetryCount[E1] < 2) await wait(10) - while (currentRetryCount[E2] < 2) await wait(10) - await wait(150) // ... for the retry to be processed and metrics to be collected - expect(currentRetryCount[E1]).to.eq(2) - expect(currentRetryCount[E2]).to.eq(2) - - // Wait until at least 1 second has passed since the initial call - const timeAfterFirstRetry = Date.now() - if (timeAfterFirstRetry - timeOfInitialCall < 1000) { - await wait(1000 - (timeAfterFirstRetry - timeOfInitialCall)) - } + // The queue has made its first delivery attempt for both services (handler invocation count is + // observed directly via the rejecting `before('call')` handler — pure CAP event observation). + await expectEventually(() => { + expect(currentRetryCount[E1]).to.be.gte(1) + expect(currentRetryCount[E2]).to.be.gte(1) + + expect(metricValue('cold_entries', E1)).to.eq(0) + expect(metricValue('remaining_entries', E1)).to.eq(1) + expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) + expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) + expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) + expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) + expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) + expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) + + expect(metricValue('cold_entries', E2)).to.eq(0) + expect(metricValue('remaining_entries', E2)).to.eq(1) + expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) + expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) + expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) + expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) + expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) + expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) + }) - await wait(150) // ... for metrics to be collected again - - expect(metricValue('cold_entries', E1)).to.eq(0) - expect(metricValue('remaining_entries', E1)).to.eq(1) - expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) - expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) - expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) - expect(metricValue('min_storage_time_in_seconds', E1)).to.be.gte(1) - expect(metricValue('med_storage_time_in_seconds', E1)).to.be.gte(1) - expect(metricValue('max_storage_time_in_seconds', E1)).to.be.gte(1) - - expect(metricValue('cold_entries', E2)).to.eq(0) - expect(metricValue('remaining_entries', E2)).to.eq(1) - expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) - expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) - expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) - expect(metricValue('min_storage_time_in_seconds', E2)).to.be.gte(1) - expect(metricValue('med_storage_time_in_seconds', E2)).to.be.gte(1) - expect(metricValue('max_storage_time_in_seconds', E2)).to.be.gte(1) - - // Wait for the second retry to be initiated - while (currentRetryCount[E1] < 3) await wait(10) - while (currentRetryCount[E2] < 3) await wait(10) - await wait(150) // ... for the retry to be processed and metrics to be collected - expect(currentRetryCount[E1]).to.eq(3) - expect(currentRetryCount[E2]).to.eq(3) - - expect(metricValue('cold_entries', E1)).to.eq(0) - expect(metricValue('remaining_entries', E1)).to.eq(0) - expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) - expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) - expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) - expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) - - expect(metricValue('cold_entries', E2)).to.eq(0) - expect(metricValue('remaining_entries', E2)).to.eq(0) - expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) - expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) - expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) - expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) + // The storage_time gauges need a real second to elapse since the messages were enqueued — + // this is the one place the test fundamentally depends on wall-clock time. + const elapsed = Date.now() - timeOfInitialCall + if (elapsed < 1500) await wait(1500 - elapsed) + + await expectEventually(() => { + // Either still on attempt 2 (waiting to retry) or on attempt 3 (delivered) — both are fine + // for these assertions, the message has been in the queue >=1s either way. + expect(currentRetryCount[E1]).to.be.gte(2) + expect(currentRetryCount[E2]).to.be.gte(2) + + expect(metricValue('cold_entries', E1)).to.eq(0) + expect(metricValue('remaining_entries', E1)).to.eq(1) + expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) + expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) + expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) + expect(metricValue('min_storage_time_in_seconds', E1)).to.be.gte(1) + expect(metricValue('med_storage_time_in_seconds', E1)).to.be.gte(1) + expect(metricValue('max_storage_time_in_seconds', E1)).to.be.gte(1) + + expect(metricValue('cold_entries', E2)).to.eq(0) + expect(metricValue('remaining_entries', E2)).to.eq(1) + expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) + expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) + expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) + expect(metricValue('min_storage_time_in_seconds', E2)).to.be.gte(1) + expect(metricValue('med_storage_time_in_seconds', E2)).to.be.gte(1) + expect(metricValue('max_storage_time_in_seconds', E2)).to.be.gte(1) + }) + + // Final attempt — the message is delivered and removed from the outbox. + await expectEventually(() => { + expect(currentRetryCount[E1]).to.be.gte(ATTEMPTS_TO_FAIL + 1) + expect(currentRetryCount[E2]).to.be.gte(ATTEMPTS_TO_FAIL + 1) + + expect(metricValue('cold_entries', E1)).to.eq(0) + expect(metricValue('remaining_entries', E1)).to.eq(0) + expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) + expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) + expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) + expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) + expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) + expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) + + expect(metricValue('cold_entries', E2)).to.eq(0) + expect(metricValue('remaining_entries', E2)).to.eq(0) + expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) + expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) + expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) + expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) + expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) + expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) + }) }) }) @@ -256,25 +276,25 @@ describe('queue metrics for single tenant service', () => { await GET('/odata/v4/proxy/proxyCallToExternalServiceOne', admin) await GET('/odata/v4/proxy/proxyCallToExternalServiceTwo', admin) - await wait(150) // ... for metrics to be collected - - expect(metricValue('cold_entries', E1)).to.eq(1) - expect(metricValue('remaining_entries', E1)).to.eq(0) - expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) - expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) - expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) - expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) - - expect(metricValue('cold_entries', E2)).to.eq(1) - expect(metricValue('remaining_entries', E2)).to.eq(0) - expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) - expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) - expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) - expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) + await expectEventually(() => { + expect(metricValue('cold_entries', E1)).to.eq(1) + expect(metricValue('remaining_entries', E1)).to.eq(0) + expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) + expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) + expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) + expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) + expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) + expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) + + expect(metricValue('cold_entries', E2)).to.eq(1) + expect(metricValue('remaining_entries', E2)).to.eq(0) + expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) + expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) + expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) + expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) + expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) + expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) + }) }) }) diff --git a/test/metrics.test.js b/test/metrics.test.js index 78765d24..6582b7a3 100644 --- a/test/metrics.test.js +++ b/test/metrics.test.js @@ -1,36 +1,90 @@ -// process.env.HOST_METRICS_RETAIN_SYSTEM = 'true' //> with this the test would fail -process.env.HOST_METRICS_LOG_SYSTEM = 'true' +// Integration tests for metrics collection — asserts on what is actually COLLECTED (which +// instruments produce datapoints, and how many), captured in-memory by MyInMemoryMetricReader +// (wired via the `metrics` profile in .cdsrc.json) instead of scraping ConsoleMetricExporter's +// log output. The formatting of those metrics is unit-tested in console-metric-exporter.test.js. const cds = require('@sap/cds') +const { setTimeout: wait } = require('node:timers/promises') + +const { captured, forceFlush, reset } = require('./bookshop/lib/MyInMemoryMetricReader') + const { expect, GET } = cds.test(__dirname + '/bookshop', '--profile', 'metrics') -const log = cds.test.log() -const wait = require('node:timers/promises').setTimeout +// State-based wait: force the wired meter provider to collect + export, then re-run the assertion +// block. Replaces fixed-time sleeps — the loop completes the instant the captured datapoints +// reflect the asserted state. forceFlush() throws fast if the provider isn't wired, so a +// misconfigured profile fails loudly instead of busy-spinning the full timeout. +async function expectEventually(assertion, { timeout = 10000, interval = 25 } = {}) { + const start = Date.now() + let lastError + while (true) { + await forceFlush() + try { + assertion() + return + } catch (err) { + lastError = err + if (Date.now() - start >= timeout) throw lastError + await wait(interval) + } + } +} + +// All metric descriptor names present across every captured export. +function capturedMetricNames() { + const names = new Set() + for (const rm of captured) { + for (const scopeMetrics of rm.scopeMetrics) { + for (const metric of scopeMetrics.metrics) names.add(metric.descriptor.name) + } + } + return names +} + +// Most recent captured MetricData for the given descriptor name (newest export first). +function latestMetric(name) { + for (let i = captured.length - 1; i >= 0; i--) { + for (const scopeMetrics of captured[i].scopeMetrics) { + for (const metric of scopeMetrics.metrics) { + if (metric.descriptor.name === name && metric.dataPoints?.length) return metric + } + } + } + return null +} describe('metrics', () => { const admin = { auth: { username: 'alice' } } - beforeEach(log.clear) + beforeEach(reset) test('system metrics are not collected by default', async () => { const { status } = await GET('/odata/v4/admin/Books', admin) expect(status).to.equal(200) - await wait(100) - - expect(log.output).to.match(/process/i) - expect(log.output).not.to.match(/network/i) + await expectEventually(() => { + const names = capturedMetricNames() + // process.* host metrics ARE collected out of the box ... + expect([...names].some(n => n.startsWith('process.'))).to.be.true + // ... but system.* (network/cpu/memory) collection is NOT enabled by default. + expect([...names].some(n => n.startsWith('system.'))).to.be.false + expect([...names].some(n => n.includes('network'))).to.be.false + }) }) - test('other metrics with multiple datapoints are logged as array', async () => { + test('other metrics can carry multiple datapoints', async () => { const { status } = await GET('/odata/v4/admin/Books', admin) expect(status).to.equal(200) - await wait(200) - - // nodejs.eventloop.time has multiple datapoints (active + idle) → logged as array - expect(log.output).to.match(/nodejs\.eventloop\.time: \[/) - // nodejs.eventloop.utilization has single datapoint → logged unwrapped (not as array) - expect(log.output).to.match(/nodejs\.eventloop\.utilization: \{/) + await expectEventually(() => { + // nodejs.eventloop.time is collected with multiple datapoints (active + idle) ... + const time = latestMetric('nodejs.eventloop.time') + expect(time).to.exist + expect(time.dataPoints.length).to.be.greaterThan(1) + // ... whereas nodejs.eventloop.utilization is a single datapoint. + const utilization = latestMetric('nodejs.eventloop.utilization') + expect(utilization).to.exist + expect(utilization.dataPoints.length).to.equal(1) + }) }) }) From eb28f9b92129e163f111e4f9575a775737a84c16 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:01:49 +0200 Subject: [PATCH 09/31] chore(deps-dev): bump the OTLP dev-dependency group to 0.221 (pin sdk-logs, #482) (#483) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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. --- package-lock.json | 576 ++++++---------------------------------------- package.json | 15 +- 2 files changed, 77 insertions(+), 514 deletions(-) diff --git a/package-lock.json b/package-lock.json index ef4c98c5..3d2554fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,12 +27,12 @@ "@cap-js/sqlite": "^3", "@cap-js/telemetry": "file:.", "@grpc/grpc-js": "^1.9.14", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.219", - "@opentelemetry/exporter-metrics-otlp-proto": "^0.219", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.219", - "@opentelemetry/exporter-trace-otlp-proto": "^0.219", - "@opentelemetry/instrumentation-host-metrics": "^0.2.0", - "@opentelemetry/instrumentation-runtime-node": "^0.32.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.221", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.221", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.221", + "@opentelemetry/exporter-trace-otlp-proto": "^0.221", + "@opentelemetry/instrumentation-host-metrics": "^0.4.0", + "@opentelemetry/instrumentation-runtime-node": "^0.34.0", "@sap-cloud-sdk/http-client": "^4", "@sap/cds-mtxs": "^4", "axios": "^1.6.7", @@ -422,20 +422,15 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-grpc": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.219.0.tgz", - "integrity": "sha512-6LaaSrPxK5L55bXevWajvOMxGOpNm0n12tG53TeZaUeNzXwLPg6d2KCC1zAlGsojan+xRG71mA4Qqs9K2VVrKQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-grpc/-/exporter-metrics-otlp-grpc-0.221.0.tgz", + "integrity": "sha512-KOgCtO15FC6C1T/xOqBcr7EyUs7B+7yomGNb5Y97d3s38rPbCCk5sewkmE2b0/itOkQ/PptX8CLlD+kn2mEtTg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.8.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.219.0", - "@opentelemetry/otlp-exporter-base": "0.219.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", - "@opentelemetry/otlp-transformer": "0.219.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/sdk-metrics": "2.8.0" + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -444,68 +439,18 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", - "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-grpc/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.8.0.tgz", - "integrity": "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.219.0.tgz", - "integrity": "sha512-6CaDRbMVHZSDWzNXwrR8y/H4B/Z1eMNnkHiPQlTx3Ojz2OHY4X/aff/UC4P/3pHUQSuTfi3oh2UsPPZppw+Vrg==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.221.0.tgz", + "integrity": "sha512-sRfCKbOzgy8xZQV2as0RzIZlnCmCseCKZGLfRcrpo2CBngJDr+rPtX0zkG0+oUCV5kfQPUoW3W3C96Ag3Y/Clg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/otlp-exporter-base": "0.219.0", - "@opentelemetry/otlp-transformer": "0.219.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/sdk-metrics": "2.8.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-metrics": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -514,69 +459,16 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", - "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.8.0.tgz", - "integrity": "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.219.0.tgz", - "integrity": "sha512-DUS7XyIiEnoeccQUvuKy0G2/YqeKhpN8FVIrGbrLNIVMj10yeIFLRzRv0tibCI2kXXvlTTABVexGAk78wHk2ug==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.221.0.tgz", + "integrity": "sha512-YMF4LveY2I3yhw61rn6nmC9FE8U24IZHPeKU1Duc5+sbwjMd8FwZAwba318ImdThCg/HuVQvhm2y6bfgNPnfYg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/exporter-metrics-otlp-http": "0.219.0", - "@opentelemetry/otlp-exporter-base": "0.219.0", - "@opentelemetry/otlp-transformer": "0.219.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/sdk-metrics": "2.8.0" + "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -585,70 +477,17 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/resources": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", - "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-proto/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.8.0.tgz", - "integrity": "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.219.0.tgz", - "integrity": "sha512-BkDNv1UD6BscW19MxbAxVmSYSSFuyeqR6buV2/HTYqA7GrR0EbTFzqG6h86T3PtXmpdbsWjMGLDdjG2rikG27Q==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.221.0.tgz", + "integrity": "sha512-zXminlZedtq9LvOW64CnNkOqk15zV75k8JgtdTuWFge6+jk2m4GmAUm6L2eIiG1o2a2bZxXw2PDrszm+bps0IA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.8.0", - "@opentelemetry/otlp-exporter-base": "0.219.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", - "@opentelemetry/otlp-transformer": "0.219.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/sdk-trace-base": "2.8.0" + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -657,69 +496,16 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", - "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", - "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.219.0.tgz", - "integrity": "sha512-lF/LUBfhOFmxJa+SQsLN7ziV4MHa2pyKgOM6JNehSOfU+npjM4gwm9oIKEJrzrWcexMcqydiyoFy0XCb1Ql3wQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.221.0.tgz", + "integrity": "sha512-Z9i2T7vgZbWe9rSLYxXVIbeW+XyzUq4rZanW3ZyVNwVDqCsh0EJKUgBWWQ0CZfeuUA+RQPzKgJQHMuWAUnKqXw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/otlp-exporter-base": "0.219.0", - "@opentelemetry/otlp-transformer": "0.219.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/sdk-trace-base": "2.8.0" + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -728,57 +514,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", - "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", - "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, "node_modules/@opentelemetry/instrumentation": { "version": "0.221.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.221.0.tgz", @@ -797,13 +532,13 @@ } }, "node_modules/@opentelemetry/instrumentation-host-metrics": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-host-metrics/-/instrumentation-host-metrics-0.2.0.tgz", - "integrity": "sha512-NIttCEOLdg1ebbDiJpCf0Ly1OGIa10isesik+K2dnXy2P99q4muUFjpaLtTnhkENrt9SmR0Zrxzq7B+W/VNWyw==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-host-metrics/-/instrumentation-host-metrics-0.4.0.tgz", + "integrity": "sha512-jnFyX2sTn2B+9mjsL3qgAHzpxdaia4/FV2GSlf9QrpaiMi6O0M0lA9JZTsf4FTvuOwrIngCk+MeVXjsxgBXXyg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.219.0", + "@opentelemetry/instrumentation": "^0.221.0", "systeminformation": "^5.31.6" }, "engines": { @@ -813,37 +548,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-host-metrics/node_modules/@opentelemetry/api-logs": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", - "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-host-metrics/node_modules/@opentelemetry/instrumentation": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.219.0.tgz", - "integrity": "sha512-X5t7I8GyIO9rmGHwoedZLREpQqrF1WW2nxzNNym6HOKpFiE+rvqV3ngC0xcZVO2YwIGf3KKmRdWrYwdwz3H9RQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.219.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, "node_modules/@opentelemetry/instrumentation-http": { "version": "0.221.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.221.0.tgz", @@ -863,46 +567,15 @@ } }, "node_modules/@opentelemetry/instrumentation-runtime-node": { - "version": "0.32.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.32.0.tgz", - "integrity": "sha512-Jo1jSgrHlah3lPpGNPsIpF0q52D5uSLRJrztWUoPc1/Tli2ZWZ+cArgNtcdmiLuKhW21MwYbbcrNw1fbOPeR3A==", + "version": "0.34.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.34.0.tgz", + "integrity": "sha512-Yb3PcmuK/iIOWY49GSEcSGl7fR4r6UqhZnuy5EWvFaqKqqs9SYnQeyQUEAbnBnSougRpwFBDbdN1SSGcvMhbtQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.219.0", + "@opentelemetry/api-logs": "^0.221.0", "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.219.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/api-logs": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", - "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/instrumentation": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.219.0.tgz", - "integrity": "sha512-X5t7I8GyIO9rmGHwoedZLREpQqrF1WW2nxzNNym6HOKpFiE+rvqV3ngC0xcZVO2YwIGf3KKmRdWrYwdwz3H9RQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.219.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" + "@opentelemetry/instrumentation": "^0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -929,14 +602,14 @@ } }, "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.219.0.tgz", - "integrity": "sha512-zvIxQX/AZUVKDU+hCuYx+7UkiP7GRdnk1ZbFQRYzHvYp47cAWR4j3IhoPhV9KaeXEv2xdGq3IA6PnpzDmLcmSA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", + "integrity": "sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/otlp-transformer": "0.219.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -945,33 +618,17 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.219.0.tgz", - "integrity": "sha512-iIk/s8QQu39zpTrRRmsW/Eg3SE2+Hg8tLWepr2FLRgmwUpNd0IpCTLJEHJ77hpt4hgIS8MAh44UYI4xQPZwWlw==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.221.0.tgz", + "integrity": "sha512-rQDmNgyiGCTrescjnzH2ntVyUKVIq6I2UjuK8+stT/Xg0ZOT71FVJqwjFdspQl6Yol/Yqsut9bDo+ame8oTmDQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.8.0", - "@opentelemetry/otlp-exporter-base": "0.219.0", - "@opentelemetry/otlp-transformer": "0.219.0" + "@opentelemetry/core": "2.10.0", + "@opentelemetry/otlp-exporter-base": "0.221.0", + "@opentelemetry/otlp-transformer": "0.221.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -980,35 +637,19 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.219.0.tgz", - "integrity": "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", + "integrity": "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.219.0", - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/sdk-logs": "0.219.0", - "@opentelemetry/sdk-metrics": "2.8.0", - "@opentelemetry/sdk-trace-base": "2.8.0" + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", + "@opentelemetry/sdk-logs": "0.221.0", + "@opentelemetry/sdk-metrics": "2.10.0", + "@opentelemetry/sdk-trace": "2.10.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1017,87 +658,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/api-logs": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", - "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", - "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-metrics": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.8.0.tgz", - "integrity": "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.8.0.tgz", - "integrity": "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, "node_modules/@opentelemetry/resources": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz", diff --git a/package.json b/package.json index 14c10c4c..3c52a465 100644 --- a/package.json +++ b/package.json @@ -39,12 +39,12 @@ "@cap-js/sqlite": "^3", "@cap-js/telemetry": "file:.", "@grpc/grpc-js": "^1.9.14", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.219", - "@opentelemetry/exporter-metrics-otlp-proto": "^0.219", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.219", - "@opentelemetry/exporter-trace-otlp-proto": "^0.219", - "@opentelemetry/instrumentation-host-metrics": "^0.2.0", - "@opentelemetry/instrumentation-runtime-node": "^0.32.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.221", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.221", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.221", + "@opentelemetry/exporter-trace-otlp-proto": "^0.221", + "@opentelemetry/instrumentation-host-metrics": "^0.4.0", + "@opentelemetry/instrumentation-runtime-node": "^0.34.0", "@sap-cloud-sdk/http-client": "^4", "@sap/cds-mtxs": "^4", "axios": "^1.6.7", @@ -52,6 +52,9 @@ "oxfmt": "0.63.0", "vitest": "^4" }, + "overrides": { + "@opentelemetry/sdk-logs": "0.219.0" + }, "cds": { "requires": { "telemetry": { From 8f8485dcf485b017677863848dd8f93046ac7d0f Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:53:40 +0200 Subject: [PATCH 10/31] =?UTF-8?q?test:=20run=20full=20suite=20on=20HANA=20?= =?UTF-8?q?+=20fix=20two=20HANA=20span/metric=20bugs=20(#477=20=C2=A72,=20?= =?UTF-8?q?=C2=A75)=20(#481)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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). --- CHANGELOG.md | 2 + lib/metrics/queue.js | 29 +++- lib/tracing/trace.js | 4 +- test/bookshop/.cdsrc.json | 2 +- test/logging.test.js | 13 +- test/metrics-outbox-multitenant.test.js | 8 +- test/metrics-outbox.test.js | 103 ++++++++----- test/tracing-attributes.test.js | 3 - test/tracing-messaging-inboxed.test.js | 10 +- ...racing-messaging-persistent-outbox.test.js | 10 +- test/tracing-messaging.js | 94 ++++++++++-- test/tracing-mt.test.js | 3 + test/tracing-outboxed-batch.test.js | 135 +++++++++++------- test/tracing-remote-cloudsdk.test.js | 3 - test/tracing-remote-native.test.js | 3 - test/tracing-scheduled.test.js | 87 +++++++---- test/tracing.test.js | 67 +++++++-- vitest.config.mjs | 40 +++++- 18 files changed, 449 insertions(+), 167 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39154001..23ba12a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Fixed - Cloud SDK outbound requests are traced again (patch getter-only `@sap-cloud-sdk/http-client` exports via `Object.defineProperty`) +- Raw SQL no longer leaks into HANA INSERT `prepare` span names (now uses operation + table, matching SELECT) +- Queue `*_storage_time_in_seconds` metrics are now correct on HANA (timezone-naive `min`/`max` timestamp aggregates were parsed as local time, skewing the values by the machine's UTC offset) ## Version 2.0.1 - 2026-07-03 diff --git a/lib/metrics/queue.js b/lib/metrics/queue.js index d6cbaa37..eb800b4d 100644 --- a/lib/metrics/queue.js +++ b/lib/metrics/queue.js @@ -6,6 +6,26 @@ const LOG = cds.log('telemetry') const PERSISTENT_QUEUE_DB_NAME = 'cds.outbox.Messages' +// Parse a queue `timestamp` value to epoch millis, robust to the DB driver's format. +// Direct column reads return an ISO-8601 UTC string ("...Z"), but HANA's min()/max() +// aggregates return a timezone-naive string ("2026-08-13 22:50:41.2270000" — space +// separator, sub-ms digits, no zone). Passing that straight to `new Date()` parses it as +// LOCAL time, so storage-time gauges were off by the machine's UTC offset on HANA (e.g. 7200s +// in CEST). Normalize naive strings to UTC before parsing; ISO/Date/number inputs pass through. +function timestampToEpoch(ts) { + if (ts == null) return null + if (ts instanceof Date) return ts.getTime() + if (typeof ts === 'number') return ts + let s = String(ts).trim() + // Already zoned (ends with Z or ±HH:MM / ±HHMM)? leave as-is; otherwise treat as UTC. + if (!/[zZ]$|[+-]\d\d:?\d\d$/.test(s)) { + // "YYYY-MM-DD HH:MM:SS.fffffff" -> "YYYY-MM-DDTHH:MM:SS.fffZ" (trim sub-ms to 3 digits) + s = s.replace(' ', 'T').replace(/(\.\d{3})\d+$/, '$1') + 'Z' + } + const ms = Date.parse(s) + return Number.isNaN(ms) ? null : ms +} + async function collectLatestQueueInfo(queueEntity, serviceName, maxAttempts) { const coldEntriesRow = await SELECT.one .columns([{ func: 'count', args: [{ val: 1 }], as: 'cold_count' }]) @@ -111,14 +131,17 @@ function initQueueObservation(statistics) { batchResult.observe(observables.remainingEntries, stats.remainingEntries, observationAttributes) // 'maxTimestamp' holds the most recent timestamp - const minStorageTimeSeconds = stats.maxTimestamp ? Math.floor((now - new Date(stats.maxTimestamp)) / 1000) : 0 + const maxEpoch = timestampToEpoch(stats.maxTimestamp) + const minStorageTimeSeconds = maxEpoch ? Math.floor((now - maxEpoch) / 1000) : 0 batchResult.observe(observables.minStorageTimeSeconds, minStorageTimeSeconds, observationAttributes) - const medStorageTimeSeconds = stats.medTimestamp ? Math.floor((now - new Date(stats.medTimestamp)) / 1000) : 0 + const medEpoch = timestampToEpoch(stats.medTimestamp) + const medStorageTimeSeconds = medEpoch ? Math.floor((now - medEpoch) / 1000) : 0 batchResult.observe(observables.medStorageTimeSeconds, medStorageTimeSeconds, observationAttributes) // 'minTimestamp' holds the least recent timestamp - const maxStorageTimeSeconds = stats.minTimestamp ? Math.floor((now - new Date(stats.minTimestamp)) / 1000) : 0 + const minEpoch = timestampToEpoch(stats.minTimestamp) + const maxStorageTimeSeconds = minEpoch ? Math.floor((now - minEpoch) / 1000) : 0 batchResult.observe(observables.maxStorageTimeInSeconds, maxStorageTimeSeconds, observationAttributes) batchResult.observe(observables.incomingMessages, stats.incomingMessages, observationAttributes) diff --git a/lib/tracing/trace.js b/lib/tracing/trace.js index dc9d70db..feb85fb7 100644 --- a/lib/tracing/trace.js +++ b/lib/tracing/trace.js @@ -313,7 +313,9 @@ function trace(req, fn, that, args, opts = {}) { // Matches "@cap-js/ - " optionally followed by " ", // where is prepare | exec | stmt.. Covers both the sqlite/pg case // (SQL is already baked in) and the HANA-promisified case (SQL not yet appended). - const dbNameMatch = name.match(/^(@cap-js\/\w+ - (?:prepare|exec|stmt\.\w+))(?:\s.*)?$/) + // Note: [\s\S] (not .) so multi-line SQL — e.g. HANA's INSERT ... WITH SRC AS (...) — + // is matched and stripped too; `.` alone would miss it and leak the raw statement. + const dbNameMatch = name.match(/^(@cap-js\/\w+ - (?:prepare|exec|stmt\.\w+))(?:\s[\s\S]*)?$/) if (dbNameMatch && (options.attributes[ATTR_DB_OPERATION_NAME] || options.attributes[ATTR_DB_SQL_TABLE])) { const SQL_VERB = { READ: 'SELECT', CREATE: 'INSERT' } const op = options.attributes[ATTR_DB_OPERATION_NAME] diff --git a/test/bookshop/.cdsrc.json b/test/bookshop/.cdsrc.json index 606f66f5..7b046bcf 100644 --- a/test/bookshop/.cdsrc.json +++ b/test/bookshop/.cdsrc.json @@ -45,7 +45,7 @@ "telemetry": { "metrics": { "config": { - "exportIntervalMillis": 100 + "exportIntervalMillis": 1000 }, "_db_pool": false, "_queue": true, diff --git a/test/logging.test.js b/test/logging.test.js index db8d1298..84f06f3b 100644 --- a/test/logging.test.js +++ b/test/logging.test.js @@ -3,20 +3,19 @@ // REVISIT: even with profile "logging", cls_custom_fields from package.json wins process.env.cds_log = JSON.stringify({ cls_custom_fields: ['foo'] }) +// This test asserts the exported LogRecords only. Disable the tracing signal (no exporter → +// lib/tracing/index.js bails out early) so the queue SchedulingService's outbox-scan "elapsed +// times:" trace primer is never produced and can't land in the console.dir spy window. Without +// this, on HANA the outbox poll fires later than any fixed drain and the primer flakes the count. +process.env.cds_requires_telemetry_tracing = JSON.stringify({ exporter: false }) + const cds = require('@sap/cds') const { expect, GET } = cds.test(__dirname + '/bookshop', '--profile', 'logging') -const wait = require('node:timers/promises').setTimeout - describe('logging', () => { const admin = { auth: { username: 'alice' } } const { dir } = console - // The queue's SchedulingService runs an initial outbox scan on server "listening"; its - // telemetry "elapsed times:" trace primer is exported asynchronously and would otherwise - // land in the spy window below. Drain it once up front before installing the spy. - // REVISIT: replace this fixed wait by polling for the primer / an in-memory exporter (see #478). - beforeAll(() => wait(500)) beforeEach(() => { console.dir = vi.fn() }) diff --git a/test/metrics-outbox-multitenant.test.js b/test/metrics-outbox-multitenant.test.js index d97bc09f..e5115d18 100644 --- a/test/metrics-outbox-multitenant.test.js +++ b/test/metrics-outbox-multitenant.test.js @@ -38,12 +38,10 @@ async function expectEventually(assertion, { timeout = 10000, interval = 25 } = } } +// Multitenancy needs a bound BTP Service Manager (MTX) to provision per-tenant HDI containers. +// The HANA CI runs against a single pre-provisioned HDI container with no Service Manager, so this +// suite is excluded from the HANA job in vitest.config.mjs. It runs on sqlite (in-memory tenants). describe('queue metrics for multi tenant service', () => { - if (cds.version.split('.')[0] < 9) { - test.skip('skipping tests for cds version < 9', () => {}) - return - } - const T1 = 'tenant_1' const T2 = 'tenant_2' diff --git a/test/metrics-outbox.test.js b/test/metrics-outbox.test.js index ab9f0d95..dc079f6d 100644 --- a/test/metrics-outbox.test.js +++ b/test/metrics-outbox.test.js @@ -17,12 +17,35 @@ function metricValue(metric, queuedServiceName) { return latestDataPointValue(metric, { 'queue.name': queuedServiceName }) } +// Best-effort outbox clear that can NEVER hang the surrounding hook. On the shared HANA HDI +// container a background queue worker may be holding the connection pool (draining/retrying), +// so a bare `DELETE` can block indefinitely — which previously turned into a 100s hook timeout +// that starved the pool and cascaded into ECONNREFUSED for the NEXT test file's server. Race the +// DELETE against a short timeout and swallow errors: if it can't complete quickly, the leftover +// rows are handled by the next file's own beforeEach clear anyway. +async function clearOutbox(timeout = 5000) { + try { + await Promise.race([DELETE.from('cds.outbox.Messages'), wait(timeout)]) + } catch { + // pool draining / server shutting down — nothing left to clean matters + } +} + // State-based wait: force the wired meter provider to collect + export, then re-run the assertion // block. Replaces all fixed-time `wait(150)` sleeps — the loop completes the instant the in-memory -// queue statistics (kept fresh by the existing cds.spawn poller) reflect the asserted state. +// queue statistics (kept fresh by the queue-stats cds.spawn poller) reflect the asserted state. // forceFlush() throws fast if the provider isn't wired, so a misconfigured profile fails loudly // instead of busy-spinning the full timeout. -async function expectEventually(assertion, { timeout = 10000, interval = 25 } = {}) { +// +// interval is 500ms (NOT a few ms): each forceFlush() triggers a metric collection that runs the +// queue-stats poller's SELECTs against the DB. On the SHARED HANA HDI container a tight poll loop +// (plus the profile's background export) starves the queue worker of connections, so its retries +// stall and delivery never completes — which manifested as `expected N to be at least M` flakes +// and, via ensuing hook hangs + pool exhaustion, ECONNREFUSED cascades into later files' servers. +// Polling at 500ms (with the profile's exportIntervalMillis raised to 1000ms) leaves the worker +// enough DB headroom to make all its attempts. The loop still returns the instant the state holds, +// so sqlite (per-file in-memory DB) still satisfies in well under a second. +async function expectEventually(assertion, { timeout = 30000, interval = 500 } = {}) { const start = Date.now() let lastError while (true) { @@ -41,11 +64,6 @@ async function expectEventually(assertion, { timeout = 10000, interval = 25 } = const debugLog = (cds.log('telemetry').debug = vi.fn(() => {})) describe('queue metrics for single tenant service', () => { - if (cds.version.split('.')[0] < 9) { - test.skip('skipping tests for cds version < 9', () => {}) - return - } - let totalInc = { [E1]: 0, [E2]: 0 } let totalOut = { [E1]: 0, [E2]: 0 } let totalFailed = { [E1]: 0, [E2]: 0 } @@ -88,11 +106,26 @@ describe('queue metrics for single tenant service', () => { }) beforeEach(async () => { - await DELETE.from('cds.outbox.Messages') + await clearOutbox() reset() debugLog.mockClear() }) + // Leave the shared DB clean for the next test file and let background queue workers settle. + // On HANA all files share one HDI container, so (a) the undeliverable `unknown-service` row + // inserted by the last case below would otherwise linger and skew another file's queue metrics, + // and (b) an in-flight worker retrying a message could fire this file's `before('call')` handler + // during teardown. Clear, wait a beat for any in-flight worker iteration to finish, clear again. + // Every clear is timeout-bounded (clearOutbox) so a draining pool can't hang the hook. HANA-only: + // sqlite gets a fresh in-memory DB per file, so the settle is pointless there. + afterAll(async () => { + await clearOutbox() + if (process.env.TELEMETRY_TEST_HANA) { + await wait(5000) + await clearOutbox() + } + }) + describe('given the target service succeeds immediately', () => { test('metrics are collected', async () => { await GET('/odata/v4/proxy/proxyCallToExternalServiceOne', admin) @@ -124,7 +157,15 @@ describe('queue metrics for single tenant service', () => { }) describe('given a target service that requires retries', () => { - let currentRetryCount, customizedHandler + // Initialized at declaration (not left undefined): the `before('call')` handler registered in + // beforeAll stays live for the whole describe, so a background queue-worker retry can fire it + // OUTSIDE any test's window (between tests, or during teardown). If currentRetryCount were + // undefined then, `currentRetryCount[E]` throws — the queue logs "Programming error detected" + // and the delivery the test expects never completes. On HANA the slower retry cadence + pool + // drain at teardown reliably hits that gap; sqlite's timing never exposed it. beforeEach still + // re-zeroes it per test. + let currentRetryCount = { [E1]: 0, [E2]: 0 } + let customizedHandler // Fail the first 3 attempts so the 4th delivers. With the queue's exp-backoff schedule // (0.5s, 1.25s, 2.375s, ...), this places the 4th attempt at ~t=4.1s after enqueue — @@ -166,30 +207,26 @@ describe('queue metrics for single tenant service', () => { // Reference time taken after GETs return — i.e. after both messages are persisted in the outbox. const timeOfInitialCall = Date.now() - // The queue has made its first delivery attempt for both services (handler invocation count is - // observed directly via the rejecting `before('call')` handler — pure CAP event observation). - await expectEventually(() => { - expect(currentRetryCount[E1]).to.be.gte(1) - expect(currentRetryCount[E2]).to.be.gte(1) - - expect(metricValue('cold_entries', E1)).to.eq(0) - expect(metricValue('remaining_entries', E1)).to.eq(1) - expect(metricValue('incoming_messages', E1)).to.eq(totalInc[E1]) - expect(metricValue('outgoing_messages', E1)).to.eq(totalOut[E1]) - expect(metricValue('processing_failures', E1)).to.eq(totalFailed[E1]) - expect(metricValue('min_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E1)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E1)).to.eq(0) - - expect(metricValue('cold_entries', E2)).to.eq(0) - expect(metricValue('remaining_entries', E2)).to.eq(1) - expect(metricValue('incoming_messages', E2)).to.eq(totalInc[E2]) - expect(metricValue('outgoing_messages', E2)).to.eq(totalOut[E2]) - expect(metricValue('processing_failures', E2)).to.eq(totalFailed[E2]) - expect(metricValue('min_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('med_storage_time_in_seconds', E2)).to.eq(0) - expect(metricValue('max_storage_time_in_seconds', E2)).to.eq(0) - }) + // Freshly-enqueued state: each message is present (remaining == 1) and not cold. We assert + // each service in its OWN poll (E1 and E2 stagger; coupling them risks one aging out before + // the other aligns). Storage_time is asserted as a small upper bound rather than exactly 0: + // the "just enqueued, ~0s old" state is a sub-second transient and on HANA the queue-stats + // poller's first observation already lands with storage_time >= 1 (poll interval + query + // latency), so `== 0` is not reliably observable. The `< 60` bound still guards the timezone + // regression this suite covers (a naive-timestamp misparse reported storage_time as ~7200s); + // storage-time GROWTH is asserted in the next block, delivery/removal in the one after. + const assertFreshlyEnqueued = E => + expectEventually(() => { + expect(metricValue('cold_entries', E)).to.eq(0) + expect(metricValue('remaining_entries', E)).to.eq(1) + expect(metricValue('incoming_messages', E)).to.eq(totalInc[E]) + expect(metricValue('outgoing_messages', E)).to.eq(totalOut[E]) + expect(metricValue('processing_failures', E)).to.eq(totalFailed[E]) + expect(metricValue('min_storage_time_in_seconds', E)).to.be.lessThan(60) + expect(metricValue('med_storage_time_in_seconds', E)).to.be.lessThan(60) + expect(metricValue('max_storage_time_in_seconds', E)).to.be.lessThan(60) + }) + await Promise.all([assertFreshlyEnqueued(E1), assertFreshlyEnqueued(E2)]) // The storage_time gauges need a real second to elapse since the messages were enqueued — // this is the one place the test fundamentally depends on wall-clock time. diff --git a/test/tracing-attributes.test.js b/test/tracing-attributes.test.js index 5972584f..5b539128 100644 --- a/test/tracing-attributes.test.js +++ b/test/tracing-attributes.test.js @@ -41,9 +41,6 @@ describe('tracing attributes', () => { afterAll(() => new Promise(resolve => server.close(resolve))) test('HTTP client attributes are set on remote service span', async () => { - // skip for cds 8 due to Cloud SDK resilience module resolution issues in test environment - if (Number(cds.version.split('.')[0]) < 9) return - // configure destination URL directly on credentials cds.env.requires.TestRemote = { kind: 'odata', credentials: { url: `http://localhost:${port}` } } const remote = await cds.connect.to('TestRemote') diff --git a/test/tracing-messaging-inboxed.test.js b/test/tracing-messaging-inboxed.test.js index a5ec21e3..bd5fdb3d 100644 --- a/test/tracing-messaging-inboxed.test.js +++ b/test/tracing-messaging-inboxed.test.js @@ -54,13 +54,15 @@ const CHECK = ({ expect, rootSpans, groupedByTrace }) => { expect(rootSpans.length).to.be.lte(5) } -const cds = require('@sap/cds') - describe(`tracing messaging - ${CASE}`, () => { // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. - if (cds.env.requires.db?.kind === 'sqlite') { + // Detect the DB via the env var set by vitest.config.mjs for the HANA job, NOT via cds.env: + // reading cds.env at collection time would freeze the singleton before cds.test() applies its + // `--profile`, so the tracer provider would be built with the default ConsoleSpanExporter and + // MyInMemorySpanExporter would never receive spans. + if (!process.env.TELEMETRY_TEST_HANA) { test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) return } - require('./tracing-messaging')(CASE, CHECK, { waitMs: 4000 }) + require('./tracing-messaging')(CASE, CHECK) }) diff --git a/test/tracing-messaging-persistent-outbox.test.js b/test/tracing-messaging-persistent-outbox.test.js index 82b89109..708e8802 100644 --- a/test/tracing-messaging-persistent-outbox.test.js +++ b/test/tracing-messaging-persistent-outbox.test.js @@ -94,13 +94,15 @@ const CHECK = ({ expect, rootSpans, groupedByTrace }) => { } } -const cds = require('@sap/cds') - describe(`tracing messaging - ${CASE}`, () => { // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. - if (cds.env.requires.db?.kind === 'sqlite') { + // Detect the DB via the env var set by vitest.config.mjs for the HANA job, NOT via cds.env: + // reading cds.env at collection time would freeze the singleton before cds.test() applies its + // `--profile`, so the tracer provider would be built with the default ConsoleSpanExporter and + // MyInMemorySpanExporter would never receive spans. + if (!process.env.TELEMETRY_TEST_HANA) { test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) return } - require('./tracing-messaging')(CASE, CHECK, { waitMs: 4000 }) + require('./tracing-messaging')(CASE, CHECK) }) diff --git a/test/tracing-messaging.js b/test/tracing-messaging.js index 15202e34..147fc594 100644 --- a/test/tracing-messaging.js +++ b/test/tracing-messaging.js @@ -1,12 +1,66 @@ -module.exports = (CASE, CHECK, { waitMs = 4000 } = {}) => { +module.exports = (CASE, CHECK) => { const cds = require('@sap/cds') const { expect, POST } = cds.test(__dirname + '/bookshop', '--profile', `${CASE},tracing-in-memory`) - const { reset, rootSpans, groupedByTrace, captured } = require('./bookshop/lib/MyInMemorySpanExporter') + const { reset, groupedByTrace, captured } = require('./bookshop/lib/MyInMemorySpanExporter') + const otel = require('@opentelemetry/api') const wait = require('node:timers/promises').setTimeout + // Best-effort outbox clear that can NEVER hang the surrounding hook. On the shared HANA HDI + // container a background queue worker may hold the connection pool, so a bare DELETE can block + // indefinitely — which would turn into a hook timeout that starves the pool and cascades into + // ECONNREFUSED for the next file's server. Race the DELETE against a short timeout; leftover + // rows are cleared by the next file's own beforeEach anyway. + async function clearOutbox(timeout = 5000) { + try { + await Promise.race([DELETE.from('cds.outbox.Messages'), wait(timeout)]) + } catch { + // pool draining during shutdown — nothing left to clean matters + } + } + + // Force-flush the tracer provider's span processor so any spans buffered by background + // queue-worker activity are exported into `captured`. The global provider is a + // ProxyTracerProvider (no forceFlush) whose delegate is the real NodeTracerProvider; + // guard for the no-op provider so a misconfigured profile fails loudly, not silently. + async function flushSpans() { + const provider = otel.trace.getTracerProvider() + const delegate = provider.getDelegate?.() ?? provider + if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() + } + + // State-based wait: repeatedly flush + re-run the assertion until it holds or times out. + // Replaces the fixed `wait(waitMs)` sleep that flakes on HANA, where the two queue workers + // flush their spans well after any reasonable fixed window. + async function eventually(fn, { timeout = 15000, interval = 50 } = {}) { + const start = Date.now() + let lastError + while (true) { + await flushSpans() + try { + await fn() + return + } catch (err) { + lastError = err + if (Date.now() - start >= timeout) throw lastError + await wait(interval) + } + } + } + const admin = { auth: { username: 'alice' } } + // The queue scheduler periodically scans `cds.outbox.Messages` in its own `db - tx` (a + // SELECT + optional UPDATE that finds nothing to dispatch). On HANA these bookkeeping scans + // land as extra root traces that have nothing to do with the emit under test — and because + // the single HDI container is shared across all test files, scans triggered by other files' + // lingering workers show up too. Filter those pure outbox-scan traces so the CHECKs' exact + // root-count assertions stay stable. A scan trace is a `db - tx` root whose every span only + // touches `cds.outbox.Messages` (no application entity, no messaging/handle span). + const isOutboxScanTrace = g => + g.root.name === 'db - tx' && g.all.every(s => s.name === 'db - tx' || s.name.includes('cds.outbox.Messages')) + const meaningful = groups => groups.filter(g => !isOutboxScanTrace(g)) + const rm = () => { try { require('fs').rmSync(require('path').join(__dirname, CASE)) @@ -21,22 +75,40 @@ module.exports = (CASE, CHECK, { waitMs = 4000 } = {}) => { }) afterAll(async () => { - // Wait long enough for any background queue-worker / scheduling-service timers to - // fire one last time before jest tears down the env. Without this, those timers can - // fire after teardown and crash with "cds.error.isSystemError is not a function" - // (cds module is reloaded between tests, but the timer references the old instance). - await wait(2000) + // On the shared HANA HDI container, a still-draining background queue worker from THIS file + // would dispatch into the NEXT file's run and add foreign `cds.spawn - run task` roots that + // break its exact root-count CHECKs. Clear the shared outbox, let the last worker settle, then + // clear again. Every clear is timeout-bounded (clearOutbox) so a draining pool can't hang the + // hook. HANA-only: sqlite gets a fresh in-memory DB per file, so the settle is pointless there. + if (process.env.TELEMETRY_TEST_HANA) { + await clearOutbox() + await wait(5000) + await clearOutbox() + } rm() }) - beforeEach(() => { + beforeEach(async () => { + // Clear any outbox rows left behind by a prior test file BEFORE resetting the span buffer. + // The single HANA HDI container is shared across all files, so a leftover message would be + // dispatched by THIS file's queue worker — producing a foreign `cds.spawn - run task` root + // that breaks the exact root-count CHECKs. Reset AFTER so the DELETE's own spans aren't + // captured. (No-op on sqlite, where each file gets its own in-memory DB.) + await clearOutbox() reset() }) test('emit is traced', async () => { await POST('/odata/v4/admin/test_emit', {}, admin) - await wait(waitMs) - // CHECK is called with span-level data: { expect, rootSpans, groupedByTrace, captured, cds } - CHECK({ expect, rootSpans: rootSpans(), groupedByTrace: groupedByTrace(), captured: [...captured], cds }) + // Poll (flush + re-check) until both queue workers have run and exported their spans; + // on HANA the worker latency exceeds any reasonable fixed sleep. Pass the meaningful + // (non-outbox-scan) traces so the CHECK's exact root-count assertions aren't thrown off by + // the scheduler's bookkeeping scans on the shared HANA container. + await eventually(() => { + const groups = meaningful(groupedByTrace()) + const roots = groups.flatMap(g => g.roots) + // CHECK is called with span-level data: { expect, rootSpans, groupedByTrace, captured, cds } + CHECK({ expect, rootSpans: roots, groupedByTrace: groups, captured: [...captured], cds }) + }) }) } diff --git a/test/tracing-mt.test.js b/test/tracing-mt.test.js index 872a6dc2..8aa91e74 100644 --- a/test/tracing-mt.test.js +++ b/test/tracing-mt.test.js @@ -4,6 +4,9 @@ const { expect, GET } = cds.test('serve', '--in-memory', '--project', __dirname const { reset, captured } = require('./bookshop/lib/MyInMemorySpanExporter') +// Multitenancy needs a bound BTP Service Manager (MTX) to provision per-tenant HDI containers. +// The HANA CI runs against a single pre-provisioned HDI container with no Service Manager, so this +// suite is excluded from the HANA job in vitest.config.mjs. It runs on sqlite (in-memory tenants). describe('tracing with multitenancy', () => { const TENANT1 = 'tenant_1' const TENANT2 = 'tenant_2' diff --git a/test/tracing-outboxed-batch.test.js b/test/tracing-outboxed-batch.test.js index d55993ec..a1d8f9bc 100644 --- a/test/tracing-outboxed-batch.test.js +++ b/test/tracing-outboxed-batch.test.js @@ -6,16 +6,46 @@ const cds = require('@sap/cds') const { expect, POST } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'tracing-in-memory') const { reset, captured, groupedByTrace } = require('./bookshop/lib/MyInMemorySpanExporter') const { hrTimeToNanoseconds } = require('@opentelemetry/core') +const otel = require('@opentelemetry/api') const wait = require('node:timers/promises').setTimeout -describe('tracing for outboxed batch (chunk-size fan-out)', () => { - if (Number(cds.version.split('.')[0]) < 9) { - test.skip('skipping for cds < 9', () => {}) - return +// Force-flush the tracer provider's span processor so any spans buffered by background +// outbox/queue activity are exported into `captured`. The global provider is a +// ProxyTracerProvider (no forceFlush) whose delegate is the real NodeTracerProvider; +// guard for the no-op provider so a misconfigured profile fails loudly, not silently. +async function flushSpans() { + const provider = otel.trace.getTracerProvider() + const delegate = provider.getDelegate?.() ?? provider + if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() +} + +// State-based wait: repeatedly flush + re-run the assertion until it holds or times out. +// Replaces fixed `wait(...)` sleeps that flake on HANA, where background work flushes spans +// after the sleep window. +async function eventually(fn, { timeout = 15000, interval = 50 } = {}) { + const start = Date.now() + let lastError + while (true) { + await flushSpans() + try { + await fn() + return + } catch (err) { + lastError = err + if (Date.now() - start >= timeout) throw lastError + await wait(interval) + } } +} + +describe('tracing for outboxed batch (chunk-size fan-out)', () => { // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. - if (cds.env.requires.db?.kind === 'sqlite') { + // Detect the DB via the env var set by vitest.config.mjs for the HANA job, NOT via cds.env: + // reading cds.env at collection time would freeze the singleton before cds.test() applies its + // `--profile`, so the tracer provider would be built with the default ConsoleSpanExporter and + // MyInMemorySpanExporter would never receive spans (captured stays empty). + if (!process.env.TELEMETRY_TEST_HANA) { test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) return } @@ -25,56 +55,65 @@ describe('tracing for outboxed batch (chunk-size fan-out)', () => { externalOne.on('call', () => 'ok') }) - beforeEach(reset) + beforeEach(async () => { + // Clear outbox rows left by a prior test file BEFORE resetting the span buffer — the HANA + // HDI container is shared across all files, so a leftover message would be dispatched by this + // file's worker and add a foreign `cds.spawn - run task` root. Reset AFTER so the DELETE's own + // spans aren't captured. (No-op on sqlite: per-file in-memory DB.) + await DELETE.from('cds.outbox.Messages') + reset() + }) test('three queued sends produce parallel dispatch spans under one worker root', async () => { await POST('/odata/v4/admin/test_outboxed_send_batch', {}, { auth: { username: 'alice' } }) - await wait(2500) - // Producer wrote three rows to the outbox. - const upserts = captured.filter(s => s.name === 'db - UPSERT cds.outbox.Messages') - expect(upserts.length, 'expected three producer outbox UPSERTs').to.be.gte(3) + await eventually(() => { + // Producer wrote three rows to the outbox. + const upserts = captured.filter(s => s.name === 'db - UPSERT cds.outbox.Messages') + expect(upserts.length, 'expected three producer outbox UPSERTs').to.be.gte(3) - // Look for a queue worker root containing multiple dispatch tx spans. - const workerTrace = groupedByTrace().find( - g => g.root.name === 'cds.spawn - run task' && g.all.filter(s => s.name === 'ExternalServiceOne - tx').length >= 2 - ) - expect(workerTrace, 'expected a worker trace with multiple ExternalServiceOne - tx children').to.exist + // Look for a queue worker root containing multiple dispatch tx spans. + const workerTrace = groupedByTrace().find( + g => + g.root.name === 'cds.spawn - run task' && g.all.filter(s => s.name === 'ExternalServiceOne - tx').length >= 2 + ) + expect(workerTrace, 'expected a worker trace with multiple ExternalServiceOne - tx children').to.exist - // The worker root must have exactly one lock tx (db - tx with READ + UPDATE)… - const lockTxs = workerTrace.all.filter( - s => - s.name === 'db - tx' && - workerTrace.all.some( - c => c.parentSpanContext?.spanId === s.spanContext().spanId && c.name === 'db - READ cds.outbox.Messages' - ) - ) - expect(lockTxs, 'expected one lock tx (db - tx with READ + UPDATE)').to.have.lengthOf(1) + // The worker root must have exactly one lock tx (db - tx with READ + UPDATE)… + const lockTxs = workerTrace.all.filter( + s => + s.name === 'db - tx' && + workerTrace.all.some( + c => c.parentSpanContext?.spanId === s.spanContext().spanId && c.name === 'db - READ cds.outbox.Messages' + ) + ) + expect(lockTxs, 'expected one lock tx (db - tx with READ + UPDATE)').to.have.lengthOf(1) - // …and multiple dispatch txs, each containing an ExternalServiceOne handle span + DELETE. - const dispatchTxs = workerTrace.all.filter(s => s.name === 'ExternalServiceOne - tx') - expect(dispatchTxs.length, 'expected multiple dispatch txs (chunk-size fan-out)').to.be.gte(2) - for (const tx of dispatchTxs) { - const kids = workerTrace.all.filter(k => k.parentSpanContext?.spanId === tx.spanContext().spanId) - expect( - kids.some(k => k.name.match(/ExternalServiceOne - handle/)), - 'dispatch tx should contain handle call' - ).to.be.true - expect( - kids.some(k => k.name === 'db - DELETE cds.outbox.Messages'), - 'dispatch tx should contain DELETE' - ).to.be.true - } + // …and multiple dispatch txs, each containing an ExternalServiceOne handle span + DELETE. + const dispatchTxs = workerTrace.all.filter(s => s.name === 'ExternalServiceOne - tx') + expect(dispatchTxs.length, 'expected multiple dispatch txs (chunk-size fan-out)').to.be.gte(2) + for (const tx of dispatchTxs) { + const kids = workerTrace.all.filter(k => k.parentSpanContext?.spanId === tx.spanContext().spanId) + expect( + kids.some(k => k.name.match(/ExternalServiceOne - handle/)), + 'dispatch tx should contain handle call' + ).to.be.true + expect( + kids.some(k => k.name === 'db - DELETE cds.outbox.Messages'), + 'dispatch tx should contain DELETE' + ).to.be.true + } - // The dispatch txs should overlap in time (parallel), not be strictly sequential. - if (dispatchTxs.length >= 2) { - const sorted = [...dispatchTxs].sort( - (a, b) => hrTimeToNanoseconds(a.startTime) - hrTimeToNanoseconds(b.startTime) - ) - const firstEndNs = hrTimeToNanoseconds(sorted[0].endTime) - const secondStartNs = hrTimeToNanoseconds(sorted[1].startTime) - // Parallel: second starts before first ends (allow a tiny slack). - expect(secondStartNs, 'expected parallel dispatch: task2 starts before task1 ends').to.be.lessThan(firstEndNs) - } + // The dispatch txs should overlap in time (parallel), not be strictly sequential. + if (dispatchTxs.length >= 2) { + const sorted = [...dispatchTxs].sort( + (a, b) => hrTimeToNanoseconds(a.startTime) - hrTimeToNanoseconds(b.startTime) + ) + const firstEndNs = hrTimeToNanoseconds(sorted[0].endTime) + const secondStartNs = hrTimeToNanoseconds(sorted[1].startTime) + // Parallel: second starts before first ends (allow a tiny slack). + expect(secondStartNs, 'expected parallel dispatch: task2 starts before task1 ends').to.be.lessThan(firstEndNs) + } + }) }) }) diff --git a/test/tracing-remote-cloudsdk.test.js b/test/tracing-remote-cloudsdk.test.js index 3708cecf..4adfcede 100644 --- a/test/tracing-remote-cloudsdk.test.js +++ b/test/tracing-remote-cloudsdk.test.js @@ -8,9 +8,6 @@ const http = require('http') // export so the outbound call produces a @cap-js/telemetry CLIENT span carrying // the sap.btp.destination attribute. describe('tracing remote via cloud sdk', () => { - // cloud-sdk resilience module resolution has issues on cds 8 - if (Number(cds.version.split('.')[0]) < 9) return - const log = vi.spyOn(console, 'dir') beforeEach(log.mockClear) diff --git a/test/tracing-remote-native.test.js b/test/tracing-remote-native.test.js index 36c00beb..f930a5d8 100644 --- a/test/tracing-remote-native.test.js +++ b/test/tracing-remote-native.test.js @@ -12,9 +12,6 @@ const http = require('http') // comes from that instrumentation scope (NOT @opentelemetry/instrumentation-http, and // NOT our cloud_sdk wrapper) and carries the standard http.* / url.* / server.* attributes. describe('tracing remote via native fetch', () => { - // cloud-sdk resilience module resolution has issues on cds 8 - if (Number(cds.version.split('.')[0]) < 9) return - const log = vi.spyOn(console, 'dir') beforeEach(log.mockClear) diff --git a/test/tracing-scheduled.test.js b/test/tracing-scheduled.test.js index a5928956..aa9c2a49 100644 --- a/test/tracing-scheduled.test.js +++ b/test/tracing-scheduled.test.js @@ -21,19 +21,49 @@ const cds = require('@sap/cds') const { expect, POST } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'tracing-in-memory') const { reset, captured, groupedByTrace, rootSpans } = require('./bookshop/lib/MyInMemorySpanExporter') +const otel = require('@opentelemetry/api') const wait = require('node:timers/promises').setTimeout -describe('tracing for scheduled tasks', () => { - if (Number(cds.version.split('.')[0]) < 9) { - test.skip('skipping for cds < 9', () => {}) - return +// Force-flush the tracer provider's span processor so any spans buffered by background +// queue/worker activity are exported into `captured`. The global provider is a +// ProxyTracerProvider (no forceFlush) whose delegate is the real NodeTracerProvider; +// guard for the no-op provider so a misconfigured profile fails loudly, not silently. +async function flushSpans() { + const provider = otel.trace.getTracerProvider() + const delegate = provider.getDelegate?.() ?? provider + if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() +} + +// State-based wait: repeatedly flush + re-run the assertion until it holds or times out. +// Replaces fixed `wait(...)` sleeps that flake on HANA, where the worker flushes spans after +// the sleep window. +async function eventually(fn, { timeout = 15000, interval = 50 } = {}) { + const start = Date.now() + let lastError + while (true) { + await flushSpans() + try { + await fn() + return + } catch (err) { + lastError = err + if (Date.now() - start >= timeout) throw lastError + await wait(interval) + } } +} + +describe('tracing for scheduled tasks', () => { // Queue-worker spans (cds.spawn - run task root) require @sap/cds to route the sqlite // queue worker through cds.spawn. Published cds uses a raw setTimeout bypass on sqlite // (to avoid a single-writer deadlock), so those spans never appear. Skip until the cds // fix lands (cap/cds test/queue-spawn-sqlite-extended-tenant). REMOVE with follow-up PR. - if (cds.env.requires.db?.kind === 'sqlite') { + // Detect the DB via the env var set by vitest.config.mjs for the HANA job, NOT via cds.env: + // reading cds.env at collection time would freeze the singleton before cds.test() applies its + // `--profile`, so the tracer provider would be built with the default ConsoleSpanExporter and + // MyInMemorySpanExporter would never receive spans (captured stays empty). + if (!process.env.TELEMETRY_TEST_HANA) { test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) return } @@ -43,31 +73,40 @@ describe('tracing for scheduled tasks', () => { externalOne.on('call', () => 'ok') }) - beforeEach(reset) + beforeEach(async () => { + // Clear outbox rows left by a prior test file BEFORE resetting the span buffer — the HANA + // HDI container is shared across all files, so a leftover message would be dispatched by this + // file's worker and add a foreign `cds.spawn - run task` root. Reset AFTER so the DELETE's own + // spans aren't captured. (No-op on sqlite: per-file in-memory DB.) + await DELETE.from('cds.outbox.Messages') + reset() + }) test('schedule .after() is fully traced through the queue worker', async () => { await POST('/odata/v4/admin/test_scheduled', {}, { auth: { username: 'alice' } }) - // wait long enough for the scheduled task to fire (10ms after-delay + worker latency) - await wait(1500) - // Producer trace: writes the task row inside the HTTP request tx. - const producer = groupedByTrace().find(g => g.all.some(s => s.name === 'AdminService - handle test_scheduled')) - expect(producer, 'expected a producer trace').to.exist - expect(producer.root.name).to.equal('AdminService - tx') - expect(producer.all.some(s => s.name === 'db - UPSERT cds.outbox.Messages')).to.be.true - expect(producer.all.some(s => s.name === 'cds.spawn - schedule task')).to.be.true + // Poll (flush + re-check) until the scheduled task has fired and all spans have been + // exported; on HANA the worker latency exceeds any reasonable fixed sleep. + await eventually(() => { + // Producer trace: writes the task row inside the HTTP request tx. + const producer = groupedByTrace().find(g => g.all.some(s => s.name === 'AdminService - handle test_scheduled')) + expect(producer, 'expected a producer trace').to.exist + expect(producer.root.name).to.equal('AdminService - tx') + expect(producer.all.some(s => s.name === 'db - UPSERT cds.outbox.Messages')).to.be.true + expect(producer.all.some(s => s.name === 'cds.spawn - schedule task')).to.be.true - // Queue worker trace: rooted at cds.spawn - run task, contains both tx spans. - const workerTrace = groupedByTrace().find(g => g.root.name === 'cds.spawn - run task') - expect(workerTrace, 'expected a queue-worker spawn-root trace').to.exist - expect(workerTrace.all.some(s => s.name === 'db - tx')).to.be.true - expect(workerTrace.all.some(s => s.name === 'ExternalServiceOne - tx')).to.be.true + // Queue worker trace: rooted at cds.spawn - run task, contains both tx spans. + const workerTrace = groupedByTrace().find(g => g.root.name === 'cds.spawn - run task') + expect(workerTrace, 'expected a queue-worker spawn-root trace').to.exist + expect(workerTrace.all.some(s => s.name === 'db - tx')).to.be.true + expect(workerTrace.all.some(s => s.name === 'ExternalServiceOne - tx')).to.be.true - // The ExternalServiceOne handler was invoked. - expect(captured.some(s => s.name.match(/ExternalServiceOne - handle/))).to.be.true + // The ExternalServiceOne handler was invoked. + expect(captured.some(s => s.name.match(/ExternalServiceOne - handle/))).to.be.true - // Total meaningful roots: producer + worker (+ optional bookkeeping scan). - expect(rootSpans().length).to.be.gte(2) - expect(rootSpans().length).to.be.lte(3) + // Total meaningful roots: producer + worker (+ optional bookkeeping scan). + expect(rootSpans().length).to.be.gte(2) + expect(rootSpans().length).to.be.lte(3) + }) }) }) diff --git a/test/tracing.test.js b/test/tracing.test.js index b1cd985a..d81806c9 100644 --- a/test/tracing.test.js +++ b/test/tracing.test.js @@ -9,10 +9,50 @@ const { expect, GET, POST } = cds.test(__dirname + '/bookshop', '--profile', 'tr // Assert against the structured ReadableSpan objects captured by MyInMemorySpanExporter // (configured via the tracing-in-memory profile in test/bookshop/.cdsrc.json) — no // console spying, no string-regex matching of formatted output. -const { reset, rootSpans, captured } = require('./bookshop/lib/MyInMemorySpanExporter') +const { reset, rootSpans, groupedByTrace, captured } = require('./bookshop/lib/MyInMemorySpanExporter') +const otel = require('@opentelemetry/api') const wait = require('node:timers/promises').setTimeout +// Force-flush the tracer provider's span processor so any spans buffered by background +// activity are exported into `captured`. The global provider is a ProxyTracerProvider (no +// forceFlush) whose delegate is the real NodeTracerProvider; guard for the no-op provider +// so a misconfigured profile fails loudly, not silently. +async function flushSpans() { + const provider = otel.trace.getTracerProvider() + const delegate = provider.getDelegate?.() ?? provider + if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() +} + +// State-based wait: repeatedly flush + re-run the assertion until it holds or times out. +// Replaces fixed `wait(...)` sleeps that flake on HANA, where spawned/emitted work flushes +// spans after the sleep window. +async function eventually(fn, { timeout = 15000, interval = 50 } = {}) { + const start = Date.now() + let lastError + while (true) { + await flushSpans() + try { + await fn() + return + } catch (err) { + lastError = err + if (Date.now() - start >= timeout) throw lastError + await wait(interval) + } + } +} + +// On HANA the persistent-outbox queue poller periodically scans `cds.outbox.Messages` in its +// own `db - tx`, producing an extra root trace that is unrelated to what these tests exercise. +// Filter those bookkeeping traces out so root-count assertions stay stable across both DBs. +const isOutboxScanTrace = g => + g.root.name === 'db - tx' && g.all.every(s => s.name === 'db - tx' || s.name.includes('cds.outbox.Messages')) +const meaningfulRoots = () => + groupedByTrace() + .filter(g => !isOutboxScanTrace(g)) + .flatMap(g => g.roots) + describe('tracing', () => { const admin = { auth: { username: 'alice' } } @@ -42,13 +82,19 @@ describe('tracing', () => { }) test('NonRecordingSpans are handled correctly', async () => { + // Idempotent cleanup: this file has no data.reset, and on the persistent HANA container a + // leftover Author 42 from a prior run would make the POST fail with a unique-constraint 500. + await DELETE.from('sap.capire.bookshop.Authors').where({ ID: 42 }) + reset() const { status: postStatus } = await POST('/odata/v4/admin/Authors', { ID: 42, name: 'Douglas Adams' }, admin) expect(postStatus).to.equal(201) const { status: getStatus } = await GET('/odata/v4/admin/Authors?$select=ID', admin) expect(getStatus).to.equal(200) // The sampler in this test ignores /odata/v4/admin/Authors — no spans should be captured for it. // (Other unrelated background work may still produce spans; assert only that none mention Authors.) - expect(captured.filter(s => s.attributes['url.path']?.includes('/admin/Authors'))).to.have.lengthOf(0) + await eventually(() => { + expect(captured.filter(s => s.attributes['url.path']?.includes('/admin/Authors'))).to.have.lengthOf(0) + }) }) // REVISIT: jest breaks otel's patching of incoming request handling -> behavior to test not reproducible @@ -68,23 +114,23 @@ describe('tracing', () => { // With the tx wrap (lib/tracing/cds.js), each batch request's tx becomes a single root — // the previously-visible 4 sub-roots (POST: CREATE + read-after-write; GET: read actives + // read drafts) are now nested under 2 root tx spans, one per batch entry. - expect(rootSpans()).to.have.lengthOf(2) + await eventually(() => expect(meaningfulRoots()).to.have.lengthOf(2)) }) test('cds.spawn is traced', async () => { await POST('/odata/v4/admin/test_spawn', {}, admin) - await wait(30) // 2 visible roots: the action invocation + the spawned task - expect(rootSpans()).to.have.lengthOf(2) - expect(captured.some(s => s.name === 'cds.spawn - schedule task')).to.be.true - expect(captured.some(s => s.name === 'cds.spawn - run task')).to.be.true + await eventually(() => { + expect(meaningfulRoots()).to.have.lengthOf(2) + expect(captured.some(s => s.name === 'cds.spawn - schedule task')).to.be.true + expect(captured.some(s => s.name === 'cds.spawn - run task')).to.be.true + }) }) test('emit is traced', async () => { await POST('/odata/v4/admin/test_emit', {}, admin) - await wait(100) // local-messaging keeps the consumer in the same context → exactly 1 visible root - expect(rootSpans()).to.have.lengthOf(1) + await eventually(() => expect(meaningfulRoots()).to.have.lengthOf(1)) }) describe('db', () => { @@ -105,8 +151,7 @@ describe('tracing', () => { test('custom spans are supported', async () => { await GET('/odata/v4/catalog/ListOfBooks', {}, admin) - await wait(100) - expect(captured.filter(s => s.name === 'my custom span')).to.have.lengthOf(1) + await eventually(() => expect(captured.filter(s => s.name === 'my custom span')).to.have.lengthOf(1)) }) // --- TODO --- diff --git a/vitest.config.mjs b/vitest.config.mjs index 49b75cf5..987f3e1b 100644 --- a/vitest.config.mjs +++ b/vitest.config.mjs @@ -1,15 +1,30 @@ -import { defineConfig } from 'vitest/config' +import { defineConfig, configDefaults } from 'vitest/config' // Default: 42s timeout, run every *.test.js file. let testTimeout = 42000 +let hookTimeout = 30000 let include = ['test/**/*.test.js'] +let exclude = configDefaults.exclude -// HANA CI runs only a small subset with a 10x timeout (ported from the old -// jest.config.js). The `cds_requires_telemetry_tracing` env has to be set here, -// before any test file requires @sap/cds, so keep it in the config module. -if (process.env.CI && process.env.HANA_DRIVER) { +// HANA CI runs the FULL suite (`test/**/*.test.js`, the default `include`) with a +// 10x test timeout since HANA is slower than sqlite. The `cds_requires_telemetry_tracing` +// env has to be set here, before any test file requires @sap/cds, so keep it in the +// config module. +const HANA = process.env.CI && process.env.HANA_DRIVER +if (HANA) { testTimeout *= 10 - include = ['test/**/tracing-attributes.test.js', 'test/**/passport.test.js'] + + // Multitenancy needs a bound BTP Service Manager (MTX) to provision per-tenant HDI + // containers. The HANA CI runs against a single pre-provisioned HDI container with no + // Service Manager, so these two suites can't run there — exclude them from the HANA job + // entirely (they still run on sqlite with in-memory tenants). + exclude = [...configDefaults.exclude, '**/tracing-mt.test.js', '**/metrics-outbox-multitenant.test.js'] + + // Signal "running on HANA" to test files that must branch at COLLECTION time (before + // cds.test() applies its --profile), e.g. the queue/outbox files that skip the sqlite-only + // cds.spawn cases. Reading cds.env at collection time would freeze the env singleton before + // the profile is applied, so files read this env var instead. + process.env.TELEMETRY_TEST_HANA = '1' if (process.env.HANA_PROM) process.env.cds_requires_telemetry_tracing = JSON.stringify({ _hana_prom: process.env.HANA_PROM === 'true' }) @@ -21,7 +36,14 @@ export default defineConfig({ // them in every test file (smallest diff to the existing jest suite). globals: true, include, + exclude, testTimeout, + hookTimeout, + // A couple of queue/outbox tests are timing-sensitive against the SHARED remote HANA Cloud + // HDI container (non-deterministic queue-worker latency); the afterAll settle reduces but + // can't fully remove the flakiness. Retry on HANA only so an unlucky timing miss self-heals; + // sqlite (per-file in-memory DB) is deterministic and gets no retries. + retry: HANA ? 2 : 0, // The OTLP exporters (and CAP's telemetry SDK) can leave open handles/timers // alive. Run each test file in its own forked child process so that, once a // file finishes, its process is torn down and the handles die with it. This @@ -31,6 +53,12 @@ export default defineConfig({ // fresh child per file: matches jest's per-file isolation and preserves the // top-of-module process.env mutations some test files rely on. isolate: true, + // On HANA every test file shares ONE HDI container (unlike sqlite's per-file + // in-memory DB), so files must not run concurrently: parallel workers collide on + // fixture INSERTs and on the shared cds.outbox.Messages table. Run files serially + // on HANA; the queue/outbox test files also clear the outbox in a beforeAll so a + // prior file's leftover rows can't bleed in. (sqlite keeps full parallelism.) + fileParallelism: !HANA, // don't hang the run waiting on lingering handles at teardown. teardownTimeout: 5000 } From bfb8bcd1f86d8758819a26e1f8f509131b6c0d7d Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 17 Aug 2026 10:54:53 +0200 Subject: [PATCH 11/31] chore: release workflow (#484) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # 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
PR Bot Information **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`
--- .github/workflows/release.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5c7791f4..c24be2d3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,6 @@ jobs: node-version: 24 registry-url: https://registry.npmjs.org/ - name: run tests - # REVISIT: remove "npm explore better-sqlite3 -- npm run install" with cds^10 run: | npm i -g @sap/cds-dk npm i From 0b6211ec08c88a958e94a144b9cb2dfd0786ecc0 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 17 Aug 2026 12:41:21 +0200 Subject: [PATCH 12/31] fix(logging): guard cds.log.format interception against re-entrancy; unpin sdk-logs (#482) (#489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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. --- CHANGELOG.md | 1 + lib/logging/index.js | 43 ++++++++++++++++++++++++-------- package-lock.json | 58 +++++--------------------------------------- package.json | 3 --- 4 files changed, 40 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23ba12a8..ed9bec78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Fixed +- Logging no longer recurses through `@opentelemetry/sdk-logs` 0.221's export path: the log-processor construction now adapts to the installed sdk-logs version (0.221+ takes an `{ exporter }` options object, earlier versions the positional exporter), and a re-entrancy guard was added to the `cds.log.format` interception - Cloud SDK outbound requests are traced again (patch getter-only `@sap-cloud-sdk/http-client` exports via `Object.defineProperty`) - Raw SQL no longer leaks into HANA INSERT `prepare` span names (now uses operation + table, matching SELECT) - Queue `*_storage_time_in_seconds` metrics are now correct on HANA (timezone-naive `min`/`max` timestamp aggregates were parsed as local time, skewing the values by the machine's UTC offset) diff --git a/lib/logging/index.js b/lib/logging/index.js index 45a1ea36..a1fe08e0 100644 --- a/lib/logging/index.js +++ b/lib/logging/index.js @@ -11,6 +11,19 @@ const _protocol2module = { 'http/json': '@opentelemetry/exporter-logs-otlp-http' } +// @opentelemetry/sdk-logs 0.221 changed the log processor constructors (Simple/Batch, and hence +// any subclass) from a positional exporter argument `(exporter)` to an options object `({ exporter })`. +// Passing the wrong shape leaves the exporter undefined, so every export throws and — since our diag +// logger is wired to cds.log — recurses back through cds.log.format into an unbounded loop (see #482). +// To stay correct regardless of which sdk-logs a consumer's tree resolves, detect the installed +// version and build the constructor argument accordingly (no hard version floor needed). +function logProcessorArg(exporter) { + const version = require('@opentelemetry/sdk-logs/package.json').version + const [major, minor] = version.split('.').map(Number) + const usesOptionsObject = major > 0 || minor >= 221 + return usesOptionsObject ? { exporter } : exporter +} + function _getExporter() { let { kind, @@ -67,7 +80,7 @@ function _getCustomProcessor(exporter) { if (!loggingProcessorModule[loggingProcessor.class]) throw new Error(`Unknown logs processor "${loggingProcessor.class}" in module "${loggingProcessor.module}"`) - const processor = new loggingProcessorModule[loggingProcessor.class](exporter) + const processor = new loggingProcessorModule[loggingProcessor.class](logProcessorArg(exporter)) LOG._debug && LOG.debug('Using logs processor:', processor) return processor @@ -89,13 +102,18 @@ module.exports = resource => { const custom_fields = cds.env.log.cls_custom_fields || [] // intercept logs via format + // re-entrancy guard: while we are inside our own logger.emit(), skip re-emitting anything + // that emit() logs synchronously (e.g. via the export path or the SDK's diag logger, which + // is wired to cds.log('telemetry')). This prevents cds.log.format from recursing back into + // logger.emit() and looping through the logs SDK's export/diagnostic output (#482). + let emitting = false const { format: _format } = cds.log const format = (cds.log.format = function (module, level, ...args) { const res = _format.call(this, module, level, ...args) let log try { - log = res.length === 1 && res[0].startsWith?.('{"') && JSON.parse(res[0]) + log = !emitting && res.length === 1 && res[0].startsWith?.('{"') && JSON.parse(res[0]) } catch { // ignore } @@ -115,12 +133,17 @@ module.exports = resource => { log.msg = log.msg.replace(e.stack, e.stack?.split('\n')[0]) } for (const field of custom_fields) if (field in log) attributes[field] = log[field] - logger.emit({ - severityNumber: SeverityNumber[severity], - severityText: severity, - body: log.msg, - attributes - }) + emitting = true + try { + logger.emit({ + severityNumber: SeverityNumber[severity], + severityText: severity, + body: log.msg, + attributes + }) + } finally { + emitting = false + } } return res @@ -137,8 +160,8 @@ module.exports = resource => { const processor = _getCustomProcessor(exporter) || (process.env.NODE_ENV === 'production' - ? new BatchLogRecordProcessor(exporter) - : new SimpleLogRecordProcessor(exporter)) + ? new BatchLogRecordProcessor(logProcessorArg(exporter)) + : new SimpleLogRecordProcessor(logProcessorArg(exporter))) /* * either add processor as delegate in CALM... diff --git a/package-lock.json b/package-lock.json index 3d2554fc..620f8da8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -675,15 +675,15 @@ } }, "node_modules/@opentelemetry/sdk-logs": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.219.0.tgz", - "integrity": "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA==", + "version": "0.221.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", + "integrity": "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.219.0", - "@opentelemetry/core": "2.8.0", - "@opentelemetry/resources": "2.8.0", + "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/core": "2.10.0", + "@opentelemetry/resources": "2.10.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -693,52 +693,6 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/api-logs": { - "version": "0.219.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.219.0.tgz", - "integrity": "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.8.0.tgz", - "integrity": "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.8.0.tgz", - "integrity": "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.8.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, "node_modules/@opentelemetry/sdk-metrics": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.10.0.tgz", diff --git a/package.json b/package.json index 3c52a465..b3128b8f 100644 --- a/package.json +++ b/package.json @@ -52,9 +52,6 @@ "oxfmt": "0.63.0", "vitest": "^4" }, - "overrides": { - "@opentelemetry/sdk-logs": "0.219.0" - }, "cds": { "requires": { "telemetry": { From 0c28b57d54d4718efd0052db4fb91d67d29cc8c1 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:47:12 +0200 Subject: [PATCH 13/31] test: convert remote/span-name tracing tests to in-memory span exporter; drop tracing-attributes profile (#478) (#490) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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). --- test/bookshop/.cdsrc.json | 11 ----------- test/tracing-remote-cloudsdk.test.js | 12 ++++++++---- test/tracing-remote-native.test.js | 12 ++++++++---- test/tracing-span-names.test.js | 19 +++++++++++++------ 4 files changed, 29 insertions(+), 25 deletions(-) diff --git a/test/bookshop/.cdsrc.json b/test/bookshop/.cdsrc.json index 7b046bcf..842dfd24 100644 --- a/test/bookshop/.cdsrc.json +++ b/test/bookshop/.cdsrc.json @@ -75,17 +75,6 @@ } } }, - "[tracing-attributes]": { - "requires": { - "telemetry": { - "tracing": { - "exporter": { - "module": "@opentelemetry/sdk-trace-node" - } - } - } - } - }, "[tracing-in-memory]": { "requires": { "telemetry": { diff --git a/test/tracing-remote-cloudsdk.test.js b/test/tracing-remote-cloudsdk.test.js index 4adfcede..16b5b79e 100644 --- a/test/tracing-remote-cloudsdk.test.js +++ b/test/tracing-remote-cloudsdk.test.js @@ -1,17 +1,21 @@ const cds = require('@sap/cds') -const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-attributes') +const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') const http = require('http') +// The tracing-in-memory profile (see test/bookshop/.cdsrc.json) configures +// MyInMemorySpanExporter as the trace exporter. We read the captured ReadableSpan +// objects directly out of its shared buffer — no console spy. +const { captured, reset } = require('./bookshop/lib/MyInMemorySpanExporter') + // Cloud SDK path: with @sap-cloud-sdk/http-client installed (as in the bookshop) and // cds.env.remote.native_fetch NOT set, CAP routes outbound remote calls through // getCloudSdk().executeHttpRequestWithOrigin(...). lib/tracing/cloud_sdk.js wraps that // export so the outbound call produces a @cap-js/telemetry CLIENT span carrying // the sap.btp.destination attribute. describe('tracing remote via cloud sdk', () => { - const log = vi.spyOn(console, 'dir') - beforeEach(log.mockClear) + beforeEach(reset) - const getSpans = () => log.mock.calls.map(c => c[0]).filter(Boolean) + const getSpans = () => captured const getCapSpans = () => getSpans().filter(s => s.instrumentationScope?.name === '@cap-js/telemetry') let server, port diff --git a/test/tracing-remote-native.test.js b/test/tracing-remote-native.test.js index f930a5d8..857c5f19 100644 --- a/test/tracing-remote-native.test.js +++ b/test/tracing-remote-native.test.js @@ -3,19 +3,23 @@ process.env.cds_remote_native__fetch = 'true' const cds = require('@sap/cds') -const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-attributes') +const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') const http = require('http') +// The tracing-in-memory profile (see test/bookshop/.cdsrc.json) configures +// MyInMemorySpanExporter as the trace exporter. We read the captured ReadableSpan +// objects directly out of its shared buffer — no console spy. +const { captured, reset } = require('./bookshop/lib/MyInMemorySpanExporter') + // Native fetch path: when cds.env.remote.native_fetch === true (or no cloud sdk is // installed), CAP routes outbound remote calls through native fetch, which is // instrumented by @opentelemetry/instrumentation-undici. The outbound span therefore // comes from that instrumentation scope (NOT @opentelemetry/instrumentation-http, and // NOT our cloud_sdk wrapper) and carries the standard http.* / url.* / server.* attributes. describe('tracing remote via native fetch', () => { - const log = vi.spyOn(console, 'dir') - beforeEach(log.mockClear) + beforeEach(reset) - const getSpans = () => log.mock.calls.map(c => c[0]).filter(Boolean) + const getSpans = () => captured let server, port diff --git a/test/tracing-span-names.test.js b/test/tracing-span-names.test.js index 8ff39d39..73a30903 100644 --- a/test/tracing-span-names.test.js +++ b/test/tracing-span-names.test.js @@ -1,14 +1,21 @@ const cds = require('@sap/cds') -const { expect, data } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-attributes') +const { expect, data } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') const http = require('http') -describe('span names', () => { - beforeEach(data.reset) +// The tracing-in-memory profile (see test/bookshop/.cdsrc.json) configures +// MyInMemorySpanExporter as the trace exporter. We read the captured ReadableSpan +// objects directly out of its shared buffer — no console spy. +const { captured, reset } = require('./bookshop/lib/MyInMemorySpanExporter') - const log = vi.spyOn(console, 'dir') - beforeEach(log.mockClear) +describe('span names', () => { + beforeEach(async () => { + // data.reset is itself heavily traced (it runs DELETEs + INSERTs for the seed data) — + // run it first, THEN clear the buffer so the test only sees its own spans. + await data.reset() + reset() + }) - const getSpans = () => log.mock.calls.map(c => c[0]).filter(Boolean) + const getSpans = () => captured // Spans from our tracer only (excludes HTTP instrumentation spans) const getCapSpans = () => getSpans().filter(s => s.instrumentationScope?.name === '@cap-js/telemetry') From 13e2c7e70868265610247ef129cd7e67257c042b Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Thu, 20 Aug 2026 15:58:48 +0200 Subject: [PATCH 14/31] test: enable HTTP instrumentation in tests; assert incoming SERVER spans + traceparent propagation (#475) (#491) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- test/bookshop/package.json | 4 +- test/tracing-messaging-inboxed.test.js | 11 ++- ...racing-messaging-persistent-outbox.test.js | 19 ++-- test/tracing-messaging-without-outbox.test.js | 17 +++- test/tracing-messaging.js | 10 +- test/tracing-scheduled.test.js | 24 +++-- test/tracing.test.js | 92 +++++++++++++++---- 7 files changed, 134 insertions(+), 43 deletions(-) diff --git a/test/bookshop/package.json b/test/bookshop/package.json index 7cabf2be..bf574fe9 100644 --- a/test/bookshop/package.json +++ b/test/bookshop/package.json @@ -22,9 +22,7 @@ "instrumentations": { "http": { "config": { - "ignoreIncomingRequestHook": "./lib/MyIgnoreIncomingRequestHook.js", - "disableIncomingRequestInstrumentation": true, - "disableOutgoingRequestInstrumentation": true + "ignoreIncomingRequestHook": "./lib/MyIgnoreIncomingRequestHook.js" } } }, diff --git a/test/tracing-messaging-inboxed.test.js b/test/tracing-messaging-inboxed.test.js index bd5fdb3d..d7a158fc 100644 --- a/test/tracing-messaging-inboxed.test.js +++ b/test/tracing-messaging-inboxed.test.js @@ -1,14 +1,18 @@ const CASE = 'inboxed' +const otel = require('@opentelemetry/api') + // `inboxed: true` combined with the default outboxed messaging behavior means TWO queue // workers get involved per emit — one on the producer side (drains outbox to broker) and // one on the consumer side (drains inbox to subscribers). Each worker runs two // transactions (tx 1: lock; tx 2: handle + delete). // // Each worker iteration is wrapped by `cds.spawn`, so both txs collapse under a single -// `cds.spawn - run task` root. 4 meaningful roots: +// `cds.spawn - run task` root. With incoming HTTP instrumentation on, the producer trace is +// rooted at the incoming SERVER span for the emit-triggering POST (AdminService - tx nests +// under it). 4 meaningful roots: // -// 1. AdminService - tx (producer: handle test_emit, UPSERT outbox) +// 1. POST (incoming SERVER span) (producer: AdminService - tx, handle test_emit, UPSERT outbox) // 2. cds.spawn - run task (outbox worker: dispatches to file) // ├─ db - tx (tx 1: lock) // └─ messaging - tx (tx 2: handle foo — writes to file — + DELETE) @@ -31,7 +35,8 @@ const CHECK = ({ expect, rootSpans, groupedByTrace }) => { // Producer trace const producer = groupedByTrace.find(g => g.all.some(s => s.name === 'AdminService - handle test_emit')) expect(producer, 'expected a producer trace').to.exist - expect(producer.root.name).to.equal('AdminService - tx') + expect(producer.root.kind, 'producer trace rooted at the incoming SERVER span').to.equal(otel.SpanKind.SERVER) + expect(producer.all.some(s => s.name === 'AdminService - tx')).to.be.true expect(producer.all.some(s => s.name === 'db - UPSERT cds.outbox.Messages')).to.be.true // The inbox worker must have run the application handler (SELECT Books). diff --git a/test/tracing-messaging-persistent-outbox.test.js b/test/tracing-messaging-persistent-outbox.test.js index 708e8802..6744e530 100644 --- a/test/tracing-messaging-persistent-outbox.test.js +++ b/test/tracing-messaging-persistent-outbox.test.js @@ -1,5 +1,7 @@ const CASE = 'persistent-outbox' +const otel = require('@opentelemetry/api') + // REVISIT: even with profile "persistent-outbox", messaging kind and file from package.json wins process.env.cds_requires_messaging = JSON.stringify({ kind: 'file-based-messaging', @@ -18,13 +20,17 @@ process.env.cds_requires_messaging = JSON.stringify({ // wraps to emit a single `cds.spawn - run task` CONSUMER root that both worker tx spans // nest under. // +// With incoming HTTP instrumentation on, the producer trace is rooted at the incoming SERVER +// span for the emit-triggering POST (AdminService - tx nests under it). +// // Expected shape (3 meaningful roots, same for sqlite and HANA): // -// 1. AdminService - tx (producer trace) -// └─ AdminService - handle test_emit -// └─ messaging - emit outgoing foo -// └─ db - UPSERT cds.outbox.Messages -// └─ cds.spawn - schedule task +// 1. POST (incoming SERVER span) (producer trace) +// └─ AdminService - tx +// └─ AdminService - handle test_emit +// └─ messaging - emit outgoing foo +// └─ db - UPSERT cds.outbox.Messages +// └─ cds.spawn - schedule task // // 2. cds.spawn - run task (queue worker root) // ├─ db - tx (tx 1) @@ -44,7 +50,8 @@ const CHECK = ({ expect, rootSpans, groupedByTrace }) => { // Producer trace const producer = groupedByTrace.find(g => g.all.some(s => s.name === 'AdminService - handle test_emit')) expect(producer, 'expected a producer trace').to.exist - expect(producer.root.name).to.equal('AdminService - tx') + expect(producer.root.kind, 'producer trace rooted at the incoming SERVER span').to.equal(otel.SpanKind.SERVER) + expect(producer.all.some(s => s.name === 'AdminService - tx')).to.be.true expect(producer.all.some(s => s.name === 'messaging - emit outgoing foo')).to.be.true expect(producer.all.some(s => s.name === 'db - UPSERT cds.outbox.Messages')).to.be.true expect(producer.all.some(s => s.name === 'cds.spawn - schedule task')).to.be.true diff --git a/test/tracing-messaging-without-outbox.test.js b/test/tracing-messaging-without-outbox.test.js index 81138218..021a84a4 100644 --- a/test/tracing-messaging-without-outbox.test.js +++ b/test/tracing-messaging-without-outbox.test.js @@ -1,5 +1,7 @@ const CASE = 'without-outbox' +const otel = require('@opentelemetry/api') + // REVISIT: even with profile "without-outbox", messaging kind and file from package.json wins process.env.cds_requires_messaging = JSON.stringify({ kind: 'file-based-messaging', @@ -11,11 +13,15 @@ process.env.cds_requires_messaging = JSON.stringify({ // transaction (no queue worker). The file watcher delivers asynchronously as a new // SpanKind.CONSUMER root. // +// With incoming HTTP instrumentation on, the producer trace is now rooted at the incoming +// SERVER span (SpanKind.SERVER) for the emit-triggering POST; `AdminService - tx` nests under it. +// // Expected roots: -// 1. AdminService - tx (producer) -// └─ AdminService - handle test_emit -// └─ messaging - emit outgoing foo -// └─ messaging - handle foo (writes to file, in-process) +// 1. POST (incoming SERVER span) (producer) +// └─ AdminService - tx +// └─ AdminService - handle test_emit +// └─ messaging - emit outgoing foo +// └─ messaging - handle foo (writes to file, in-process) // // 2. messaging - tx (file-based CONSUMER) // └─ messaging - emit outgoing foo @@ -29,7 +35,8 @@ const CHECK = ({ expect, rootSpans, groupedByTrace }) => { // Producer trace const producer = groupedByTrace.find(g => g.all.some(s => s.name === 'AdminService - handle test_emit')) expect(producer, 'expected a producer trace').to.exist - expect(producer.root.name).to.equal('AdminService - tx') + expect(producer.root.kind, 'producer trace rooted at the incoming SERVER span').to.equal(otel.SpanKind.SERVER) + expect(producer.all.some(s => s.name === 'AdminService - tx')).to.be.true expect(producer.all.some(s => s.name.match(/messaging - emit outgoing/))).to.be.true // File-based CONSUMER trace diff --git a/test/tracing-messaging.js b/test/tracing-messaging.js index 147fc594..fd7cca22 100644 --- a/test/tracing-messaging.js +++ b/test/tracing-messaging.js @@ -3,6 +3,14 @@ module.exports = (CASE, CHECK) => { const { expect, POST } = cds.test(__dirname + '/bookshop', '--profile', `${CASE},tracing-in-memory`) const { reset, groupedByTrace, captured } = require('./bookshop/lib/MyInMemorySpanExporter') const otel = require('@opentelemetry/api') + const { suppressTracing } = require('@opentelemetry/core') + + // The test's HTTP client runs in-process and, with outgoing HTTP instrumentation now enabled, + // would itself create a CLIENT span for the emit-triggering POST — an artificial extra root + // above the incoming SERVER span. Real callers are separate, un-instrumented processes, so we + // run the request under suppressTracing to model that: the outgoing CLIENT span is skipped and + // the incoming SERVER span is the producer trace's root. + const asExternalClient = fn => otel.context.with(suppressTracing(otel.context.active()), fn) const wait = require('node:timers/promises').setTimeout @@ -99,7 +107,7 @@ module.exports = (CASE, CHECK) => { }) test('emit is traced', async () => { - await POST('/odata/v4/admin/test_emit', {}, admin) + await asExternalClient(() => POST('/odata/v4/admin/test_emit', {}, admin)) // Poll (flush + re-check) until both queue workers have run and exported their spans; // on HANA the worker latency exceeds any reasonable fixed sleep. Pass the meaningful // (non-outbox-scan) traces so the CHECK's exact root-count assertions aren't thrown off by diff --git a/test/tracing-scheduled.test.js b/test/tracing-scheduled.test.js index aa9c2a49..d0c8b511 100644 --- a/test/tracing-scheduled.test.js +++ b/test/tracing-scheduled.test.js @@ -6,10 +6,11 @@ // // Expected meaningful roots (unified across sqlite and HANA): // -// 1. AdminService - tx (producer trace) -// └─ AdminService - handle test_scheduled -// └─ db - UPSERT cds.outbox.Messages -// └─ cds.spawn - schedule task +// 1. POST (incoming SERVER span) (producer trace) +// └─ AdminService - tx +// └─ AdminService - handle test_scheduled +// └─ db - UPSERT cds.outbox.Messages +// └─ cds.spawn - schedule task // // 2. cds.spawn - run task (queue worker root) // ├─ db - tx (tx 1: lock) @@ -22,9 +23,17 @@ const cds = require('@sap/cds') const { expect, POST } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'tracing-in-memory') const { reset, captured, groupedByTrace, rootSpans } = require('./bookshop/lib/MyInMemorySpanExporter') const otel = require('@opentelemetry/api') +const { suppressTracing } = require('@opentelemetry/core') const wait = require('node:timers/promises').setTimeout +// With incoming HTTP instrumentation on, the in-process test client would otherwise emit its own +// outgoing CLIENT span for the POST — an artifact that pollutes the producer trace and can even be +// picked as its root. Real callers are separate, un-instrumented processes, so run client requests +// under suppressTracing to model that: the CLIENT span is skipped and the genuine incoming SERVER +// span is the producer trace's root. +const asExternalClient = fn => otel.context.with(suppressTracing(otel.context.active()), fn) + // Force-flush the tracer provider's span processor so any spans buffered by background // queue/worker activity are exported into `captured`. The global provider is a // ProxyTracerProvider (no forceFlush) whose delegate is the real NodeTracerProvider; @@ -83,7 +92,7 @@ describe('tracing for scheduled tasks', () => { }) test('schedule .after() is fully traced through the queue worker', async () => { - await POST('/odata/v4/admin/test_scheduled', {}, { auth: { username: 'alice' } }) + await asExternalClient(() => POST('/odata/v4/admin/test_scheduled', {}, { auth: { username: 'alice' } })) // Poll (flush + re-check) until the scheduled task has fired and all spans have been // exported; on HANA the worker latency exceeds any reasonable fixed sleep. @@ -91,7 +100,10 @@ describe('tracing for scheduled tasks', () => { // Producer trace: writes the task row inside the HTTP request tx. const producer = groupedByTrace().find(g => g.all.some(s => s.name === 'AdminService - handle test_scheduled')) expect(producer, 'expected a producer trace').to.exist - expect(producer.root.name).to.equal('AdminService - tx') + // With incoming HTTP instrumentation on, the producer trace roots at the incoming SERVER + // span for the POST; `AdminService - tx` now nests under it. + expect(producer.root.kind, 'producer trace rooted at the incoming SERVER span').to.equal(otel.SpanKind.SERVER) + expect(producer.all.some(s => s.name === 'AdminService - tx')).to.be.true expect(producer.all.some(s => s.name === 'db - UPSERT cds.outbox.Messages')).to.be.true expect(producer.all.some(s => s.name === 'cds.spawn - schedule task')).to.be.true diff --git a/test/tracing.test.js b/test/tracing.test.js index d81806c9..94138349 100644 --- a/test/tracing.test.js +++ b/test/tracing.test.js @@ -11,6 +11,14 @@ const { expect, GET, POST } = cds.test(__dirname + '/bookshop', '--profile', 'tr // console spying, no string-regex matching of formatted output. const { reset, rootSpans, groupedByTrace, captured } = require('./bookshop/lib/MyInMemorySpanExporter') const otel = require('@opentelemetry/api') +const { suppressTracing } = require('@opentelemetry/core') + +// The test's HTTP client runs in-process and, with outgoing HTTP instrumentation now enabled, +// would itself create a CLIENT span for every request — an artificial extra root that also +// overwrites any manually-set `traceparent` header. Real callers are separate, un-instrumented +// processes, so we run client-side requests under suppressTracing to model that: the outgoing +// CLIENT span is skipped, the incoming SERVER span is created normally by the server handler. +const asExternalClient = fn => otel.context.with(suppressTracing(otel.context.active()), fn) const wait = require('node:timers/promises').setTimeout @@ -67,12 +75,22 @@ describe('tracing', () => { expect(rootSpans().length).to.be.gte(1) }) - // REVISIT: jest breaks otel's patching of incoming request handling -> no span for 'GET' -> behavior to test not reproducible - xtest('GET with traceparent is traced', async () => { - const config = { ...admin, headers: { traceparent: '00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01' } } - const { status } = await GET('/odata/v4/admin/Books', config) + // With incoming HTTP instrumentation on, the SERVER span adopts the W3C trace context from the + // request's `traceparent` header: the whole request trace continues the given trace id and the + // SERVER span is a child of the given (external) span id. + test('GET with traceparent is traced', async () => { + const traceId = '0af7651916cd43dd8448eb211c80319c' + const parentSpanId = 'b7ad6b7169203331' + const config = { ...admin, headers: { traceparent: `00-${traceId}-${parentSpanId}-01` } } + const { status } = await asExternalClient(() => GET('/odata/v4/admin/Books', config)) expect(status).to.equal(200) expect(captured.some(s => s.name === 'AdminService - READ AdminService.Books')).to.be.true + // The incoming SERVER span continued the propagated trace and parented off the external span id. + await eventually(() => { + const server = captured.find(s => s.kind === otel.SpanKind.SERVER && s.spanContext().traceId === traceId) + expect(server, 'incoming SERVER span adopting the propagated trace').to.exist + expect(server.parentSpanContext?.spanId).to.equal(parentSpanId) + }) }) test('custom GET is traced', async () => { @@ -97,24 +115,60 @@ describe('tracing', () => { }) }) - // REVISIT: jest breaks otel's patching of incoming request handling -> behavior to test not reproducible - xtest('instrumentation hooks', async () => {}) + // Incoming HTTP instrumentation produces a SERVER span (SpanKind.SERVER === 1) per request, + // carrying the mount-relative `url.path`. Two independent mechanisms suppress that span: + // - the sampler's `ignoreIncomingPaths` (set at the top of this file for /odata/v4/admin/Authors) + // - the `ignoreIncomingRequestHook` (MyIgnoreIncomingRequestHook: /odata/v4/admin/Authors + /Books(252)) + // A non-ignored path still produces a SERVER span; the ignored paths must produce none. + test('instrumentation hooks', async () => { + const serverSpansFor = path => + captured.filter(s => s.kind === otel.SpanKind.SERVER && s.attributes['url.path'] === path) + + // Baseline: a non-ignored path DOES yield an incoming SERVER span. + await asExternalClient(() => GET('/odata/v4/admin/Books', admin)) + await eventually(() => expect(serverSpansFor('/Books')).to.have.lengthOf(1)) + + // Sampler path: /odata/v4/admin/Authors is in ignoreIncomingPaths -> no SERVER span. + reset() + await asExternalClient(() => GET('/odata/v4/admin/Authors?$select=ID', admin)) + await eventually(() => { + expect(captured.some(s => s.kind === otel.SpanKind.SERVER && s.attributes['url.path']?.includes('/Authors'))).to + .be.false + }) + + // ignoreIncomingRequestHook path: /Books(252) is ignored by the hook (not the sampler) -> no SERVER span. + reset() + await asExternalClient(() => GET('/odata/v4/admin/Books(252)', admin)) + await eventually(() => { + expect(serverSpansFor('/Books(252)')).to.have.lengthOf(0) + }) + }) test('$batch is traced', async () => { - await POST( - '/odata/v4/genre/$batch', - { - requests: [ - { id: 'r1', method: 'POST', url: '/Genres', headers: { 'content-type': 'application/json' }, body: {} }, - { id: 'r2', method: 'GET', url: '/Genres', headers: {} } - ] - }, - admin + await asExternalClient(() => + POST( + '/odata/v4/genre/$batch', + { + requests: [ + { id: 'r1', method: 'POST', url: '/Genres', headers: { 'content-type': 'application/json' }, body: {} }, + { id: 'r2', method: 'GET', url: '/Genres', headers: {} } + ] + }, + admin + ) ) - // With the tx wrap (lib/tracing/cds.js), each batch request's tx becomes a single root — - // the previously-visible 4 sub-roots (POST: CREATE + read-after-write; GET: read actives + - // read drafts) are now nested under 2 root tx spans, one per batch entry. - await eventually(() => expect(meaningfulRoots()).to.have.lengthOf(2)) + // With incoming HTTP instrumentation on, the single $batch POST produces one incoming SERVER + // span that becomes the trace root. Both batch sub-requests (the POST -> CREATE Genres draft + // and the GET -> READ Genres) run within that request context, so their tx spans reparent + // under the SERVER span rather than surfacing as separate roots. Result: exactly 1 meaningful + // root (the SERVER span), containing both the CREATE and the READ sub-operations. + await eventually(() => { + const roots = meaningfulRoots() + expect(roots).to.have.lengthOf(1) + expect(roots[0].kind).to.equal(otel.SpanKind.SERVER) + expect(captured.some(s => s.name === 'GenreService - CREATE GenreService.Genres.drafts')).to.be.true + expect(captured.some(s => s.name === 'GenreService - READ GenreService.Genres')).to.be.true + }) }) test('cds.spawn is traced', async () => { From 65157bcd521c556c7db9b79080d8bab0384f2639 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Thu, 20 Aug 2026 21:08:49 +0200 Subject: [PATCH 15/31] test: centralize duplicated test helpers into test-utils (#488) (#492) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- test/metrics-outbox-multitenant.test.js | 27 ++----- test/metrics-outbox.test.js | 41 ++-------- test/metrics.test.js | 26 ++----- test/tracing-messaging.js | 63 +--------------- test/tracing-outboxed-batch.test.js | 33 +-------- test/tracing-scheduled.test.js | 40 +--------- test/tracing.test.js | 52 +------------ test/utils.js | 99 +++++++++++++++++++++++++ 8 files changed, 125 insertions(+), 256 deletions(-) create mode 100644 test/utils.js diff --git a/test/metrics-outbox-multitenant.test.js b/test/metrics-outbox-multitenant.test.js index e5115d18..3f7956bf 100644 --- a/test/metrics-outbox-multitenant.test.js +++ b/test/metrics-outbox-multitenant.test.js @@ -4,6 +4,7 @@ const { setTimeout: wait } = require('node:timers/promises') // Exported metric data is captured in-memory by MyInMemoryMetricReader (wired via the // metrics-outbox profile in .cdsrc.json) instead of scraping ConsoleMetricExporter's console.dir. const { latestDataPointValue, forceFlush, reset } = require('./bookshop/lib/MyInMemoryMetricReader') +const { makeExpectEventually } = require('./utils') const { expect, GET, axios } = cds.test( __dirname + '/bookshop', @@ -17,26 +18,12 @@ function metricValue(tenant, metric) { return latestDataPointValue(metric, { 'sap.tenancy.tenant_id': tenant }) } -// State-based wait: force the wired meter provider to collect + export, then re-run the assertion -// block. Replaces all fixed-time `wait(…)` sleeps — the loop completes the instant the in-memory -// per-tenant queue statistics (kept fresh by the existing cds.spawn poller) reflect the asserted -// state. forceFlush() throws fast if the provider isn't wired, so a misconfigured profile fails -// loudly instead of busy-spinning the full timeout. -async function expectEventually(assertion, { timeout = 10000, interval = 25 } = {}) { - const start = Date.now() - let lastError - while (true) { - await forceFlush() - try { - assertion() - return - } catch (err) { - lastError = err - if (Date.now() - start >= timeout) throw lastError - await wait(interval) - } - } -} +// State-based wait for metric assertions: force the wired meter provider (forceFlush) to collect + +// export, then re-run the assertion block. Replaces all fixed-time `wait(…)` sleeps — the loop +// completes the instant the in-memory per-tenant queue statistics (kept fresh by the existing +// cds.spawn poller) reflect the asserted state. forceFlush() throws fast if the provider isn't +// wired, so a misconfigured profile fails loudly instead of busy-spinning the full timeout. +const expectEventually = makeExpectEventually(forceFlush, { timeout: 10000, interval: 25 }) // Multitenancy needs a bound BTP Service Manager (MTX) to provision per-tenant HDI containers. // The HANA CI runs against a single pre-provisioned HDI container with no Service Manager, so this diff --git a/test/metrics-outbox.test.js b/test/metrics-outbox.test.js index dc079f6d..ce1847f2 100644 --- a/test/metrics-outbox.test.js +++ b/test/metrics-outbox.test.js @@ -9,6 +9,7 @@ const { setTimeout: wait } = require('node:timers/promises') // Exported metric data is captured in-memory by MyInMemoryMetricReader (wired via the // metrics-outbox profile in .cdsrc.json) instead of scraping ConsoleMetricExporter's console.dir. const { latestDataPointValue, forceFlush, reset } = require('./bookshop/lib/MyInMemoryMetricReader') +const { clearOutbox, makeExpectEventually } = require('./utils') const { expect, GET, axios } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'metrics-outbox') axios.defaults.validateStatus = () => true @@ -17,25 +18,11 @@ function metricValue(metric, queuedServiceName) { return latestDataPointValue(metric, { 'queue.name': queuedServiceName }) } -// Best-effort outbox clear that can NEVER hang the surrounding hook. On the shared HANA HDI -// container a background queue worker may be holding the connection pool (draining/retrying), -// so a bare `DELETE` can block indefinitely — which previously turned into a 100s hook timeout -// that starved the pool and cascaded into ECONNREFUSED for the NEXT test file's server. Race the -// DELETE against a short timeout and swallow errors: if it can't complete quickly, the leftover -// rows are handled by the next file's own beforeEach clear anyway. -async function clearOutbox(timeout = 5000) { - try { - await Promise.race([DELETE.from('cds.outbox.Messages'), wait(timeout)]) - } catch { - // pool draining / server shutting down — nothing left to clean matters - } -} - -// State-based wait: force the wired meter provider to collect + export, then re-run the assertion -// block. Replaces all fixed-time `wait(150)` sleeps — the loop completes the instant the in-memory -// queue statistics (kept fresh by the queue-stats cds.spawn poller) reflect the asserted state. -// forceFlush() throws fast if the provider isn't wired, so a misconfigured profile fails loudly -// instead of busy-spinning the full timeout. +// State-based wait for metric assertions: force the wired meter provider (forceFlush) to collect + +// export, then re-run the assertion block. Replaces all fixed-time `wait(150)` sleeps — the loop +// completes the instant the in-memory queue statistics (kept fresh by the queue-stats cds.spawn +// poller) reflect the asserted state. forceFlush() throws fast if the provider isn't wired, so a +// misconfigured profile fails loudly instead of busy-spinning the full timeout. // // interval is 500ms (NOT a few ms): each forceFlush() triggers a metric collection that runs the // queue-stats poller's SELECTs against the DB. On the SHARED HANA HDI container a tight poll loop @@ -45,21 +32,7 @@ async function clearOutbox(timeout = 5000) { // Polling at 500ms (with the profile's exportIntervalMillis raised to 1000ms) leaves the worker // enough DB headroom to make all its attempts. The loop still returns the instant the state holds, // so sqlite (per-file in-memory DB) still satisfies in well under a second. -async function expectEventually(assertion, { timeout = 30000, interval = 500 } = {}) { - const start = Date.now() - let lastError - while (true) { - await forceFlush() - try { - assertion() - return - } catch (err) { - lastError = err - if (Date.now() - start >= timeout) throw lastError - await wait(interval) - } - } -} +const expectEventually = makeExpectEventually(forceFlush, { timeout: 30000, interval: 500 }) const debugLog = (cds.log('telemetry').debug = vi.fn(() => {})) diff --git a/test/metrics.test.js b/test/metrics.test.js index 6582b7a3..84d68f1d 100644 --- a/test/metrics.test.js +++ b/test/metrics.test.js @@ -4,31 +4,17 @@ // log output. The formatting of those metrics is unit-tested in console-metric-exporter.test.js. const cds = require('@sap/cds') -const { setTimeout: wait } = require('node:timers/promises') const { captured, forceFlush, reset } = require('./bookshop/lib/MyInMemoryMetricReader') +const { makeExpectEventually } = require('./utils') const { expect, GET } = cds.test(__dirname + '/bookshop', '--profile', 'metrics') -// State-based wait: force the wired meter provider to collect + export, then re-run the assertion -// block. Replaces fixed-time sleeps — the loop completes the instant the captured datapoints -// reflect the asserted state. forceFlush() throws fast if the provider isn't wired, so a -// misconfigured profile fails loudly instead of busy-spinning the full timeout. -async function expectEventually(assertion, { timeout = 10000, interval = 25 } = {}) { - const start = Date.now() - let lastError - while (true) { - await forceFlush() - try { - assertion() - return - } catch (err) { - lastError = err - if (Date.now() - start >= timeout) throw lastError - await wait(interval) - } - } -} +// State-based wait for metric assertions: force the wired meter provider (forceFlush) to collect + +// export, then re-run the assertion block. Replaces fixed-time sleeps — the loop completes the +// instant the captured datapoints reflect the asserted state. forceFlush() throws fast if the +// provider isn't wired, so a misconfigured profile fails loudly instead of busy-spinning the timeout. +const expectEventually = makeExpectEventually(forceFlush, { timeout: 10000, interval: 25 }) // All metric descriptor names present across every captured export. function capturedMetricNames() { diff --git a/test/tracing-messaging.js b/test/tracing-messaging.js index fd7cca22..ef12a8a0 100644 --- a/test/tracing-messaging.js +++ b/test/tracing-messaging.js @@ -2,73 +2,12 @@ module.exports = (CASE, CHECK) => { const cds = require('@sap/cds') const { expect, POST } = cds.test(__dirname + '/bookshop', '--profile', `${CASE},tracing-in-memory`) const { reset, groupedByTrace, captured } = require('./bookshop/lib/MyInMemorySpanExporter') - const otel = require('@opentelemetry/api') - const { suppressTracing } = require('@opentelemetry/core') - - // The test's HTTP client runs in-process and, with outgoing HTTP instrumentation now enabled, - // would itself create a CLIENT span for the emit-triggering POST — an artificial extra root - // above the incoming SERVER span. Real callers are separate, un-instrumented processes, so we - // run the request under suppressTracing to model that: the outgoing CLIENT span is skipped and - // the incoming SERVER span is the producer trace's root. - const asExternalClient = fn => otel.context.with(suppressTracing(otel.context.active()), fn) + const { asExternalClient, clearOutbox, eventually, meaningful } = require('./utils') const wait = require('node:timers/promises').setTimeout - // Best-effort outbox clear that can NEVER hang the surrounding hook. On the shared HANA HDI - // container a background queue worker may hold the connection pool, so a bare DELETE can block - // indefinitely — which would turn into a hook timeout that starves the pool and cascades into - // ECONNREFUSED for the next file's server. Race the DELETE against a short timeout; leftover - // rows are cleared by the next file's own beforeEach anyway. - async function clearOutbox(timeout = 5000) { - try { - await Promise.race([DELETE.from('cds.outbox.Messages'), wait(timeout)]) - } catch { - // pool draining during shutdown — nothing left to clean matters - } - } - - // Force-flush the tracer provider's span processor so any spans buffered by background - // queue-worker activity are exported into `captured`. The global provider is a - // ProxyTracerProvider (no forceFlush) whose delegate is the real NodeTracerProvider; - // guard for the no-op provider so a misconfigured profile fails loudly, not silently. - async function flushSpans() { - const provider = otel.trace.getTracerProvider() - const delegate = provider.getDelegate?.() ?? provider - if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() - } - - // State-based wait: repeatedly flush + re-run the assertion until it holds or times out. - // Replaces the fixed `wait(waitMs)` sleep that flakes on HANA, where the two queue workers - // flush their spans well after any reasonable fixed window. - async function eventually(fn, { timeout = 15000, interval = 50 } = {}) { - const start = Date.now() - let lastError - while (true) { - await flushSpans() - try { - await fn() - return - } catch (err) { - lastError = err - if (Date.now() - start >= timeout) throw lastError - await wait(interval) - } - } - } - const admin = { auth: { username: 'alice' } } - // The queue scheduler periodically scans `cds.outbox.Messages` in its own `db - tx` (a - // SELECT + optional UPDATE that finds nothing to dispatch). On HANA these bookkeeping scans - // land as extra root traces that have nothing to do with the emit under test — and because - // the single HDI container is shared across all test files, scans triggered by other files' - // lingering workers show up too. Filter those pure outbox-scan traces so the CHECKs' exact - // root-count assertions stay stable. A scan trace is a `db - tx` root whose every span only - // touches `cds.outbox.Messages` (no application entity, no messaging/handle span). - const isOutboxScanTrace = g => - g.root.name === 'db - tx' && g.all.every(s => s.name === 'db - tx' || s.name.includes('cds.outbox.Messages')) - const meaningful = groups => groups.filter(g => !isOutboxScanTrace(g)) - const rm = () => { try { require('fs').rmSync(require('path').join(__dirname, CASE)) diff --git a/test/tracing-outboxed-batch.test.js b/test/tracing-outboxed-batch.test.js index a1d8f9bc..a7a4d44c 100644 --- a/test/tracing-outboxed-batch.test.js +++ b/test/tracing-outboxed-batch.test.js @@ -6,38 +6,7 @@ const cds = require('@sap/cds') const { expect, POST } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'tracing-in-memory') const { reset, captured, groupedByTrace } = require('./bookshop/lib/MyInMemorySpanExporter') const { hrTimeToNanoseconds } = require('@opentelemetry/core') -const otel = require('@opentelemetry/api') - -const wait = require('node:timers/promises').setTimeout - -// Force-flush the tracer provider's span processor so any spans buffered by background -// outbox/queue activity are exported into `captured`. The global provider is a -// ProxyTracerProvider (no forceFlush) whose delegate is the real NodeTracerProvider; -// guard for the no-op provider so a misconfigured profile fails loudly, not silently. -async function flushSpans() { - const provider = otel.trace.getTracerProvider() - const delegate = provider.getDelegate?.() ?? provider - if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() -} - -// State-based wait: repeatedly flush + re-run the assertion until it holds or times out. -// Replaces fixed `wait(...)` sleeps that flake on HANA, where background work flushes spans -// after the sleep window. -async function eventually(fn, { timeout = 15000, interval = 50 } = {}) { - const start = Date.now() - let lastError - while (true) { - await flushSpans() - try { - await fn() - return - } catch (err) { - lastError = err - if (Date.now() - start >= timeout) throw lastError - await wait(interval) - } - } -} +const { eventually } = require('./utils') describe('tracing for outboxed batch (chunk-size fan-out)', () => { // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. diff --git a/test/tracing-scheduled.test.js b/test/tracing-scheduled.test.js index d0c8b511..31c33182 100644 --- a/test/tracing-scheduled.test.js +++ b/test/tracing-scheduled.test.js @@ -23,45 +23,7 @@ const cds = require('@sap/cds') const { expect, POST } = cds.test(__dirname + '/bookshop', '--with-mocks', '--profile', 'tracing-in-memory') const { reset, captured, groupedByTrace, rootSpans } = require('./bookshop/lib/MyInMemorySpanExporter') const otel = require('@opentelemetry/api') -const { suppressTracing } = require('@opentelemetry/core') - -const wait = require('node:timers/promises').setTimeout - -// With incoming HTTP instrumentation on, the in-process test client would otherwise emit its own -// outgoing CLIENT span for the POST — an artifact that pollutes the producer trace and can even be -// picked as its root. Real callers are separate, un-instrumented processes, so run client requests -// under suppressTracing to model that: the CLIENT span is skipped and the genuine incoming SERVER -// span is the producer trace's root. -const asExternalClient = fn => otel.context.with(suppressTracing(otel.context.active()), fn) - -// Force-flush the tracer provider's span processor so any spans buffered by background -// queue/worker activity are exported into `captured`. The global provider is a -// ProxyTracerProvider (no forceFlush) whose delegate is the real NodeTracerProvider; -// guard for the no-op provider so a misconfigured profile fails loudly, not silently. -async function flushSpans() { - const provider = otel.trace.getTracerProvider() - const delegate = provider.getDelegate?.() ?? provider - if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() -} - -// State-based wait: repeatedly flush + re-run the assertion until it holds or times out. -// Replaces fixed `wait(...)` sleeps that flake on HANA, where the worker flushes spans after -// the sleep window. -async function eventually(fn, { timeout = 15000, interval = 50 } = {}) { - const start = Date.now() - let lastError - while (true) { - await flushSpans() - try { - await fn() - return - } catch (err) { - lastError = err - if (Date.now() - start >= timeout) throw lastError - await wait(interval) - } - } -} +const { asExternalClient, eventually } = require('./utils') describe('tracing for scheduled tasks', () => { // Queue-worker spans (cds.spawn - run task root) require @sap/cds to route the sqlite diff --git a/test/tracing.test.js b/test/tracing.test.js index 94138349..4e7508b0 100644 --- a/test/tracing.test.js +++ b/test/tracing.test.js @@ -11,55 +11,9 @@ const { expect, GET, POST } = cds.test(__dirname + '/bookshop', '--profile', 'tr // console spying, no string-regex matching of formatted output. const { reset, rootSpans, groupedByTrace, captured } = require('./bookshop/lib/MyInMemorySpanExporter') const otel = require('@opentelemetry/api') -const { suppressTracing } = require('@opentelemetry/core') - -// The test's HTTP client runs in-process and, with outgoing HTTP instrumentation now enabled, -// would itself create a CLIENT span for every request — an artificial extra root that also -// overwrites any manually-set `traceparent` header. Real callers are separate, un-instrumented -// processes, so we run client-side requests under suppressTracing to model that: the outgoing -// CLIENT span is skipped, the incoming SERVER span is created normally by the server handler. -const asExternalClient = fn => otel.context.with(suppressTracing(otel.context.active()), fn) - -const wait = require('node:timers/promises').setTimeout - -// Force-flush the tracer provider's span processor so any spans buffered by background -// activity are exported into `captured`. The global provider is a ProxyTracerProvider (no -// forceFlush) whose delegate is the real NodeTracerProvider; guard for the no-op provider -// so a misconfigured profile fails loudly, not silently. -async function flushSpans() { - const provider = otel.trace.getTracerProvider() - const delegate = provider.getDelegate?.() ?? provider - if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() -} - -// State-based wait: repeatedly flush + re-run the assertion until it holds or times out. -// Replaces fixed `wait(...)` sleeps that flake on HANA, where spawned/emitted work flushes -// spans after the sleep window. -async function eventually(fn, { timeout = 15000, interval = 50 } = {}) { - const start = Date.now() - let lastError - while (true) { - await flushSpans() - try { - await fn() - return - } catch (err) { - lastError = err - if (Date.now() - start >= timeout) throw lastError - await wait(interval) - } - } -} - -// On HANA the persistent-outbox queue poller periodically scans `cds.outbox.Messages` in its -// own `db - tx`, producing an extra root trace that is unrelated to what these tests exercise. -// Filter those bookkeeping traces out so root-count assertions stay stable across both DBs. -const isOutboxScanTrace = g => - g.root.name === 'db - tx' && g.all.every(s => s.name === 'db - tx' || s.name.includes('cds.outbox.Messages')) -const meaningfulRoots = () => - groupedByTrace() - .filter(g => !isOutboxScanTrace(g)) - .flatMap(g => g.roots) +const { asExternalClient, eventually, meaningful } = require('./utils') + +const meaningfulRoots = () => meaningful(groupedByTrace()).flatMap(g => g.roots) describe('tracing', () => { const admin = { auth: { username: 'alice' } } diff --git a/test/utils.js b/test/utils.js new file mode 100644 index 00000000..66400b5f --- /dev/null +++ b/test/utils.js @@ -0,0 +1,99 @@ +// Shared test helpers, centralized here so the ~10 tracing/metrics suites stop copy-pasting them. +// +// Kept dependency-light on purpose: it does NOT `require('@sap/cds')` at module top (doing so once +// broke span capture for the in-memory span exporter — the cds require has to happen inside the test +// file, after the profile is applied). Only @opentelemetry primitives + node timers here. +// +// `clearOutbox` uses the global `DELETE` (the cds query API). That global is installed by cds.test() +// in the test process, and clearOutbox is only ever called from within hooks/tests (after setup), so +// the global is reliably present at call time — the module never needs to require @sap/cds itself. + +const otel = require('@opentelemetry/api') +const { suppressTracing } = require('@opentelemetry/core') +const { setTimeout: wait } = require('node:timers/promises') + +// The test's HTTP client runs in-process and, with outgoing HTTP instrumentation now enabled, would +// itself create a CLIENT span for every request — an artificial extra root that also overwrites any +// manually-set `traceparent` header. Real callers are separate, un-instrumented processes, so we run +// client-side requests under suppressTracing to model that: the outgoing CLIENT span is skipped and +// the incoming SERVER span is created normally by the server handler. +const asExternalClient = fn => otel.context.with(suppressTracing(otel.context.active()), fn) + +// Best-effort outbox clear that can NEVER hang the surrounding hook. On the shared HANA HDI +// container a background queue worker may be holding the connection pool (draining/retrying), so a +// bare `DELETE` can block indefinitely — which previously turned into a hook timeout that starved +// the pool and cascaded into ECONNREFUSED for the NEXT test file's server. Race the DELETE against a +// short timeout and swallow errors: if it can't complete quickly, the leftover rows are handled by +// the next file's own beforeEach clear anyway. +async function clearOutbox(timeout = 5000) { + try { + await Promise.race([DELETE.from('cds.outbox.Messages'), wait(timeout)]) + } catch { + // pool draining / server shutting down — nothing left to clean matters + } +} + +// Force-flush the tracer provider's span processor so any spans buffered by background activity are +// exported into `captured`. The global provider is a ProxyTracerProvider (no forceFlush) whose +// delegate is the real NodeTracerProvider; guard for the no-op provider so a misconfigured profile +// fails loudly, not silently. +async function flushSpans() { + const provider = otel.trace.getTracerProvider() + const delegate = provider.getDelegate?.() ?? provider + if (typeof delegate.forceFlush === 'function') await delegate.forceFlush() +} + +// State-based wait: repeatedly flush + re-run the assertion until it holds or times out. Replaces +// the fixed `wait(...)` sleeps that flake on HANA, where background/spawned work flushes its data +// after any reasonable fixed window. +// +// `flush` is the target to force before each re-check. It defaults to `flushSpans` (the span +// callers). Metric callers pass the meter provider's `forceFlush` (exported by MyInMemoryMetricReader) +// — passing it keeps this module from depending on the reader. Defaults (timeout 15000, interval 50) +// match the span call sites; metric call sites pass their own timeout/interval explicitly. +async function eventually(fn, { flush = flushSpans, timeout = 15000, interval = 50 } = {}) { + const start = Date.now() + let lastError + while (true) { + await flush() + try { + await fn() + return + } catch (err) { + lastError = err + if (Date.now() - start >= timeout) throw lastError + await wait(interval) + } + } +} + +// Build an `expectEventually(assertion)` bound to a specific flush target + poll defaults, so the +// metric suites don't each re-declare the same one-line wrapper. Metric callers pass the meter +// provider's `forceFlush` and their own {timeout, interval} (which vary per suite); the returned +// helper takes just the assertion. Equivalent to `a => eventually(a, { flush, timeout, interval })`. +const makeExpectEventually = + (flush, { timeout, interval } = {}) => + assertion => + eventually(assertion, { flush, timeout, interval }) + +// On HANA the persistent-outbox queue scheduler periodically scans `cds.outbox.Messages` in its own +// `db - tx` (a SELECT + optional UPDATE that finds nothing to dispatch). Those land as extra root +// traces unrelated to the emit under test — and because the single HDI container is shared across all +// test files, scans triggered by other files' lingering workers show up too. Filter those pure +// outbox-scan traces so the exact root-count assertions stay stable. A scan trace is a `db - tx` root +// whose every span only touches `cds.outbox.Messages` (no application entity, no messaging/handle span). +const isOutboxScanTrace = g => + g.root.name === 'db - tx' && g.all.every(s => s.name === 'db - tx' || s.name.includes('cds.outbox.Messages')) + +// Drop the outbox-scan bookkeeping traces from a `groupedByTrace()` array, returning the meaningful groups. +const meaningful = groups => groups.filter(g => !isOutboxScanTrace(g)) + +module.exports = { + asExternalClient, + clearOutbox, + flushSpans, + eventually, + makeExpectEventually, + isOutboxScanTrace, + meaningful +} From f125e34bf9eccac67569037c3a62b3229d60f63c Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Thu, 20 Aug 2026 21:50:40 +0200 Subject: [PATCH 16/31] test: configure via .cdsrc.json profiles instead of process.env.cds_* (#486) (#493) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- test/bookshop/.cdsrc.json | 32 +++++++++++++++++++ test/bookshop/package.json | 10 ------ test/logging.test.js | 18 +++++------ test/passport.test.js | 8 ++--- test/tracing-attributes.test.js | 6 ++-- test/tracing-messaging-inboxed.test.js | 9 ++---- ...racing-messaging-persistent-outbox.test.js | 7 ++-- test/tracing-messaging-without-outbox.test.js | 8 ++--- test/tracing-remote-native.test.js | 9 +++--- test/tracing.test.js | 12 ++++--- 10 files changed, 65 insertions(+), 54 deletions(-) diff --git a/test/bookshop/.cdsrc.json b/test/bookshop/.cdsrc.json index 842dfd24..4b9e60df 100644 --- a/test/bookshop/.cdsrc.json +++ b/test/bookshop/.cdsrc.json @@ -1,8 +1,19 @@ { + "requires": { + "messaging": { + "kind": "local-messaging", + "_kind": "file-based-messaging", + "file": "../msg-box" + } + }, + "log": { + "cls_custom_fields": ["tenant_id"] + }, "[logging]": { "requires": { "telemetry": { "tracing": { + "exporter": false, "sampler": { "ignoreIncomingPaths": ["/odata/v4/admin/Genres"] } @@ -87,6 +98,27 @@ } } }, + "[sampler-ignore-authors]": { + "requires": { + "telemetry": { + "tracing": { + "sampler": { + "ignoreIncomingPaths": ["/odata/v4/admin/Authors"] + } + } + } + } + }, + "[native-fetch]": { + "remote": { + "native_fetch": true + } + }, + "[no-scheduling]": { + "requires": { + "scheduling": false + } + }, "[persistent-outbox]": { "requires": { "messaging": { diff --git a/test/bookshop/package.json b/test/bookshop/package.json index bf574fe9..0c5c0c71 100644 --- a/test/bookshop/package.json +++ b/test/bookshop/package.json @@ -39,11 +39,6 @@ } } }, - "messaging": { - "kind": "local-messaging", - "_kind": "file-based-messaging", - "file": "../msg-box" - }, "queue": { "legacyLocking": false }, @@ -76,11 +71,6 @@ } } }, - "log": { - "cls_custom_fields": [ - "tenant_id" - ] - }, "fiori": { "draft_deletion_timeout": false } diff --git a/test/logging.test.js b/test/logging.test.js index 84f06f3b..c75b5000 100644 --- a/test/logging.test.js +++ b/test/logging.test.js @@ -1,14 +1,14 @@ /* eslint-disable no-console */ -// REVISIT: even with profile "logging", cls_custom_fields from package.json wins -process.env.cds_log = JSON.stringify({ cls_custom_fields: ['foo'] }) - -// This test asserts the exported LogRecords only. Disable the tracing signal (no exporter → -// lib/tracing/index.js bails out early) so the queue SchedulingService's outbox-scan "elapsed -// times:" trace primer is never produced and can't land in the console.dir spy window. Without -// this, on HANA the outbox poll fires later than any fixed drain and the primer flakes the count. -process.env.cds_requires_telemetry_tracing = JSON.stringify({ exporter: false }) - +// Config lives in the `[logging]` profile of test/bookshop/.cdsrc.json: +// - log.cls_custom_fields: ['foo'] — the profile's own override (the base default is +// ['tenant_id']). #486 moved the base `cds.log`/`messaging` defaults out of package.json into +// .cdsrc.json base so profiles can win: package.json config loads AFTER .cdsrc.json and would +// otherwise override any profile. +// - requires.telemetry.tracing.exporter: false to disable the tracing signal (no exporter → +// lib/tracing/index.js bails out early) so the queue SchedulingService's outbox-scan "elapsed +// times:" trace primer is never produced and can't land in the console.dir spy window. Without +// this, on HANA the outbox poll fires later than any fixed drain and the primer flakes the count. const cds = require('@sap/cds') const { expect, GET } = cds.test(__dirname + '/bookshop', '--profile', 'logging') diff --git a/test/passport.test.js b/test/passport.test.js index 1f9de9b0..0b58693e 100644 --- a/test/passport.test.js +++ b/test/passport.test.js @@ -2,11 +2,11 @@ process.env.SAP_PASSPORT = 'true' // CDS v10 enables scheduling by default; its periodic outbox reads run in // their own transactions and cause spurious SAP_PASSPORT set/reset pairs on -// the connection, breaking the deterministic _count assertions below. -process.env.cds_requires_scheduling = 'false' - +// the connection, breaking the deterministic _count assertions below. The +// `no-scheduling` profile (requires.scheduling: false) in test/bookshop/.cdsrc.json +// disables it. const cds = require('@sap/cds') -const { expect, GET } = cds.test().in(__dirname + '/bookshop') +const { expect, GET } = cds.test(__dirname + '/bookshop', '--profile', 'no-scheduling') describe('SAP Passport', () => { if (cds.env.requires.db.kind === 'sqlite') return test.skip('n/a for SQLite', () => {}) diff --git a/test/tracing-attributes.test.js b/test/tracing-attributes.test.js index 5b539128..535cf863 100644 --- a/test/tracing-attributes.test.js +++ b/test/tracing-attributes.test.js @@ -1,8 +1,8 @@ // Use native fetch in CDS OQ so @opentelemetry/instrumentation-undici can see outbound calls -process.env.cds_remote_native__fetch = 'true' - +// (cds.env.remote.native_fetch = true), configured via the `native-fetch` profile in +// test/bookshop/.cdsrc.json, composed with `tracing-in-memory`. const cds = require('@sap/cds') -const { expect, data } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') +const { expect, data } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory, native-fetch') const http = require('http') // The tracing-in-memory profile (see test/bookshop/.cdsrc.json) configures diff --git a/test/tracing-messaging-inboxed.test.js b/test/tracing-messaging-inboxed.test.js index d7a158fc..399a335e 100644 --- a/test/tracing-messaging-inboxed.test.js +++ b/test/tracing-messaging-inboxed.test.js @@ -24,13 +24,8 @@ const otel = require('@opentelemetry/api') // // Tolerated: allow one extra root for the scheduling-service bookkeeping startup scan. -// REVISIT: profile config wins for kind/file, but explicit env override sidesteps it. -process.env.cds_requires_messaging = JSON.stringify({ - kind: 'file-based-messaging', - file: `../${CASE}`, - inboxed: true -}) - +// Messaging config (kind/file/inboxed) comes from the `inboxed` profile in +// test/bookshop/.cdsrc.json, composed with `tracing-in-memory` by the shared harness. const CHECK = ({ expect, rootSpans, groupedByTrace }) => { // Producer trace const producer = groupedByTrace.find(g => g.all.some(s => s.name === 'AdminService - handle test_emit')) diff --git a/test/tracing-messaging-persistent-outbox.test.js b/test/tracing-messaging-persistent-outbox.test.js index 6744e530..bb00d588 100644 --- a/test/tracing-messaging-persistent-outbox.test.js +++ b/test/tracing-messaging-persistent-outbox.test.js @@ -2,11 +2,8 @@ const CASE = 'persistent-outbox' const otel = require('@opentelemetry/api') -// REVISIT: even with profile "persistent-outbox", messaging kind and file from package.json wins -process.env.cds_requires_messaging = JSON.stringify({ - kind: 'file-based-messaging', - file: `../${CASE}` -}) +// Messaging config (kind/file) comes from the `persistent-outbox` profile in +// test/bookshop/.cdsrc.json, composed with `tracing-in-memory` by the shared harness. // --- Span hierarchy for the persistent-outbox case --------------------------------------- // diff --git a/test/tracing-messaging-without-outbox.test.js b/test/tracing-messaging-without-outbox.test.js index 021a84a4..15b9b647 100644 --- a/test/tracing-messaging-without-outbox.test.js +++ b/test/tracing-messaging-without-outbox.test.js @@ -2,12 +2,8 @@ const CASE = 'without-outbox' const otel = require('@opentelemetry/api') -// REVISIT: even with profile "without-outbox", messaging kind and file from package.json wins -process.env.cds_requires_messaging = JSON.stringify({ - kind: 'file-based-messaging', - file: `../${CASE}`, - outboxed: false -}) +// Messaging config (kind/file/outboxed) comes from the `without-outbox` profile in +// test/bookshop/.cdsrc.json, composed with `tracing-in-memory` by the shared harness. // Without outbox, file-based messaging writes directly to the file from the producer's // transaction (no queue worker). The file watcher delivers asynchronously as a new diff --git a/test/tracing-remote-native.test.js b/test/tracing-remote-native.test.js index 857c5f19..9d6a4954 100644 --- a/test/tracing-remote-native.test.js +++ b/test/tracing-remote-native.test.js @@ -1,9 +1,8 @@ -// Force CAP to use native fetch for outbound remote calls (instead of the cloud sdk). -// This must be set before @sap/cds is loaded, so it lives at the very top of the file. -process.env.cds_remote_native__fetch = 'true' - +// Force CAP to use native fetch for outbound remote calls (instead of the cloud sdk) via the +// `native-fetch` profile (cds.env.remote.native_fetch = true) in test/bookshop/.cdsrc.json, +// composed with `tracing-in-memory`. const cds = require('@sap/cds') -const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') +const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory, native-fetch') const http = require('http') // The tracing-in-memory profile (see test/bookshop/.cdsrc.json) configures diff --git a/test/tracing.test.js b/test/tracing.test.js index 4e7508b0..47407588 100644 --- a/test/tracing.test.js +++ b/test/tracing.test.js @@ -1,10 +1,12 @@ // REVISIT: jest breaks otel's patching of incoming request handling -> we can't ignore via ignoreIncomingRequestHook -process.env.cds_requires_telemetry_tracing_sampler = JSON.stringify({ - ignoreIncomingPaths: ['/odata/v4/admin/Authors'] -}) - +// The sampler's ignoreIncomingPaths (/odata/v4/admin/Authors) is configured via the +// `sampler-ignore-authors` profile in test/bookshop/.cdsrc.json, composed with `tracing-in-memory`. const cds = require('@sap/cds') -const { expect, GET, POST } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') +const { expect, GET, POST } = cds.test( + __dirname + '/bookshop', + '--profile', + 'tracing-in-memory, sampler-ignore-authors' +) // Assert against the structured ReadableSpan objects captured by MyInMemorySpanExporter // (configured via the tracing-in-memory profile in test/bookshop/.cdsrc.json) — no From 057c829f6732ea1fb7d5739bbe02fb5be096e0db Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Fri, 21 Aug 2026 14:22:10 +0200 Subject: [PATCH 17/31] docs: add TESTING.md; trim duplicated test comments to pointers (#487) (#494) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- TESTING.md | 134 ++++++++++++++++++ test/metrics-outbox-multitenant.test.js | 5 +- test/tracing-messaging-inboxed.test.js | 7 +- ...racing-messaging-persistent-outbox.test.js | 7 +- test/tracing-messaging.js | 14 +- test/tracing-mt.test.js | 5 +- test/tracing-outboxed-batch.test.js | 13 +- test/tracing-scheduled.test.js | 16 +-- 8 files changed, 154 insertions(+), 47 deletions(-) create mode 100644 TESTING.md diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 00000000..07c99264 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,134 @@ +# Testing + +This document is the single, authoritative place for the hard-won, non-obvious knowledge behind the `@cap-js/telemetry` test suite. It is meant for contributors: read it before adding or debugging a test. Test files keep only the rationale that is local to a specific test; anything general or repeated lives here. + +## Running the tests + +```sh +npm test # vitest, sqlite in-memory (the default) +node_modules/.bin/vitest run # same, without the --silent from the npm script +``` + +- **Runner:** [Vitest](https://vitest.dev). Config in [`vitest.config.mjs`](vitest.config.mjs). +- **Default database:** `@cap-js/sqlite`, in-memory. No external services are needed for the default run. +- **Test app:** a small bookshop under [`test/bookshop`](test/bookshop) — CDS model, services, and test-only wiring (exporters/reader, ignore hooks). Each test spins it up with `cds.test(__dirname + '/bookshop', ...)`. +- **CI matrix:** Node 22 & 24 × cds 9 & 10 (see [`.github/workflows/ci.yml`](.github/workflows/ci.yml)). The lint job additionally runs ESLint and `oxfmt --check`. + +Lint / format locally: + +```sh +npx eslint . --max-warnings=0 # npm run lint +npx oxfmt --check # npm run format:check +``` + +## sqlite vs HANA + +The suite runs on two databases, and the difference in DB **isolation model** drives most of the test infrastructure. + +| | sqlite (default / PR CI) | HANA (separate workflow) | +| --- | --- | --- | +| DB per test file | **Own** in-memory DB — each file is fully isolated | **One shared** HDI container across *all* files | +| File parallelism | Full parallelism | **Serial** (`fileParallelism: false`) | +| Timeouts | 42s test / 30s hook | **10×** test timeout | +| Retries | 0 (deterministic) | `retry: 2` (self-heal unlucky timing) | +| Outbox bleed | Impossible (fresh DB) | Must be actively prevented (see below) | + +Because HANA reuses one container for the whole run, a background queue/outbox worker from one file can still be draining when the next file starts and would dispatch leftover rows — adding foreign `cds.spawn - run task` root spans that break exact root-count assertions. The queue/outbox test files therefore: + +- **clear the outbox** in `beforeEach` (before resetting the span buffer, so the `DELETE`'s own spans aren't captured), and +- **settle** in `afterAll`: clear, wait for the last worker iteration, clear again — every clear timeout-bounded via `clearOutbox` so a draining pool can't hang the hook. + +All of this is a **no-op on sqlite** (fresh in-memory DB per file), gated on the HANA signal. + +### How the HANA path is signalled + +- The HANA job runs when `process.env.CI && process.env.HANA_DRIVER` are set. On that path `vitest.config.mjs` raises the timeout, disables file parallelism, enables retries, and excludes the multitenancy suites (see [Sanctioned skips](#sanctioned-skips)). +- It also sets **`process.env.TELEMETRY_TEST_HANA = '1'`** in the config module. Test files that must branch at **collection time** (before `cds.test()` applies its `--profile`) read this env var rather than `cds.env`: reading `cds.env` that early would freeze the env singleton before the profile is applied, so the tracer provider would be built with the wrong exporter and no spans would be captured. +- HANA runs from its **own workflow**, [`.github/workflows/hana.yml`](.github/workflows/hana.yml) — `workflow_dispatch` only, against a protected `hana` environment with a pre-provisioned HDI container. It is **not** part of the PR CI. + +## Configuration via profiles (not env) + +Test configuration lives in **[`test/bookshop/.cdsrc.json`](test/bookshop/.cdsrc.json)** as cds config profiles, selected per test file: + +```js +cds.test(dir, '--profile', 'tracing-in-memory') // one profile +cds.test(dir, '--profile', 'metrics-outbox, multitenancy') // profiles compose +``` + +| Profile | What it does | +| --- | --- | +| `[logging]` | Disables the tracing exporter (`false`) so no outbox-scan trace primer leaks into the console spy; wires a `ConsoleLogRecordExporter` + custom processor; sets `log.format: json` and `cls_custom_fields: ['foo']`. | +| `[metrics]` | Wires `MyInMemoryMetricReader`; short `exportIntervalMillis` (100). | +| `[metrics-outbox]` | Enables the queue (`_queue: true`) + in-memory reader; `exportIntervalMillis` 1000 (leaves the shared HANA worker DB headroom). | +| `[metrics-outbox-disabled]` | Queue metrics off (`_queue: false`) — asserts no `queue.*` datapoints are ever exported. | +| `[tracing-in-memory]` | Wires `MyInMemorySpanExporter` as the trace exporter. | +| `[sampler-ignore-authors]` | Adds `/odata/v4/admin/Authors` to the sampler's `ignoreIncomingPaths`. | +| `[native-fetch]` | `remote.native_fetch = true` — routes outbound remote calls through native fetch (undici instrumentation) instead of the Cloud SDK. | +| `[no-scheduling]` | `requires.scheduling: false` — disables cds 10's default periodic outbox reads (they cause spurious passport set/reset pairs). | +| `[persistent-outbox]` | file-based messaging with a persistent outbox. | +| `[inboxed]` | file-based messaging with `inboxed: true` (producer- **and** consumer-side queue workers). | +| `[without-outbox]` | file-based messaging with `outboxed: false` (writes to the file directly from the producer tx). | + +> The `[multitenancy]` profile lives in the app's own `test/bookshop/package.json` (auth users + `multitenancy: true`), composed with the above where needed. + +### Load-order gotcha (from #486) + +`package.json` cds config is loaded **after** `.cdsrc.json` (last-writer-wins). So any base default that a profile must be able to override has to live in the **`.cdsrc.json` base**, not in `package.json` — otherwise `package.json` would clobber the profile. #486 moved the base `cds.log` / `messaging` defaults into `.cdsrc.json` for exactly this reason. + +> **Do not** reintroduce `process.env.cds_*` string-JSON config. That fragile pattern (config via stringified JSON in env vars, order-sensitive against the `@sap/cds` require) was removed in #486. Use profiles. + +## In-memory test infrastructure + +Two exporter-shaped classes capture telemetry into module-level arrays that tests import directly — asserting on **structured spans/datapoints**, never scraping `console.dir` output: + +- **[`test/bookshop/lib/MyInMemorySpanExporter.js`](test/bookshop/lib/MyInMemorySpanExporter.js)** — spans accumulate in `captured`; helpers `groupedByTrace()` / `rootSpans()` / `reset()`. Wired via the `tracing-in-memory` profile. +- **[`test/bookshop/lib/MyInMemoryMetricReader.js`](test/bookshop/lib/MyInMemoryMetricReader.js)** — metrics captured via the metrics profiles. It honors **DELTA temporality**, matching production (`lib/metrics/index.js` configures the real exporter with `AggregationTemporality.DELTA`), so the tests validate the real export shape. Under DELTA, counter datapoints report only the increment since the last collection, so the reader folds SUM increments into running totals while GAUGE datapoints keep their latest absolute value. + +**KEY RULE:** neither module may `require('@sap/cds')` at module top. Doing so once broke span capture — the cds require has to happen inside the test file, *after* the profile is applied. Both modules stay dependency-light (only `@opentelemetry/*` primitives + node timers). + +Cross-file correctness of the metric reader's process-level singletons relies on Vitest isolating each file in its own worker (`pool: 'forks'`, `isolate: true`); two files sharing the module in one process would bleed counter totals together. + +## Shared test helpers — `test/utils.js` + +Centralized in [`test/utils.js`](test/utils.js) (added in #488) so the ~10 tracing/metrics suites stop copy-pasting them. See the doc comments at each definition for full detail: + +- **`flushSpans()`** — force-flush the tracer provider's span processor so buffered spans reach `captured`. +- **`eventually(fn, { flush, timeout, interval })`** — state-based wait: repeatedly flush + re-run the assertion until it holds or times out. Replaces fixed `wait(...)` sleeps that flake on HANA (background/spawned work flushes after any reasonable fixed window). `flush` defaults to `flushSpans`. +- **`makeExpectEventually(flush, { timeout, interval })`** — builds an `expectEventually(assertion)` bound to a specific flush target + poll defaults (metric suites pass the reader's `forceFlush`). +- **`clearOutbox(timeout)`** — best-effort, timeout-bounded outbox `DELETE` that can never hang the surrounding hook (a draining HANA pool could otherwise block indefinitely). +- **`asExternalClient(fn)`** — runs a client request under `suppressTracing`. The in-process test client would otherwise create an outgoing **CLIENT** span for every request (an artificial extra root that also overwrites any manually-set `traceparent`). Real callers are separate, un-instrumented processes; this models that so the incoming SERVER span is created normally and stays the trace root. +- **`isOutboxScanTrace(g)` / `meaningful(groups)`** — filter out the queue scheduler's pure outbox-scan bookkeeping traces (a `db - tx` root touching only `cds.outbox.Messages`) so exact root-count assertions stay stable on the shared HANA container. + +**The flush + poll pattern:** instead of `await wait(500)` then asserting, wrap assertions in `eventually`/`expectEventually` — it flushes, checks, and returns the instant the state holds (fast on sqlite, resilient to HANA's variable worker latency). + +## HTTP instrumentation (from #475) + +HTTP instrumentation is enabled in the test app. Consequences the tests rely on: + +- **Incoming** requests produce a **SERVER** span that becomes each request trace's **root**; existing ` - tx` spans reparent under it (reparenting). The SERVER span also adopts the W3C trace context from an incoming `traceparent` header. +- **Outgoing** requests produce **CLIENT** spans. + +Because the test HTTP client runs in-process, its outgoing requests would themselves create CLIENT-span roots and pollute the trace. Tests therefore wrap client requests in **`asExternalClient`** (see above) to model an external, un-instrumented caller. + +## Sanctioned skips + +Only **two** skips are allowed (per #477). Any *new* skip must be justified against this bar; everything else that is skipped is tracked debt. + +1. **SAP Passport** — [`test/passport.test.js`](test/passport.test.js) skips on **sqlite** (`db.kind === 'sqlite'`). SAP Passport is a HANA session-context feature with no sqlite equivalent; it runs on HANA. +2. **Multitenancy on HANA** — `tracing-mt.test.js` and `metrics-outbox-multitenant.test.js` are **excluded from the HANA job** (in `vitest.config.mjs`). MTX tenant subscription needs a bound BTP Service Manager to provision per-tenant HDI containers, which the single pre-provisioned HDI container in CI lacks. They run fully on sqlite (in-memory tenants). + +This is the inverse pairing: passport is sqlite-skip / HANA-run; multitenancy is HANA-skip / sqlite-run. + +Other skips are **debt tracked in #477**, not sanctioned exceptions: + +- **§1 — queue-worker tracing on sqlite:** `tracing-scheduled`, `tracing-outboxed-batch`, `tracing-messaging-inboxed`, `tracing-messaging-persistent-outbox` skip their worker-span cases on sqlite. Published `@sap/cds` uses a raw `setTimeout` bypass (not `cds.spawn`) for the sqlite queue worker to avoid a single-writer deadlock, so the `cds.spawn - run task` root span never appears. Gated on a cds queue-spawn fix landing; remove with a follow-up. +- **§3 — unimplemented stubs:** placeholder `test.skip` cases in `tracing.test.js` and `tracing-mt.test.js` (individual handlers, remote, `$batch`, `srv.emit`, `cds.spawn` under multitenancy) — real coverage gaps to be written. + +## Known caveats & gotchas + +- **`startup > NO_TELEMETRY=true` local artifact.** [`test/startup.test.js`](test/startup.test.js) shells out to `cds serve` with env overrides. In some local shells the `NO_TELEMETRY=true` case can fail due to inherited environment; it passes in CI and in a clean environment. This is a pre-existing local-env artifact, not a product bug. +- **Internal-registry lockfile trap for `@sap/*` installs.** Installing `@sap/*` packages against SAP's internal registry can rewrite `package-lock.json` to internal URLs. Always install against the public npm registry and verify the lockfile is clean before committing: + + ```sh + grep -c int.repositories.cloud.sap package-lock.json # must be 0 + ``` diff --git a/test/metrics-outbox-multitenant.test.js b/test/metrics-outbox-multitenant.test.js index 3f7956bf..77a4310b 100644 --- a/test/metrics-outbox-multitenant.test.js +++ b/test/metrics-outbox-multitenant.test.js @@ -25,9 +25,8 @@ function metricValue(tenant, metric) { // wired, so a misconfigured profile fails loudly instead of busy-spinning the full timeout. const expectEventually = makeExpectEventually(forceFlush, { timeout: 10000, interval: 25 }) -// Multitenancy needs a bound BTP Service Manager (MTX) to provision per-tenant HDI containers. -// The HANA CI runs against a single pre-provisioned HDI container with no Service Manager, so this -// suite is excluded from the HANA job in vitest.config.mjs. It runs on sqlite (in-memory tenants). +// Multitenancy runs on sqlite only; excluded from the HANA job (needs a bound Service Manager). +// See TESTING.md → Sanctioned skips. describe('queue metrics for multi tenant service', () => { const T1 = 'tenant_1' const T2 = 'tenant_2' diff --git a/test/tracing-messaging-inboxed.test.js b/test/tracing-messaging-inboxed.test.js index 399a335e..b565e21f 100644 --- a/test/tracing-messaging-inboxed.test.js +++ b/test/tracing-messaging-inboxed.test.js @@ -55,11 +55,8 @@ const CHECK = ({ expect, rootSpans, groupedByTrace }) => { } describe(`tracing messaging - ${CASE}`, () => { - // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. - // Detect the DB via the env var set by vitest.config.mjs for the HANA job, NOT via cds.env: - // reading cds.env at collection time would freeze the singleton before cds.test() applies its - // `--profile`, so the tracer provider would be built with the default ConsoleSpanExporter and - // MyInMemorySpanExporter would never receive spans. + // Queue-worker tracing needs cds.spawn on sqlite — skipped here, tracked in #477 §1. + // See TESTING.md → Sanctioned skips (and HANA signalling: why we branch on TELEMETRY_TEST_HANA, not cds.env). if (!process.env.TELEMETRY_TEST_HANA) { test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) return diff --git a/test/tracing-messaging-persistent-outbox.test.js b/test/tracing-messaging-persistent-outbox.test.js index bb00d588..2cdcb212 100644 --- a/test/tracing-messaging-persistent-outbox.test.js +++ b/test/tracing-messaging-persistent-outbox.test.js @@ -99,11 +99,8 @@ const CHECK = ({ expect, rootSpans, groupedByTrace }) => { } describe(`tracing messaging - ${CASE}`, () => { - // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. - // Detect the DB via the env var set by vitest.config.mjs for the HANA job, NOT via cds.env: - // reading cds.env at collection time would freeze the singleton before cds.test() applies its - // `--profile`, so the tracer provider would be built with the default ConsoleSpanExporter and - // MyInMemorySpanExporter would never receive spans. + // Queue-worker tracing needs cds.spawn on sqlite — skipped here, tracked in #477 §1. + // See TESTING.md → Sanctioned skips (and HANA signalling: why we branch on TELEMETRY_TEST_HANA, not cds.env). if (!process.env.TELEMETRY_TEST_HANA) { test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) return diff --git a/test/tracing-messaging.js b/test/tracing-messaging.js index ef12a8a0..91a9fecc 100644 --- a/test/tracing-messaging.js +++ b/test/tracing-messaging.js @@ -22,11 +22,8 @@ module.exports = (CASE, CHECK) => { }) afterAll(async () => { - // On the shared HANA HDI container, a still-draining background queue worker from THIS file - // would dispatch into the NEXT file's run and add foreign `cds.spawn - run task` roots that - // break its exact root-count CHECKs. Clear the shared outbox, let the last worker settle, then - // clear again. Every clear is timeout-bounded (clearOutbox) so a draining pool can't hang the - // hook. HANA-only: sqlite gets a fresh in-memory DB per file, so the settle is pointless there. + // HANA-only outbox settle so a draining worker can't bleed into the next file; no-op on sqlite. + // See TESTING.md → sqlite vs HANA (outbox bleed on the shared HANA container). if (process.env.TELEMETRY_TEST_HANA) { await clearOutbox() await wait(5000) @@ -36,11 +33,8 @@ module.exports = (CASE, CHECK) => { }) beforeEach(async () => { - // Clear any outbox rows left behind by a prior test file BEFORE resetting the span buffer. - // The single HANA HDI container is shared across all files, so a leftover message would be - // dispatched by THIS file's queue worker — producing a foreign `cds.spawn - run task` root - // that breaks the exact root-count CHECKs. Reset AFTER so the DELETE's own spans aren't - // captured. (No-op on sqlite, where each file gets its own in-memory DB.) + // Clear the shared outbox before resetting the span buffer; no-op on sqlite. + // See TESTING.md → sqlite vs HANA (outbox bleed on the shared HANA container). await clearOutbox() reset() }) diff --git a/test/tracing-mt.test.js b/test/tracing-mt.test.js index 8aa91e74..4eb412c1 100644 --- a/test/tracing-mt.test.js +++ b/test/tracing-mt.test.js @@ -4,9 +4,8 @@ const { expect, GET } = cds.test('serve', '--in-memory', '--project', __dirname const { reset, captured } = require('./bookshop/lib/MyInMemorySpanExporter') -// Multitenancy needs a bound BTP Service Manager (MTX) to provision per-tenant HDI containers. -// The HANA CI runs against a single pre-provisioned HDI container with no Service Manager, so this -// suite is excluded from the HANA job in vitest.config.mjs. It runs on sqlite (in-memory tenants). +// Multitenancy runs on sqlite only; excluded from the HANA job (needs a bound Service Manager). +// See TESTING.md → Sanctioned skips. describe('tracing with multitenancy', () => { const TENANT1 = 'tenant_1' const TENANT2 = 'tenant_2' diff --git a/test/tracing-outboxed-batch.test.js b/test/tracing-outboxed-batch.test.js index a7a4d44c..5a8405f3 100644 --- a/test/tracing-outboxed-batch.test.js +++ b/test/tracing-outboxed-batch.test.js @@ -9,11 +9,8 @@ const { hrTimeToNanoseconds } = require('@opentelemetry/core') const { eventually } = require('./utils') describe('tracing for outboxed batch (chunk-size fan-out)', () => { - // Queue-worker spans need cds.spawn on sqlite (pending cds fix). REMOVE with follow-up PR. - // Detect the DB via the env var set by vitest.config.mjs for the HANA job, NOT via cds.env: - // reading cds.env at collection time would freeze the singleton before cds.test() applies its - // `--profile`, so the tracer provider would be built with the default ConsoleSpanExporter and - // MyInMemorySpanExporter would never receive spans (captured stays empty). + // Queue-worker tracing needs cds.spawn on sqlite — skipped here, tracked in #477 §1. + // See TESTING.md → Sanctioned skips (and HANA signalling: why we branch on TELEMETRY_TEST_HANA, not cds.env). if (!process.env.TELEMETRY_TEST_HANA) { test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) return @@ -25,10 +22,8 @@ describe('tracing for outboxed batch (chunk-size fan-out)', () => { }) beforeEach(async () => { - // Clear outbox rows left by a prior test file BEFORE resetting the span buffer — the HANA - // HDI container is shared across all files, so a leftover message would be dispatched by this - // file's worker and add a foreign `cds.spawn - run task` root. Reset AFTER so the DELETE's own - // spans aren't captured. (No-op on sqlite: per-file in-memory DB.) + // Clear the shared outbox before resetting the span buffer; no-op on sqlite. + // See TESTING.md → sqlite vs HANA (outbox bleed on the shared HANA container). await DELETE.from('cds.outbox.Messages') reset() }) diff --git a/test/tracing-scheduled.test.js b/test/tracing-scheduled.test.js index 31c33182..df4c0c34 100644 --- a/test/tracing-scheduled.test.js +++ b/test/tracing-scheduled.test.js @@ -26,14 +26,8 @@ const otel = require('@opentelemetry/api') const { asExternalClient, eventually } = require('./utils') describe('tracing for scheduled tasks', () => { - // Queue-worker spans (cds.spawn - run task root) require @sap/cds to route the sqlite - // queue worker through cds.spawn. Published cds uses a raw setTimeout bypass on sqlite - // (to avoid a single-writer deadlock), so those spans never appear. Skip until the cds - // fix lands (cap/cds test/queue-spawn-sqlite-extended-tenant). REMOVE with follow-up PR. - // Detect the DB via the env var set by vitest.config.mjs for the HANA job, NOT via cds.env: - // reading cds.env at collection time would freeze the singleton before cds.test() applies its - // `--profile`, so the tracer provider would be built with the default ConsoleSpanExporter and - // MyInMemorySpanExporter would never receive spans (captured stays empty). + // Queue-worker tracing needs cds.spawn on sqlite — skipped here, tracked in #477 §1. + // See TESTING.md → Sanctioned skips (and HANA signalling: why we branch on TELEMETRY_TEST_HANA, not cds.env). if (!process.env.TELEMETRY_TEST_HANA) { test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) return @@ -45,10 +39,8 @@ describe('tracing for scheduled tasks', () => { }) beforeEach(async () => { - // Clear outbox rows left by a prior test file BEFORE resetting the span buffer — the HANA - // HDI container is shared across all files, so a leftover message would be dispatched by this - // file's worker and add a foreign `cds.spawn - run task` root. Reset AFTER so the DELETE's own - // spans aren't captured. (No-op on sqlite: per-file in-memory DB.) + // Clear the shared outbox before resetting the span buffer; no-op on sqlite. + // See TESTING.md → sqlite vs HANA (outbox bleed on the shared HANA container). await DELETE.from('cds.outbox.Messages') reset() }) From 33b07efcc1c763c88a7bc421e8c03d15d8d8f38a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:49:12 +0200 Subject: [PATCH 18/31] chore(deps-dev): bump the dev-dependencies group across 1 directory with 10 updates (#485) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the dev-dependencies group with 10 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@cap-js/cds-test](https://github.com/cap-js/cds-test) | `1.0.1` | `1.0.2` | | [@opentelemetry/exporter-metrics-otlp-grpc](https://github.com/open-telemetry/opentelemetry-js) | `0.219.0` | `0.221.0` | | [@opentelemetry/exporter-metrics-otlp-proto](https://github.com/open-telemetry/opentelemetry-js) | `0.219.0` | `0.221.0` | | [@opentelemetry/exporter-trace-otlp-grpc](https://github.com/open-telemetry/opentelemetry-js) | `0.219.0` | `0.221.0` | | [@opentelemetry/exporter-trace-otlp-proto](https://github.com/open-telemetry/opentelemetry-js) | `0.219.0` | `0.221.0` | | [@opentelemetry/instrumentation-host-metrics](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/instrumentation-host-metrics) | `0.2.0` | `0.4.0` | | [@opentelemetry/instrumentation-runtime-node](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/instrumentation-runtime-node) | `0.32.0` | `0.34.0` | | [@sap/cds-mtxs](https://cap.cloud.sap/) | `4.0.1` | `4.0.2` | | [axios](https://github.com/axios/axios) | `1.18.1` | `1.19.0` | | [eslint](https://github.com/eslint/eslint) | `10.6.0` | `10.9.1` | Updates `@cap-js/cds-test` from 1.0.1 to 1.0.2
Release notes

Sourced from @​cap-js/cds-test's releases.

v1.0.2

Fixed

  • Buffer request bodies are no longer serialized via JSON.stringify()
  • In chest-powered tests, inspectPort configuration will be passed to node:test.run(...) when debugging
  • In chest-powered tests, concurrency of node:test.run(...) will be limitted to 1 to enable debugging
Changelog

Sourced from @​cap-js/cds-test's changelog.

[1.0.2] - 2026-08-04

Fixed

  • Buffer request bodies are no longer serialized via JSON.stringify()
  • In chest-powered tests, inspectPort configuration will be passed to node:test.run(...) when debugging
  • In chest-powered tests, concurrency of node:test.run(...) will be limitted to 1 to enable debugging
Commits
  • 1304e9d fix: pass debug flag to runner (#96)
  • 540bc36 Bump @​cap-js/sqlite from 2.4.0 to 3.0.2 (#102)
  • 4f150df fix(naxios): send Buffer request bodies as raw bytes (#103)
  • 4d6ca38 feat: remove sleep API (#98)
  • 6d8eac9 chore: open dependency to cap-js/sqlite 3 (#94)
  • 7456cb5 fix: fulfilled and rejected can be awaited (#93)
  • fa7721a Bump qs from 6.15.1 to 6.15.2 in the npm_and_yarn group across 1 directory (#91)
  • 7c833de fix: .to.be.fullfilled/rejected must return this (#92)
  • 0f1a65c Bump @​cap-js/sqlite from 2.2.0 to 2.4.0 (#88)
  • 90e6cf5 Temporarily reverting cds.test --with-mocks
  • Additional commits viewable in compare view

Updates `@opentelemetry/exporter-metrics-otlp-grpc` from 0.219.0 to 0.221.0
Release notes

Sourced from @​opentelemetry/exporter-metrics-otlp-grpc's releases.

experimental/v0.221.0

0.221.0

:boom: Breaking Changes

  • feat(sdk-logs)!: configure the force flush timeout per call #6931 @​LarryHu0217
    • (user-facing): LoggerProviderOptions.forceFlushTimeoutMillis has been removed; pass timeoutMillis to LoggerProvider.forceFlush() instead.
  • feat(instrumentation-http)!: emit only stable HTTP semantic conventions. The OTEL_SEMCONV_STABILITY_OPT_IN environment variable no longer changes HTTP attribute or metric emission — old (v1.7.0) and duplicate (http/http/dup) semconv outputs have been removed. #6819 @​maryliag
  • feat(instrumentation-fetch)!: emit only stable HTTP semantic conventions. The semconvStabilityOptIn instrumentation config option has been removed; old (v1.7.0) and duplicate semconv outputs are no longer emitted. #6819 @​maryliag
  • feat(instrumentation-xml-http-request)!: emit only stable HTTP semantic conventions. The semconvStabilityOptIn instrumentation config option has been removed; old (v1.7.0) and duplicate semconv outputs are no longer emitted. #6819 @​maryliag
  • feat(instrumentation-grpc)!: emit only stable network semantic conventions. The OTEL_SEMCONV_STABILITY_OPT_IN environment variable no longer changes attribute emission — net.peer.name and net.peer.port (old) are no longer set; only server.address and server.port (stable). #6819 @​maryliag

:rocket: Features

  • feat(sdk-logs): allow modifying ReadWriteLogRecord properties (including hrTime, hrTimeObserved, and spanContext) in accordance with the OpenTelemetry Logs specification #6923 @​Babul422
  • feat(sdk-node): emit a deprecation warning when the JaegerPropagator is selected via OTEL_PROPAGATORS or declarative config; use tracecontext instead. @​pichlermarc
  • feat(instrumentation-http): set error.type to status code in metrics for error requests. #6919 @​raphael-theriault-swi

experimental/v0.220.0

0.220.0

:boom: Breaking Changes

  • refactor(sdk-logs)!: refactor BatchLogRecordProcessor constructor signature #6817 @​trentm
    • (user-facing): BatchLogRecordProcessor now takes a single options object with all possible properties, instead of two separate arguments. For example, before new BatchLogRecordProcessor(exporter, { maxQueueSize: 1000 }), after new BatchLogRecordProcessor({ exporter, maxQueueSize: 1000 }).
    • interface BufferConfig -> interface BatchLogRecordProcessorOptions, and now includes the exporter property
    • interface BatchLogRecordProcessorBrowserConfig -> interface BatchLogRecordProcessorBrowserOptions
    • (user-facing): SimpleLogRecordProcessor now takes a single options object with all possible properties. For example, before new SimpleLogRecordProcessor(exporter), after new SimpleLogRecordProcessor({ exporter }). #6836
  • refactor(configuration)!: change config file parsing to not add default values, nor merge *_list fields #6765 @​trentm
  • docs(shim-opencensus): Notice: The @opentracing/shim-opencensus package will be removed in SDK 3.x, planned for approximately September 2026.
    • The OpenCensus and OpenTracing compatibility requirements in the OpenTelemetry specification have been deprecated.
  • chore(sdk-node)!: Drop support for deprecated OpenCensusMetricProducer from declarative config

:rocket: Features

  • feat(configuration): bump config schema to v1.1.0; rename without_scope_info → scope_info_enabled and without_target_info/development → target_info_enabled/development on the Prometheus pull exporter (semantics inverted), rename with_resource_constant_labels → resource_constant_labels. Validate file_format per the configuration versioning spec: accept any minor version of major 1 (e.g. 1.0, 1.1), warn when the minor version is newer than supported, and reject other major versions. #6781 @​MikeGoldsmith
  • feat(sdk-node): wire up id_generator from declarative config #6782 @​MikeGoldsmith
  • feat(sdk-node): wire up tracer_provider.sampler from declarative config (always_on, always_off, trace_id_ratio_based, parent_based); unrecognized variants warn and fall back to ParentBased(AlwaysOn) #6506 @​MikeGoldsmith
  • feat(propagator-env-carrier): empty name normalization #6827 @​pellared
  • feat(propagator-env-carrier): make EnvironmentGetter read the current process.env #6853 @​pellared

:bug: Bug Fixes

  • fix(sdk-logs): stop Logger.emit() doing work (record construction, metrics, processor onEmit) after the LoggerProvider has shut down #6826 @​anneheartrecord
  • fix(sdk-node): pass all config properties (endpoint, headers, timeout, TLS, compression, temporality preference, default histogram aggregation) to OTLP metric exporters in declarative config #6814 @​MikeGoldsmith
  • fix(sdk-logs): default BatchLogRecordProcessor scheduleDelayMillis is 1000 #6796 @​trentm
  • fix(configuration): percent-decode keys and values in resource.attributes_list per spec #6787 @​MikeGoldsmith
  • fix(configuration): default log_level to info in env-based config initialization for consistency with file-based config #6788 @​MikeGoldsmith

... (truncated)

Commits
  • 76fa6b5 chore: prepare next release (#6942)
  • 3787896 chore(deps): update dependency webpack-cli to v7.2.1 (#6934)
  • be5f757 fix(deps): update dependency body-parser to v2.3.0 [security] (#6941)
  • f6d8fbe chore(deps): lock file maintenance (#6559)
  • 9612732 chore: remove examples/dice from workspaces (#6937)
  • 7107906 chore: start using min-release-age in .npmrc, disable minimumReleaseAge for r...
  • a7e5d11 chore(deps): update dependency webpack to v5.108.4 (#6933)
  • af7a82d chore(deps): update dependency msw to v2.15.0 (#6831)
  • b9f57c6 chore(deps): update dependency @​types/webpack-env to v1.18.8 (#6877)
  • cbb4abf chore(deps): update ubuntu docker tag to v26 (#6635)
  • Additional commits viewable in compare view

Updates `@opentelemetry/exporter-metrics-otlp-proto` from 0.219.0 to 0.221.0
Release notes

Sourced from @​opentelemetry/exporter-metrics-otlp-proto's releases.

experimental/v0.221.0

0.221.0

:boom: Breaking Changes

  • feat(sdk-logs)!: configure the force flush timeout per call #6931 @​LarryHu0217
    • (user-facing): LoggerProviderOptions.forceFlushTimeoutMillis has been removed; pass timeoutMillis to LoggerProvider.forceFlush() instead.
  • feat(instrumentation-http)!: emit only stable HTTP semantic conventions. The OTEL_SEMCONV_STABILITY_OPT_IN environment variable no longer changes HTTP attribute or metric emission — old (v1.7.0) and duplicate (http/http/dup) semconv outputs have been removed. #6819 @​maryliag
  • feat(instrumentation-fetch)!: emit only stable HTTP semantic conventions. The semconvStabilityOptIn instrumentation config option has been removed; old (v1.7.0) and duplicate semconv outputs are no longer emitted. #6819 @​maryliag
  • feat(instrumentation-xml-http-request)!: emit only stable HTTP semantic conventions. The semconvStabilityOptIn instrumentation config option has been removed; old (v1.7.0) and duplicate semconv outputs are no longer emitted. #6819 @​maryliag
  • feat(instrumentation-grpc)!: emit only stable network semantic conventions. The OTEL_SEMCONV_STABILITY_OPT_IN environment variable no longer changes attribute emission — net.peer.name and net.peer.port (old) are no longer set; only server.address and server.port (stable). #6819 @​maryliag

:rocket: Features

  • feat(sdk-logs): allow modifying ReadWriteLogRecord properties (including hrTime, hrTimeObserved, and spanContext) in accordance with the OpenTelemetry Logs specification #6923 @​Babul422
  • feat(sdk-node): emit a deprecation warning when the JaegerPropagator is selected via OTEL_PROPAGATORS or declarative config; use tracecontext instead. @​pichlermarc
  • feat(instrumentation-http): set error.type to status code in metrics for error requests. #6919 @​raphael-theriault-swi

experimental/v0.220.0

0.220.0

:boom: Breaking Changes

  • refactor(sdk-logs)!: refactor BatchLogRecordProcessor constructor signature #6817 @​trentm
    • (user-facing): BatchLogRecordProcessor now takes a single options object with all possible properties, instead of two separate arguments. For example, before new BatchLogRecordProcessor(exporter, { maxQueueSize: 1000 }), after new BatchLogRecordProcessor({ exporter, maxQueueSize: 1000 }).
    • interface BufferConfig -> interface BatchLogRecordProcessorOptions, and now includes the exporter property
    • interface BatchLogRecordProcessorBrowserConfig -> interface BatchLogRecordProcessorBrowserOptions
    • (user-facing): SimpleLogRecordProcessor now takes a single options object with all possible properties. For example, before new SimpleLogRecordProcessor(exporter), after new SimpleLogRecordProcessor({ exporter }). #6836
  • refactor(configuration)!: change config file parsing to not add default values, nor merge *_list fields #6765 @​trentm
  • docs(shim-opencensus): Notice: The @opentracing/shim-opencensus package will be removed in SDK 3.x, planned for approximately September 2026.
    • The OpenCensus and OpenTracing compatibility requirements in the OpenTelemetry specification have been deprecated.
  • chore(sdk-node)!: Drop support for deprecated OpenCensusMetricProducer from declarative config

:rocket: Features

  • feat(configuration): bump config schema to v1.1.0; rename without_scope_info → scope_info_enabled and without_target_info/development → target_info_enabled/development on the Prometheus pull exporter (semantics inverted), rename with_resource_constant_labels → resource_constant_labels. Validate file_format per the configuration versioning spec: accept any minor version of major 1 (e.g. 1.0, 1.1), warn when the minor version is newer than supported, and reject other major versions. #6781 @​MikeGoldsmith
  • feat(sdk-node): wire up id_generator from declarative config #6782 @​MikeGoldsmith
  • feat(sdk-node): wire up tracer_provider.sampler from declarative config (always_on, always_off, trace_id_ratio_based, parent_based); unrecognized variants warn and fall back to ParentBased(AlwaysOn) #6506 @​MikeGoldsmith
  • feat(propagator-env-carrier): empty name normalization #6827 @​pellared
  • feat(propagator-env-carrier): make EnvironmentGetter read the current process.env #6853 @​pellared

:bug: Bug Fixes

  • fix(sdk-logs): stop Logger.emit() doing work (record construction, metrics, processor onEmit) after the LoggerProvider has shut down #6826 @​anneheartrecord
  • fix(sdk-node): pass all config properties (endpoint, headers, timeout, TLS, compression, temporality preference, default histogram aggregation) to OTLP metric exporters in declarative config #6814 @​MikeGoldsmith
  • fix(sdk-logs): default BatchLogRecordProcessor scheduleDelayMillis is 1000 #6796 @​trentm
  • fix(configuration): percent-decode keys and values in resource.attributes_list per spec #6787 @​MikeGoldsmith
  • fix(configuration): default log_level to info in env-based config initialization for consistency with file-based config #6788 @​MikeGoldsmith

... (truncated)

Commits
  • 76fa6b5 chore: prepare next release (#6942)
  • 3787896 chore(deps): update dependency webpack-cli to v7.2.1 (#6934)
  • be5f757 fix(deps): update dependency body-parser to v2.3.0 [security] (#6941)
  • f6d8fbe chore(deps): lock file maintenance (#6559)
  • 9612732 chore: remove examples/dice from workspaces (#6937)
  • 7107906 chore: start using min-release-age in .npmrc, disable minimumReleaseAge for r...
  • a7e5d11 chore(deps): update dependency webpack to v5.108.4 (#6933)
  • af7a82d chore(deps): update dependency msw to v2.15.0 (#6831)
  • b9f57c6 chore(deps): update dependency @​types/webpack-env to v1.18.8 (#6877)
  • cbb4abf chore(deps): update ubuntu docker tag to v26 (#6635)
  • Additional commits viewable in compare view

Updates `@opentelemetry/exporter-trace-otlp-grpc` from 0.219.0 to 0.221.0
Release notes

Sourced from @​opentelemetry/exporter-trace-otlp-grpc's releases.

experimental/v0.221.0

0.221.0

:boom: Breaking Changes

  • feat(sdk-logs)!: configure the force flush timeout per call #6931 @​LarryHu0217
    • (user-facing): LoggerProviderOptions.forceFlushTimeoutMillis has been removed; pass timeoutMillis to LoggerProvider.forceFlush() instead.
  • feat(instrumentation-http)!: emit only stable HTTP semantic conventions. The OTEL_SEMCONV_STABILITY_OPT_IN environment variable no longer changes HTTP attribute or metric emission — old (v1.7.0) and duplicate (http/http/dup) semconv outputs have been removed. #6819 @​maryliag
  • feat(instrumentation-fetch)!: emit only stable HTTP semantic conventions. The semconvStabilityOptIn instrumentation config option has been removed; old (v1.7.0) and duplicate semconv outputs are no longer emitted. #6819 @​maryliag
  • feat(instrumentation-xml-http-request)!: emit only stable HTTP semantic conventions. The semconvStabilityOptIn instrumentation config option has been removed; old (v1.7.0) and duplicate semconv outputs are no longer emitted. #6819 @​maryliag
  • feat(instrumentation-grpc)!: emit only stable network semantic conventions. The OTEL_SEMCONV_STABILITY_OPT_IN environment variable no longer changes attribute emission — net.peer.name and net.peer.port (old) are no longer set; only server.address and server.port (stable). #6819 @​maryliag

:rocket: Features

  • feat(sdk-logs): allow modifying ReadWriteLogRecord properties (including hrTime, hrTimeObserved, and spanContext) in accordance with the OpenTelemetry Logs specification #6923 @​Babul422
  • feat(sdk-node): emit a deprecation warning when the JaegerPropagator is selected via OTEL_PROPAGATORS or declarative config; use tracecontext instead. @​pichlermarc
  • feat(instrumentation-http): set error.type to status code in metrics for error requests. #6919 @​raphael-theriault-swi

experimental/v0.220.0

0.220.0

:boom: Breaking Changes

  • refactor(sdk-logs)!: refactor BatchLogRecordProcessor constructor signature #6817 @​trentm
    • (user-facing): BatchLogRecordProcessor now takes a single options object with all possible properties, instead of two separate arguments. For example, before new BatchLogRecordProcessor(exporter, { maxQueueSize: 1000 }), after new BatchLogRecordProcessor({ exporter, maxQueueSize: 1000 }).
    • interface BufferConfig -> interface BatchLogRecordProcessorOptions, and now includes the exporter property
    • interface BatchLogRecordProcessorBrowserConfig -> interface BatchLogRecordProcessorBrowserOptions
    • (user-facing): SimpleLogRecordProcessor now takes a single options object with all possible properties. For example, before new SimpleLogRecordProcessor(exporter), after new SimpleLogRecordProcessor({ exporter }). #6836
  • refactor(configuration)!: change config file parsing to not add default values, nor merge *_list fields #6765 @​trentm
  • docs(shim-opencensus): Notice: The @opentracing/shim-opencensus package will be removed in SDK 3.x, planned for approximately September 2026.
    • The OpenCensus and OpenTracing compatibility requirements in the OpenTelemetry specification have been deprecated.
  • chore(sdk-node)!: Drop support for deprecated OpenCensusMetricProducer from declarative config

:rocket: Features

  • feat(configuration): bump config schema to v1.1.0; rename without_scope_info → scope_info_enabled and without_target_info/development → target_info_enabled/development on the Prometheus pull exporter (semantics inverted), rename with_resource_constant_labels → resource_constant_labels. Validate file_format per the configuration versioning spec: accept any minor version of major 1 (e.g. 1.0, 1.1), warn when the minor version is newer than supported, and reject other major versions. #6781 @​MikeGoldsmith
  • feat(sdk-node): wire up id_generator from declarative config #6782 @​MikeGoldsmith
  • feat(sdk-node): wire up tracer_provider.sampler from declarative config (always_on, always_off, trace_id_ratio_based, parent_based); unrecognized variants warn and fall back to ParentBased(AlwaysOn) #6506 @​MikeGoldsmith
  • feat(propagator-env-carrier): empty name normalization #6827 @​pellared
  • feat(propagator-env-carrier): make EnvironmentGetter read the current process.env #6853 @​pellared

:bug: Bug Fixes

  • fix(sdk-logs): stop Logger.emit() doing work (record construction, metrics, processor onEmit) after the LoggerProvider has shut down #6826 @​anneheartrecord
  • fix(sdk-node): pass all config properties (endpoint, headers, timeout, TLS, compression, temporality preference, default histogram aggregation) to OTLP metric exporters in declarative config #6814 @​MikeGoldsmith
  • fix(sdk-logs): default BatchLogRecordProcessor scheduleDelayMillis is 1000 #6796 @​trentm
  • fix(configuration): percent-decode keys and values in resource.attributes_list per spec #6787 @​MikeGoldsmith
  • fix(configuration): default log_level to info in env-based config initialization for consistency with file-based config #6788 @​MikeGoldsmith

... (truncated)

Commits
  • 76fa6b5 chore: prepare next release (#6942)
  • 3787896 chore(deps): update dependency webpack-cli to v7.2.1 (#6934)
  • be5f757 fix(deps): update dependency body-parser to v2.3.0 [security] (#6941)
  • f6d8fbe chore(deps): lock file maintenance (#6559)
  • 9612732 chore: remove examples/dice from workspaces (#6937)
  • 7107906 chore: start using min-release-age in .npmrc, disable minimumReleaseAge for r...
  • a7e5d11 chore(deps): update dependency webpack to v5.108.4 (#6933)
  • af7a82d chore(deps): update dependency msw to v2.15.0 (#6831)
  • b9f57c6 chore(deps): update dependency @​types/webpack-env to v1.18.8 (#6877)
  • cbb4abf chore(deps): update ubuntu docker tag to v26 (#6635)
  • Additional commits viewable in compare view

Updates `@opentelemetry/exporter-trace-otlp-proto` from 0.219.0 to 0.221.0
Release notes

Sourced from @​opentelemetry/exporter-trace-otlp-proto's releases.

experimental/v0.221.0

0.221.0

:boom: Breaking Changes

  • feat(sdk-logs)!: configure the force flush timeout per call #6931 @​LarryHu0217
    • (user-facing): LoggerProviderOptions.forceFlushTimeoutMillis has been removed; pass timeoutMillis to LoggerProvider.forceFlush() instead.
  • feat(instrumentation-http)!: emit only stable HTTP semantic conventions. The OTEL_SEMCONV_STABILITY_OPT_IN environment variable no longer changes HTTP attribute or metric emission — old (v1.7.0) and duplicate (http/http/dup) semconv outputs have been removed. #6819 @​maryliag
  • feat(instrumentation-fetch)!: emit only stable HTTP semantic conventions. The semconvStabilityOptIn instrumentation config option has been removed; old (v1.7.0) and duplicate semconv outputs are no longer emitted. #6819 @​maryliag
  • feat(instrumentation-xml-http-request)!: emit only stable HTTP semantic conventions. The semconvStabilityOptIn instrumentation config option has been removed; old (v1.7.0) and duplicate semconv outputs are no longer emitted. #6819 @​maryliag
  • feat(instrumentation-grpc)!: emit only stable network semantic conventions. The OTEL_SEMCONV_STABILITY_OPT_IN environment variable no longer changes attribute emission — net.peer.name and net.peer.port (old) are no longer set; only server.address and server.port (stable). #6819 @​maryliag

:rocket: Features

  • feat(sdk-logs): allow modifying ReadWriteLogRecord properties (including hrTime, hrTimeObserved, and spanContext) in accordance with the OpenTelemetry Logs specification #6923 @​Babul422
  • feat(sdk-node): emit a deprecation warning when the JaegerPropagator is selected via OTEL_PROPAGATORS or declarative config; use tracecontext instead. @​pichlermarc
  • feat(instrumentation-http): set error.type to status code in metrics for error requests. #6919 @​raphael-theriault-swi

experimental/v0.220.0

0.220.0

:boom: Breaking Changes

  • refactor(sdk-logs)!: refactor BatchLogRecordProcessor constructor signature #6817 @​trentm
    • (user-facing): BatchLogRecordProcessor now takes a single options object with all possible properties, instead of two separate arguments. For example, before new BatchLogRecordProcessor(exporter, { maxQueueSize: 1000 }), after new BatchLogRecordProcessor({ exporter, maxQueueSize: 1000 }).
    • interface BufferConfig -> interface BatchLogRecordProcessorOptions, and now includes the exporter property
    • interface BatchLogRecordProcessorBrowserConfig -> interface BatchLogRecordProcessorBrowserOptions
    • (user-facing): SimpleLogRecordProcessor now takes a single options object with all possible properties. For example, before new SimpleLogRecordProcessor(exporter), after new SimpleLogRecordProcessor({ exporter }). #6836
  • refactor(configuration)!: change config file parsing to not add default values, nor merge *_list fields #6765 @​trentm
  • docs(shim-opencensus): Notice: The @opentracing/shim-opencensus package will be removed in SDK 3.x, planned for approximately September 2026.
    • The OpenCensus and OpenTracing compatibility requirements in the OpenTelemetry specification have been deprecated.
  • chore(sdk-node)!: Drop support for deprecated OpenCensusMetricProducer from declarative config

:rocket: Features

  • feat(configuration): bump config schema to v1.1.0; rename without_scope_info → scope_info_enabled and without_target_info/development → target_info_enabled/development on the Prometheus pull exporter (semantics inverted), rename with_resource_constant_labels → resource_constant_labels. Validate file_format per the configuration versioning spec: accept any minor version of major 1 (e.g. 1.0, 1.1), warn when the minor version is newer than supported, and reject other major versions. #6781 @​MikeGoldsmith
  • feat(sdk-node): wire up id_generator from declarative config #6782 @​MikeGoldsmith
  • feat(sdk-node): wire up tracer_provider.sampler from declarative config (always_on, always_off, trace_id_ratio_based, parent_based); unrecognized variants warn and fall back to ParentBased(AlwaysOn) #6506 @​MikeGoldsmith
  • feat(propagator-env-carrier): empty name normalization #6827 @​pellared
  • feat(propagator-env-carrier): make EnvironmentGetter read the current process.env #6853 @​pellared

:bug: Bug Fixes

  • fix(sdk-logs): stop Logger.emit() doing work (record construction, metrics, processor onEmit) after the LoggerProvider has shut down #6826 @​anneheartrecord
  • fix(sdk-node): pass all config properties (endpoint, headers, timeout, TLS, compression, temporality preference, default histogram aggregation) to OTLP metric exporters in declarative config #6814 @​MikeGoldsmith
  • fix(sdk-logs): default BatchLogRecordProcessor scheduleDelayMillis is 1000 #6796 @​trentm
  • fix(configuration): percent-decode keys and values in resource.attributes_list per spec #6787 @​MikeGoldsmith
  • fix(configuration): default log_level to info in env-based config initialization for consistency with file-based config #6788 @​MikeGoldsmith

... (truncated)

Commits
  • 76fa6b5 chore: prepare next release (#6942)
  • 3787896 chore(deps): update dependency webpack-cli to v7.2.1 (#6934)
  • be5f757 fix(deps): update dependency body-parser to v2.3.0 [security] (#6941)
  • f6d8fbe chore(deps): lock file maintenance (#6559)
  • 9612732 chore: remove examples/dice from workspaces (#6937)
  • 7107906 chore: start using min-release-age in .npmrc, disable minimumReleaseAge for r...
  • a7e5d11 chore(deps): update dependency webpack to v5.108.4 (#6933)
  • af7a82d chore(deps): update dependency msw to v2.15.0 (#6831)
  • b9f57c6 chore(deps): update dependency @​types/webpack-env to v1.18.8 (#6877)
  • cbb4abf chore(deps): update ubuntu docker tag to v26 (#6635)
  • Additional commits viewable in compare view

Updates `@opentelemetry/instrumentation-host-metrics` from 0.2.0 to 0.4.0
Release notes

Sourced from @​opentelemetry/instrumentation-host-metrics's releases.

instrumentation-host-metrics: v0.4.0

0.4.0 (2026-07-23)

Features

  • deps: update deps matching '@opentelemetry/*' (#3629) (466d5de)

instrumentation-console: v0.3.0

0.3.0 (2026-07-23)

Features

  • deps: update deps matching '@opentelemetry/*' (#3629) (466d5de)

sampler-aws-xray: v0.3.0

0.3.0 (2026-07-23)

⚠ BREAKING CHANGES

  • only emit stable http, network and database attributes (#3585)

Features

  • only emit stable http, network and database attributes (#3585) (5b7dd0e)

instrumentation-host-metrics: v0.3.0

0.3.0 (2026-07-03)

Features

  • deps: update deps matching '@opentelemetry/*' (#3593) (6dfb532)
Changelog

Sourced from @​opentelemetry/instrumentation-host-metrics's changelog.

0.4.0 (2026-07-23)

Features

  • deps: update deps matching '@opentelemetry/*' (#3629) (466d5de)

0.3.0 (2026-07-03)

Features

  • deps: update deps matching '@opentelemetry/*' (#3593) (6dfb532)
Commits
Maintainer changes

This version was pushed to npm by GitHub Actions, a new releaser for @​opentelemetry/instrumentation-host-metrics since your current version.


Updates `@opentelemetry/instrumentation-runtime-node` from 0.32.0 to 0.34.0
Release notes

Sourced from @​opentelemetry/instrumentation-runtime-node's releases.

instrumentation-runtime-node: v0.34.0

0.34.0 (2026-07-23)

⚠ BREAKING CHANGES

  • instrumentation-runtime-node: remove deprecated v8js.memory.heap.limit metric (#3632)

Features

  • deps: update deps matching '@opentelemetry/*' (#3629) (466d5de)
  • instrumentation-runtime-node: remove deprecated v8js.memory.heap.limit metric (#3632) (f698292)

instrumentation-runtime-node: v0.33.0

0.33.0 (2026-07-03)

Features

  • deps: update deps matching '@opentelemetry/*' (#3593) (6dfb532)
  • instrumentation-runtime-node: add active resource gauge (#3361) (f3d38e2)
Changelog

Sourced from @​opentelemetry/instrumentation-runtime-node's changelog.

0.34.0 (2026-07-23)

⚠ BREAKING CHANGES

  • instrumentation-runtime-node: remove deprecated v8js.memory.heap.limit metric (#3632)

Features

  • deps: update deps matching '@opentelemetry/*' (#3629) (466d5de)
  • instrumentation-runtime-node: remove deprecated v8js.memory.heap.limit metric (#3632) (f698292)

0.33.0 (2026-07-03)

Features

  • deps: update deps matching '@opentelemetry/*' (#3593) (6dfb532)
  • instrumentation-runtime-node: add active resource gauge (#3361) (f3d38e2)
Commits

Updates `@sap/cds-mtxs` from 4.0.1 to 4.0.2 Updates `axios` f... _Description has been truncated_ Co-authored-by: Sebastian Van Syckel --- package-lock.json | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 620f8da8..7bef89b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,6 @@ "": { "name": "@cap-js/telemetry", "version": "2.1.0", - "dev": true, "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.9", @@ -45,9 +44,9 @@ } }, "node_modules/@cap-js/cds-test": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@cap-js/cds-test/-/cds-test-1.0.1.tgz", - "integrity": "sha512-hOD4ECvHA8mVXCnsAcfWe+CB4XxLHYZskGUiD49BFEblbZVHae32xWZLfPfaawnZzAUPEJ+ugakyltgbxdKdKA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@cap-js/cds-test/-/cds-test-1.0.2.tgz", + "integrity": "sha512-autSQg1l7nvHmBwxan0inQ71Cw3cqQdBvON/RFarhbwRJSeC6oC7nQ6d1RiVxCYYkeM0wqkmfn8WGQ9K89dWXQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1569,9 +1568,9 @@ } }, "node_modules/@sap/cds-mtxs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sap/cds-mtxs/-/cds-mtxs-4.0.1.tgz", - "integrity": "sha512-H+UygJY57D3be40X4TrgtLBVZCph9G8LAn+5VWNdL7lEWFMCqZM0bNj1AogmJXEUtlWOUWK98aznlsErY7UVaQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@sap/cds-mtxs/-/cds-mtxs-4.0.2.tgz", + "integrity": "sha512-C8JzlIGPxh/p8vGGyQMOsSJPWGJlOtrakaoCOb+mxDXbuLPDNccbRlrqon0a9aqgqyf4iQCrTuRNMJpd05blTg==", "dev": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { @@ -2621,9 +2620,9 @@ } }, "node_modules/eslint": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", - "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.9.1.tgz", + "integrity": "sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==", "devOptional": true, "license": "MIT", "workspaces": [ From 99a6e2da2ca1d2c503e0918c07b7236637a64219 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:22:03 +0200 Subject: [PATCH 19/31] chore: add CODEOWNERS to develop (#497) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Add CODEOWNERS File to Repository ### Chore 🔧 Added a `CODEOWNERS` file to the repository to define default code ownership for all files. ### Changes * `.github/CODEOWNERS`: New file that assigns `@cap-js/node-js-runtime` as the default owner for all files in the repository (`*`). This ensures the team is automatically requested for review on any pull request affecting the codebase. - [ ] 🔄 Regenerate and Update Summary
PR Bot Information **Version:** `1.31.2` - 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 - Event Trigger: `pull_request.opened` - LLM: `anthropic--claude-4.6-sonnet` - Correlation ID: `d792fa50-a570-11f1-88af-cb6b0d808e90`
--- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..b2a1648b --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @cap-js/node-js-runtime From 48c9934ac869b8637f3c87092a13a822ce1b52d9 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Tue, 1 Sep 2026 11:27:08 +0200 Subject: [PATCH 20/31] feat: configurable span processor via telemetry.tracing.processor (#498) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #411. ### Problem The span processor was hard-coded off `NODE_ENV`: `BatchSpanProcessor` in production, `SimpleSpanProcessor` otherwise. There was no way to override it (e.g. force `SimpleSpanProcessor` in a prod-like env for immediate export, or tune batching `config`). ### Change Select the processor from config: `cds.requires.telemetry.tracing.processor = { kind, config? }`. - `kind` ∈ { `BatchSpanProcessor` (default), `SimpleSpanProcessor` } - `config` is passed through to the processor's constructor - The `[development]` profile defaults to `SimpleSpanProcessor` — so the previous `NODE_ENV`-based behavior is preserved (production → Batch, dev → Simple), just now overridable. - An unknown `kind` throws `Unknown span processor `, mirroring the existing `sampler`/`propagator` kind-validation in `lib/tracing/index.js`. ### Tests - `test/tracing-processor-simple.test.js` — dev profile boot activates `SimpleSpanProcessor`. - `test/tracing-processor-batch.test.js` — explicit `[tracing-batch-processor]` override activates `BatchSpanProcessor` (a true `production` boot isn't feasible in the test app — it requires `@cap-js/hana`; the production default is covered via this explicit override). - `test/tracing-processor-unknown.test.js` — an unknown kind throws via the real factory (verified non-vacuous: removing the guard makes it fail). ### Docs - README: new **Span Processor** config section. - CHANGELOG: `Added` bullet under 2.1.0. --- CHANGELOG.md | 1 + README.md | 16 ++++++++++++ lib/tracing/index.js | 9 +++---- package.json | 12 ++++++--- test/bookshop/.cdsrc.json | 11 +++++++++ test/tracing-processor-batch.test.js | 31 +++++++++++++++++++++++ test/tracing-processor-simple.test.js | 34 ++++++++++++++++++++++++++ test/tracing-processor-unknown.test.js | 25 +++++++++++++++++++ 8 files changed, 131 insertions(+), 8 deletions(-) create mode 100644 test/tracing-processor-batch.test.js create mode 100644 test/tracing-processor-simple.test.js create mode 100644 test/tracing-processor-unknown.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index ed9bec78..0ebb0fa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Added - Queue worker transactions are traced as coherent ` - tx` spans under the `cds.spawn - run task` root, instead of orphaned per-call spans +- The span processor is now configurable via `cds.requires.telemetry.tracing.processor = { kind, config? }` (`BatchSpanProcessor` or `SimpleSpanProcessor`); defaults to `BatchSpanProcessor`, and to `SimpleSpanProcessor` in the `[development]` profile ### Changed diff --git a/README.md b/README.md index 6843b802..f04e7152 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Documentation can be found at [cap.cloud.sap](https://cap.cloud.sap/docs) and [o - [Instrumentations](#instrumentations) - [Sampler](#sampler) - [Propagators](#propagators) + - [Span Processor](#span-processor) - [Exporters](#exporters) - [High Resolution Timestamps (beta)](#high-resolution-timestamps-beta) - [Environment Variables](#environment-variables) @@ -428,6 +429,21 @@ Default: ``` +### Span Processor + +Configure via `cds.requires.telemetry.tracing.processor = { kind, config? }`, where `kind` is one of `BatchSpanProcessor` or `SimpleSpanProcessor` and `config` is passed through to the processor's constructor. + +Default: `BatchSpanProcessor`, except in the `[development]` profile, which uses `SimpleSpanProcessor` so spans are exported immediately. +```json +{ + "kind": "BatchSpanProcessor" +} +``` + +> [!NOTE] +> The `ConsoleSpanExporter` (the default `telemetry-to-console` exporter) has only been tested with the `SimpleSpanProcessor`. Combining it with the `BatchSpanProcessor` is not recommended. + + ### Exporters Configure via: diff --git a/lib/tracing/index.js b/lib/tracing/index.js index bbdef34e..957ed6d3 100644 --- a/lib/tracing/index.js +++ b/lib/tracing/index.js @@ -166,11 +166,10 @@ module.exports = resource => { LOG._info && LOG.info('Dynatrace OneAgent detected, disabling trace exporter') } else { const exporter = _getExporter() - const processorConfig = cds.env.requires.telemetry.tracing.processor?.config || {} - processor = - process.env.NODE_ENV === 'production' - ? new BatchSpanProcessor(exporter, processorConfig) - : new SimpleSpanProcessor(exporter, processorConfig) + const { kind: processorKind, config: processorConfig = {} } = cds.env.requires.telemetry.tracing.processor || {} + const processors = { BatchSpanProcessor, SimpleSpanProcessor } + if (!processors[processorKind]) throw new Error(`Unknown span processor ${processorKind}`) + processor = new processors[processorKind](exporter, processorConfig) } /* diff --git a/package.json b/package.json index b3128b8f..11f1693e 100644 --- a/package.json +++ b/package.json @@ -81,11 +81,17 @@ "W3CTraceContextPropagator", "W3CBaggagePropagator" ], - "[development]": { - "hrtime": true + "processor": { + "kind": "BatchSpanProcessor" }, "_tx": false, - "_hana_prom": true + "_hana_prom": true, + "[development]": { + "hrtime": true, + "processor": { + "kind": "SimpleSpanProcessor" + } + } }, "metrics": { "config": { diff --git a/test/bookshop/.cdsrc.json b/test/bookshop/.cdsrc.json index 4b9e60df..c467bbf8 100644 --- a/test/bookshop/.cdsrc.json +++ b/test/bookshop/.cdsrc.json @@ -144,5 +144,16 @@ "outboxed": false } } + }, + "[tracing-batch-processor]": { + "requires": { + "telemetry": { + "tracing": { + "processor": { + "kind": "BatchSpanProcessor" + } + } + } + } } } diff --git a/test/tracing-processor-batch.test.js b/test/tracing-processor-batch.test.js new file mode 100644 index 00000000..7fe4d779 --- /dev/null +++ b/test/tracing-processor-batch.test.js @@ -0,0 +1,31 @@ +// Tests that an explicit processor override activates BatchSpanProcessor. +// +// The `[tracing-batch-processor]` profile in test/bookshop/.cdsrc.json explicitly +// sets `telemetry.tracing.processor.kind = "BatchSpanProcessor"`, overriding the +// `[development]` profile default of SimpleSpanProcessor. This proves the explicit- +// override path: a single config knob flips the active processor in a dev-profile run. +// +// NOTE: a "true production profile" boot (NODE_ENV=production or --profile production) +// is not feasible in the test bookshop — the production profile requires @cap-js/hana +// which is not installed. The production default (BatchSpanProcessor, defined in +// package.json's base tracing config) is therefore covered via this explicit override +// rather than a genuine production boot. +const cds = require('@sap/cds') +const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory, tracing-batch-processor') +const otel = require('@opentelemetry/api') + +// Same accessor as in tracing-processor-simple.test.js — see that file for rationale. +function activeProcessorName() { + const provider = otel.trace.getTracerProvider() + const delegate = provider.getDelegate?.() ?? provider + return delegate._activeSpanProcessor?._spanProcessors?.[0]?.constructor?.name +} + +describe('span processor selection — explicit BatchSpanProcessor override', () => { + test('tracing-batch-processor profile activates BatchSpanProcessor', () => { + // The [tracing-batch-processor] profile overrides the [development] default + // (SimpleSpanProcessor) to BatchSpanProcessor — the same processor production uses, + // proven here via explicit override in a dev-profile run. + expect(activeProcessorName()).toBe('BatchSpanProcessor') + }) +}) diff --git a/test/tracing-processor-simple.test.js b/test/tracing-processor-simple.test.js new file mode 100644 index 00000000..1b523828 --- /dev/null +++ b/test/tracing-processor-simple.test.js @@ -0,0 +1,34 @@ +// Tests that the development profile selects SimpleSpanProcessor. +// +// Per package.json `cds.requires.kinds.telemetry.tracing`, the package-level default +// (production) processor is BatchSpanProcessor; the `[development]` profile in +// package.json overrides it to SimpleSpanProcessor. In the test environment +// (NODE_ENV != production) the `[development]` profile applies automatically, so a +// plain boot should yield SimpleSpanProcessor without any additional override. +const cds = require('@sap/cds') +const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') +const otel = require('@opentelemetry/api') + +// Reach the active span processor installed by the tracing factory. +// +// The global OTel provider is a ProxyTracerProvider (returned by trace.getTracerProvider()); +// getDelegate() unwraps it to the real NodeTracerProvider built by lib/tracing/index.js. +// NodeTracerProvider wraps all registered processors in a private MultiSpanProcessor +// accessible as _activeSpanProcessor. We drill one level deeper to _spanProcessors[0] +// (the single processor the factory installed) to read its constructor name. +// _spanProcessors is a private `_`-prefixed field, but it is the only stable path to the +// live processor instance; `flushSpans()` in test/utils.js follows the same +// getDelegate() + forceFlush() pattern for the same reason. +function activeProcessorName() { + const provider = otel.trace.getTracerProvider() + const delegate = provider.getDelegate?.() ?? provider + return delegate._activeSpanProcessor?._spanProcessors?.[0]?.constructor?.name +} + +describe('span processor selection — development profile', () => { + test('development profile activates SimpleSpanProcessor', () => { + // [development] is auto-applied in non-production (NODE_ENV=test) environments. + // The plugin should have wired a SimpleSpanProcessor (not the batch default). + expect(activeProcessorName()).toBe('SimpleSpanProcessor') + }) +}) diff --git a/test/tracing-processor-unknown.test.js b/test/tracing-processor-unknown.test.js new file mode 100644 index 00000000..5033a712 --- /dev/null +++ b/test/tracing-processor-unknown.test.js @@ -0,0 +1,25 @@ +// Tests that an unknown processor kind causes the tracing factory to throw. +// +// Boot the bookshop server so cds.env is fully configured with the tracing-in-memory +// profile, then call the real tracing factory (lib/tracing/index.js) with a bogus +// processor kind. The factory throws before creating the tracerProvider or calling +// .register(), so passing {} as the resource arg is safe and causes no double-register. +// require('./cds')() and require('./cloud_sdk')() run first, but both use the wrap() +// utility which guards against double-wrapping via the __wrapped flag — idempotent. +const cds = require('@sap/cds') +const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') + +const tracing = require('../lib/tracing') + +describe('span processor selection — unknown kind', () => { + test('unknown processor kind throws with a descriptive error', () => { + const cfg = cds.env.requires.telemetry.tracing + const orig = cfg.processor + try { + cfg.processor = { kind: 'NotARealProcessor' } + expect(() => tracing({})).toThrow(/Unknown span processor NotARealProcessor/) + } finally { + cfg.processor = orig + } + }) +}) From abbed34990e4e0dd8c243ad8a878b09c529a0ede Mon Sep 17 00:00:00 2001 From: Sebastian Van Syckel Date: Mon, 7 Sep 2026 22:00:30 +0200 Subject: [PATCH 21/31] test: gate sqlite queue-worker tracing skips on @sap/cds >= 10.1 (cds.spawn fix) --- TESTING.md | 2 +- test/tracing-messaging-inboxed.test.js | 9 ++++++--- test/tracing-messaging-persistent-outbox.test.js | 9 ++++++--- test/tracing-outboxed-batch.test.js | 8 +++++--- test/tracing-scheduled.test.js | 8 +++++--- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/TESTING.md b/TESTING.md index 07c99264..5678ede2 100644 --- a/TESTING.md +++ b/TESTING.md @@ -121,7 +121,7 @@ This is the inverse pairing: passport is sqlite-skip / HANA-run; multitenancy is Other skips are **debt tracked in #477**, not sanctioned exceptions: -- **§1 — queue-worker tracing on sqlite:** `tracing-scheduled`, `tracing-outboxed-batch`, `tracing-messaging-inboxed`, `tracing-messaging-persistent-outbox` skip their worker-span cases on sqlite. Published `@sap/cds` uses a raw `setTimeout` bypass (not `cds.spawn`) for the sqlite queue worker to avoid a single-writer deadlock, so the `cds.spawn - run task` root span never appears. Gated on a cds queue-spawn fix landing; remove with a follow-up. +- **§1 — queue-worker tracing on sqlite:** `tracing-scheduled`, `tracing-outboxed-batch`, `tracing-messaging-inboxed`, `tracing-messaging-persistent-outbox` skip their worker-span cases on sqlite **when `@sap/cds` < 10.1** (gated inline via `cds.version`). cds 10.1 routes the sqlite queue worker through `cds.spawn` (the queue-spawn fix), so the `cds.spawn - run task` root span appears and the tests run; on earlier cds it's absent. Both CI matrix legs are currently below that bar — the cds-9 leg force-installs `@sap/cds@9`, and the cds-10 leg resolves `@sap/cds` 10.0.x transitively from the lockfile — so the cases stay **dormant (skipped) on CI** until the transitive `@sap/cds` reaches 10.1. HANA always runs regardless of version. - **§3 — unimplemented stubs:** placeholder `test.skip` cases in `tracing.test.js` and `tracing-mt.test.js` (individual handlers, remote, `$batch`, `srv.emit`, `cds.spawn` under multitenancy) — real coverage gaps to be written. ## Known caveats & gotchas diff --git a/test/tracing-messaging-inboxed.test.js b/test/tracing-messaging-inboxed.test.js index b565e21f..be906ae5 100644 --- a/test/tracing-messaging-inboxed.test.js +++ b/test/tracing-messaging-inboxed.test.js @@ -1,5 +1,6 @@ const CASE = 'inboxed' +const cds = require('@sap/cds') const otel = require('@opentelemetry/api') // `inboxed: true` combined with the default outboxed messaging behavior means TWO queue @@ -55,10 +56,12 @@ const CHECK = ({ expect, rootSpans, groupedByTrace }) => { } describe(`tracing messaging - ${CASE}`, () => { - // Queue-worker tracing needs cds.spawn on sqlite — skipped here, tracked in #477 §1. + // Queue-worker tracing needs the cds.spawn fix (@sap/cds >= 10.1) on sqlite; HANA always runs. Tracked in #477 §1. + // Dormant on CI until the transitive @sap/cds reaches 10.1 (the cds-10 leg resolves 10.0.x today); cds-9 always skips. // See TESTING.md → Sanctioned skips (and HANA signalling: why we branch on TELEMETRY_TEST_HANA, not cds.env). - if (!process.env.TELEMETRY_TEST_HANA) { - test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) + const [maj, min] = cds.version.split('.').map(Number) + if (!process.env.TELEMETRY_TEST_HANA && (maj < 10 || (maj === 10 && min < 1))) { + test.skip('queue-worker tracing needs @sap/cds >= 10.1 (cds.spawn fix) on sqlite', () => {}) return } require('./tracing-messaging')(CASE, CHECK) diff --git a/test/tracing-messaging-persistent-outbox.test.js b/test/tracing-messaging-persistent-outbox.test.js index 2cdcb212..e46e5a1f 100644 --- a/test/tracing-messaging-persistent-outbox.test.js +++ b/test/tracing-messaging-persistent-outbox.test.js @@ -1,5 +1,6 @@ const CASE = 'persistent-outbox' +const cds = require('@sap/cds') const otel = require('@opentelemetry/api') // Messaging config (kind/file) comes from the `persistent-outbox` profile in @@ -99,10 +100,12 @@ const CHECK = ({ expect, rootSpans, groupedByTrace }) => { } describe(`tracing messaging - ${CASE}`, () => { - // Queue-worker tracing needs cds.spawn on sqlite — skipped here, tracked in #477 §1. + // Queue-worker tracing needs the cds.spawn fix (@sap/cds >= 10.1) on sqlite; HANA always runs. Tracked in #477 §1. + // Dormant on CI until the transitive @sap/cds reaches 10.1 (the cds-10 leg resolves 10.0.x today); cds-9 always skips. // See TESTING.md → Sanctioned skips (and HANA signalling: why we branch on TELEMETRY_TEST_HANA, not cds.env). - if (!process.env.TELEMETRY_TEST_HANA) { - test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) + const [maj, min] = cds.version.split('.').map(Number) + if (!process.env.TELEMETRY_TEST_HANA && (maj < 10 || (maj === 10 && min < 1))) { + test.skip('queue-worker tracing needs @sap/cds >= 10.1 (cds.spawn fix) on sqlite', () => {}) return } require('./tracing-messaging')(CASE, CHECK) diff --git a/test/tracing-outboxed-batch.test.js b/test/tracing-outboxed-batch.test.js index 5a8405f3..d47ba678 100644 --- a/test/tracing-outboxed-batch.test.js +++ b/test/tracing-outboxed-batch.test.js @@ -9,10 +9,12 @@ const { hrTimeToNanoseconds } = require('@opentelemetry/core') const { eventually } = require('./utils') describe('tracing for outboxed batch (chunk-size fan-out)', () => { - // Queue-worker tracing needs cds.spawn on sqlite — skipped here, tracked in #477 §1. + // Queue-worker tracing needs the cds.spawn fix (@sap/cds >= 10.1) on sqlite; HANA always runs. Tracked in #477 §1. + // Dormant on CI until the transitive @sap/cds reaches 10.1 (the cds-10 leg resolves 10.0.x today); cds-9 always skips. // See TESTING.md → Sanctioned skips (and HANA signalling: why we branch on TELEMETRY_TEST_HANA, not cds.env). - if (!process.env.TELEMETRY_TEST_HANA) { - test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) + const [maj, min] = cds.version.split('.').map(Number) + if (!process.env.TELEMETRY_TEST_HANA && (maj < 10 || (maj === 10 && min < 1))) { + test.skip('queue-worker tracing needs @sap/cds >= 10.1 (cds.spawn fix) on sqlite', () => {}) return } diff --git a/test/tracing-scheduled.test.js b/test/tracing-scheduled.test.js index df4c0c34..14772b69 100644 --- a/test/tracing-scheduled.test.js +++ b/test/tracing-scheduled.test.js @@ -26,10 +26,12 @@ const otel = require('@opentelemetry/api') const { asExternalClient, eventually } = require('./utils') describe('tracing for scheduled tasks', () => { - // Queue-worker tracing needs cds.spawn on sqlite — skipped here, tracked in #477 §1. + // Queue-worker tracing needs the cds.spawn fix (@sap/cds >= 10.1) on sqlite; HANA always runs. Tracked in #477 §1. + // Dormant on CI until the transitive @sap/cds reaches 10.1 (the cds-10 leg resolves 10.0.x today); cds-9 always skips. // See TESTING.md → Sanctioned skips (and HANA signalling: why we branch on TELEMETRY_TEST_HANA, not cds.env). - if (!process.env.TELEMETRY_TEST_HANA) { - test.skip('queue-worker tracing needs cds.spawn on sqlite (pending cds fix)', () => {}) + const [maj, min] = cds.version.split('.').map(Number) + if (!process.env.TELEMETRY_TEST_HANA && (maj < 10 || (maj === 10 && min < 1))) { + test.skip('queue-worker tracing needs @sap/cds >= 10.1 (cds.spawn fix) on sqlite', () => {}) return } From 429dd1827c936781075a6b9c5354712e844125df Mon Sep 17 00:00:00 2001 From: Vitaly Kozyura <58591662+vkozyura@users.noreply.github.com> Date: Wed, 16 Sep 2026 23:29:03 +0200 Subject: [PATCH 22/31] Add telemetry-to-caas kind for CaaS (Collector as a Service) support (#436) - Add telemetry-to-caas kind definition in package.json - Add getCredsForCaaS() to extract credentials from caas-service binding - Add augmentCaaSCreds() to configure OTLP endpoint URL - Handle CaaS in tracing and metrics exporters Note: CaaS requires mTLS authentication with SAP-signed certificates. The certificate must be obtained separately via BTP Certificate Service. --------- Co-authored-by: sjvans <30337871+sjvans@users.noreply.github.com> Co-authored-by: Sebastian Van Syckel --- CHANGELOG.md | 1 + README.md | 106 ++++++++- lib/logging/index.js | 15 +- lib/metrics/index.js | 23 +- lib/tracing/index.js | 14 +- lib/utils.js | 114 +++++++++- lib/zti.js | 207 +++++++++++++++++ package.json | 19 ++ test/buffer.test.js | 149 +++++++++++++ test/caas.test.js | 519 +++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 1159 insertions(+), 8 deletions(-) create mode 100644 lib/zti.js create mode 100644 test/buffer.test.js create mode 100644 test/caas.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ebb0fa6..35b90746 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Added +- Support for `telemetry-to-caas` kind for CaaS (Collector as a Service). By default, mTLS certificates are provisioned and rotated automatically via the Zero Trust Identity (ZTI) sidecar; alternatively, mTLS certificates can be provided manually via `x509` credentials. - Queue worker transactions are traced as coherent ` - tx` spans under the `cds.spawn - run task` root, instead of orphaned per-call spans - The span processor is now configurable via `cds.requires.telemetry.tracing.processor = { kind, config? }` (`BatchSpanProcessor` or `SimpleSpanProcessor`); defaults to `BatchSpanProcessor`, and to `SimpleSpanProcessor` in the `[development]` profile diff --git a/README.md b/README.md index f04e7152..5b0da11b 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Documentation can be found at [cap.cloud.sap](https://cap.cloud.sap/docs) and [o - [`telemetry-to-console`](#telemetry-to-console) - [`telemetry-to-dynatrace`](#telemetry-to-dynatrace) - [`telemetry-to-cloud-logging`](#telemetry-to-cloud-logging) + - [`telemetry-to-caas`](#telemetry-to-caas) - [`telemetry-to-jaeger`](#telemetry-to-jaeger) - [`telemetry-to-otlp`](#telemetry-to-otlp) - [Detailed Configuration Options](#detailed-configuration-options) @@ -201,7 +202,7 @@ Please note that in order for logs to be exported via OpenTelemetry, `cds.log()` ## Predefined Kinds -There are five predefined kinds as follows: +There are six predefined kinds as follows: ### `telemetry-to-console` @@ -284,12 +285,113 @@ In order to receive OpenTelemetry credentials in the binding to the SAP Cloud Lo If you are binding your app to SAP Cloud Logging via a [user-provided service instance](https://docs.cloudfoundry.org/devguide/services/user-provided.html), make sure that it has the tag `Cloud Logging`. -> Tip: To add the required tag to an existing user-provided service, you can use: +> Tip: To add the required tag to an existing user-provided service, you can use: > ``` > cf update-user-provided-service {service-name} -t "Cloud Logging" > ``` > For detailed information about binding resolution in CAP, consult [`cds.connect()` → Service Bindings](https://cap.cloud.sap/docs/node.js/cds-connect#service-bindings). + +### `telemetry-to-caas` + +Exports traces and metrics to CaaS (Collector as a Service). +CaaS acts as a managed OpenTelemetry Collector that can route telemetry data to downstream backends like SAP Cloud Logging. + +Use via `cds.requires.telemetry.kind = 'to-caas'`. + +Required additional dependencies: +- `@opentelemetry/exporter-trace-otlp-proto` +- `@opentelemetry/exporter-metrics-otlp-proto` + +CaaS needs two things: a **CaaS collector instance** (created with its pipeline config) and an **app deployment** that authenticates to it over mTLS — automatically via ZTI (default), or with a manually provided certificate. + +#### Create the CaaS instance + +The collector's `otelConfig` (receivers, exporters, pipelines) is fixed at creation — a binding can't change it later — so create and configure the instance up front, via the BTP cockpit or the CF CLI: + +```bash +cf create-service caas my-app-caas -c '{ + "otelConfig": { + "receivers": { "otlp": { "protocols": { "grpc": {}, "http": {} } } }, + "exporters": { "otlp/sink": { "endpoint": "" } }, + "service": { "pipelines": { + "traces": { "receivers": ["otlp"], "exporters": ["otlp/sink"] }, + "metrics": { "receivers": ["otlp"], "exporters": ["otlp/sink"] } + } } + } +}' +``` + +The `receivers` block is required. See the CaaS onboarding docs for the full `otelConfig` / `secrets` / PII-redaction options. + +#### Automatic certificates via ZTI (default) + +`cds add mta` generates the base `mta.yaml`. Add the ZTI sidecar buildpack to your service module, bind the CaaS instance (as an `existing-service`, since it's pre-created) and a `zero-trust-identity` service: + +```yaml +modules: + - name: my-app-srv + type: nodejs + path: gen/srv + parameters: + buildpacks: + - zero_trust_sidecar_buildpack + - nodejs_buildpack + requires: + - name: my-app-caas + parameters: + config: + subject: + issuer: + - name: my-app-zti + parameters: + config: + app-identifier: my-app + svid-store: + file: + name: my-app-svid +resources: + - name: my-app-caas # the instance created above + type: org.cloudfoundry.existing-service + - name: my-app-zti + type: org.cloudfoundry.managed-service + parameters: + service: zero-trust-identity + service-plan: standard +``` + +##### Certificate identity (current workaround) + +`subject`/`issuer` identify the client certificate. Until MTA tooling supports service-key placeholders, extract them before deployment from a throwaway `config-policy` key: + +```bash +cf create-service zero-trust-identity config-policy tmp-cp +cf create-service-key tmp-cp k -c '{ "app-identifier": "my-app", "type": "subject_dn" }' +cf service-key tmp-cp k # copy subject/issuer into the CaaS binding above +cf delete-service-key tmp-cp k -f && cf delete-service tmp-cp -f +``` + +#### Manual mTLS certificates (alternative) + +For environments without ZTI, provide an SAP-signed certificate yourself instead of the `zero-trust-identity` binding: + +```yaml +modules: + - name: my-app-srv + requires: + - name: my-app-caas + parameters: + config: + subject: + issuer: + properties: + CDS_REQUIRES_TELEMETRY_X509_CERT: + CDS_REQUIRES_TELEMETRY_X509_KEY: +``` + +Extract `subject`/`issuer` with `openssl x509 -in cert.pem -noout -subject -issuer -nameopt RFC2253` (use the direct issuer, not the root CA). + + ### `telemetry-to-jaeger` Exports traces to Jaeger. diff --git a/lib/logging/index.js b/lib/logging/index.js index a1fe08e0..924b3c6f 100644 --- a/lib/logging/index.js +++ b/lib/logging/index.js @@ -3,7 +3,7 @@ const LOG = cds.log('telemetry') const { getStringFromEnv } = require('@opentelemetry/core') -const { getCredsForCLSAsUPS, augmentCLCreds, _require } = require('../utils') +const { getCredsForCLSAsUPS, augmentCLCreds, createCaaSExporter, _require } = require('../utils') const _protocol2module = { grpc: '@opentelemetry/exporter-logs-otlp-grpc', @@ -30,6 +30,7 @@ function _getExporter() { logging: { exporter: loggingExporter }, credentials } = cds.env.requires.telemetry + let exporter // for kind telemetry-to-otlp based on env vars if (loggingExporter === 'env') { @@ -62,7 +63,17 @@ function _getExporter() { config.credentials ??= credentials.credentials } - const exporter = new loggingExporterModule[loggingExporter.class](config) + if (kind === 'telemetry-to-caas') { + exporter = createCaaSExporter( + credentials, + config, + '/v1/logs', + cfg => new loggingExporterModule[loggingExporter.class](cfg) + ) + } + + exporter ??= new loggingExporterModule[loggingExporter.class](config) + LOG._debug && LOG.debug('Using logs exporter:', exporter) return exporter diff --git a/lib/metrics/index.js b/lib/metrics/index.js index f876433f..91697f65 100644 --- a/lib/metrics/index.js +++ b/lib/metrics/index.js @@ -6,7 +6,14 @@ const { getStringFromEnv } = require('@opentelemetry/core') const { resourceFromAttributes } = require('@opentelemetry/resources') const { AggregationTemporality, MeterProvider, PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics') -const { getDynatraceMetadata, getCredsForDTAsUPS, getCredsForCLSAsUPS, augmentCLCreds, _require } = require('../utils') +const { + getDynatraceMetadata, + getCredsForDTAsUPS, + getCredsForCLSAsUPS, + augmentCLCreds, + createCaaSExporter, + _require +} = require('../utils') const _protocol2module = { grpc: '@opentelemetry/exporter-metrics-otlp-grpc', @@ -20,6 +27,7 @@ function _getExporter() { metrics: { exporter: metricsExporter }, credentials } = cds.env.requires.telemetry + let exporter if (metricsExporter === 'env') { // ... process env to determine exporter module to use @@ -78,8 +86,19 @@ function _getExporter() { config.credentials ??= credentials.credentials } - const exporter = new metricsExporterModule[metricsExporter.class](config) + if (kind === 'telemetry-to-caas') { + exporter = createCaaSExporter( + credentials, + config, + '/v1/metrics', + cfg => new metricsExporterModule[metricsExporter.class](cfg) + ) + } + + exporter ??= new metricsExporterModule[metricsExporter.class](config) + LOG._debug && LOG.debug('Using metrics exporter:', exporter) + return exporter } diff --git a/lib/tracing/index.js b/lib/tracing/index.js index 957ed6d3..2404399e 100644 --- a/lib/tracing/index.js +++ b/lib/tracing/index.js @@ -11,6 +11,7 @@ const { getCredsForDTAsUPS, getCredsForCLSAsUPS, augmentCLCreds, + createCaaSExporter, hasDependency, _require } = require('../utils') @@ -81,6 +82,7 @@ function _getExporter() { tracing: { exporter: tracingExporter }, credentials } = cds.env.requires.telemetry + let exporter // for kind telemetry-to-otlp based on env vars if (tracingExporter === 'env') { @@ -129,7 +131,17 @@ function _getExporter() { config.credentials ??= credentials.credentials } - const exporter = new tracingExporterModule[tracingExporter.class](config) + if (kind === 'telemetry-to-caas') { + exporter = createCaaSExporter( + credentials, + config, + '/v1/traces', + cfg => new tracingExporterModule[tracingExporter.class](cfg) + ) + } + + exporter ??= new tracingExporterModule[tracingExporter.class](config) + LOG._debug && LOG.debug('Using trace exporter:', exporter) return exporter diff --git a/lib/utils.js b/lib/utils.js index f72c814e..0ace4c93 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -2,6 +2,7 @@ const cds = require('@sap/cds') const LOG = cds.log('telemetry') const fs = require('fs') +const { initializeZTI, isZTIEnabled, getRotatingAgentFactory, loadInitialCerts, certsAvailable } = require('./zti') const { DiagLogLevel } = require('@opentelemetry/api') const { hrTimeToMilliseconds, getStringFromEnv } = require('@opentelemetry/core') @@ -13,6 +14,8 @@ const { SEMRESATTRS_SERVICE_INSTANCE_ID: ATTR_SERVICE_INSTANCE_ID } = require('@opentelemetry/semantic-conventions') +const MAX_BUFFER_SIZE = Number(process.env.TELEMETRY_MAX_BUFFER_SIZE ?? 10) + function getDiagLogLevel() { if (process.env.OTEL_LOG_LEVEL) { let level = Number(process.env.OTEL_LOG_LEVEL) @@ -137,6 +140,111 @@ function getCredsForCLSAsUPS() { } } +function decodeCredentials(creds) { + if (creds.cert.startsWith('LS0t') || !creds.cert.startsWith('-----BEGIN')) { + const cert = Buffer.from(creds.cert, 'base64').toString('utf-8') + const key = Buffer.from(creds.key, 'base64').toString('utf-8') + if (!cert.includes('-----BEGIN') || !key.includes('-----BEGIN')) { + throw new Error('Decoded x509 cert or key is not valid PEM data') + } + return { cert, key } + } + return { cert: creds.cert, key: creds.key } +} + +function setupZTI() { + if (!isZTIEnabled()) return false + if (!initializeZTI()) return false + loadInitialCerts() + return true +} + +function setupStaticCerts() { + const { x509 } = cds.env.requires.telemetry || {} + if (!x509?.cert || !x509?.key) return false + + const { cert, key } = decodeCredentials(x509) + cds.env.requires.telemetry.x509 = { cert, key } + return true +} + +function augmentCaaSCreds(credentials) { + if (credentials._augmented) return + credentials._augmented = true + + if (!credentials.otlp?.http) { + throw new Error('No OTLP HTTP endpoint in CaaS credentials') + } + + credentials.baseUrl = credentials.otlp.http + + const useZTI = setupZTI() + if (!useZTI) setupStaticCerts() + + credentials.httpAgentOptions = getRotatingAgentFactory() + credentials.useZTI = useZTI +} + +function wrapExporterWithBuffer(exporter) { + const buffer = [] + let ready = false + const originalExport = exporter.export.bind(exporter) + + exporter.export = function (items, callback) { + if (!ready) { + if (certsAvailable()) { + ready = true + if (buffer.length > 0) { + LOG._debug && LOG.debug(`Flushing ${buffer.length} buffered items`) + for (const buffered of buffer) { + originalExport(buffered, () => {}) + } + buffer.length = 0 + } + // Restore original export for all future calls + exporter.export = originalExport + return originalExport(items, callback) + } + // Not ready yet — buffer this export batch. MAX_BUFFER_SIZE caps the + // number of pending batches (not records). The batch is ack'd as success + // to the SDK now; if certs never arrive, buffered batches are dropped. + if (buffer.length >= MAX_BUFFER_SIZE) { + buffer.shift() + LOG._warn && LOG.warn('Buffer full, dropping oldest batch') + } + buffer.push(items) + callback({ code: 0 }) + return + } + return originalExport(items, callback) + } + + return exporter +} + +function createCaaSExporter(credentials, config, signalPath, createExporterFn) { + if (!credentials) throw new Error('No CaaS credentials found.') + + augmentCaaSCreds(credentials) + + if (!certsAvailable() && !credentials.useZTI) { + throw new Error('CaaS requires mTLS. Bind zero-trust-identity service or configure x509 credentials.') + } + + const exporterConfig = { + ...config, + url: credentials.baseUrl + signalPath, + httpAgentOptions: credentials.httpAgentOptions + } + + let exporter = createExporterFn(exporterConfig) + + // If ZTI and certs not yet available, wrap with buffering + if (credentials.useZTI && !certsAvailable()) exporter = wrapExporterWithBuffer(exporter) + + return exporter +} + function augmentCLCreds(credentials) { if (credentials._augmented) return credentials._augmented = true @@ -203,7 +311,11 @@ module.exports = { getCredsForDTAsUPS, getCredsForCLSAsUPS, augmentCLCreds, + createCaaSExporter, hasDependency, _hrnow, - _require + _require, + // exported for testing only (reached in production via createCaaSExporter) + augmentCaaSCreds, + wrapExporterWithBuffer } diff --git a/lib/zti.js b/lib/zti.js new file mode 100644 index 00000000..4efaa463 --- /dev/null +++ b/lib/zti.js @@ -0,0 +1,207 @@ +const cds = require('@sap/cds') +const LOG = cds.log('telemetry') +const fs = require('fs') + +const SVID_DIR = '/home/vcap/app/spire-svids' + +let _paths = null +let _cached = null +let _watcher = null + +function _getSVIDCertificate() { + if (!_paths) throw new Error('ZTI paths not initialized') + + let stat + try { + stat = fs.statSync(_paths.cert) + } catch (e) { + if (_cached) { + LOG._debug && LOG.debug('Stat failure, serving cached credentials') + return _cached + } + throw e + } + + const mtime = stat.mtimeMs + if (_cached?.mtime === mtime) return _cached + + try { + _cached = { + cert: fs.readFileSync(_paths.cert, 'utf8'), + key: fs.readFileSync(_paths.key, 'utf8'), + bundle: fs.readFileSync(_paths.bundle, 'utf8'), + mtime + } + return _cached + } catch (err) { + if (_cached) { + LOG._warn && LOG.warn('Failed to reload SVID files, serving cached:', err) + return _cached + } + throw err + } +} + +function certsAvailable() { + if (isZTIEnabled() && loadInitialCerts()) { + startSVIDWatcher() + return true + } + // ZTI disabled, or enabled-by-default but not bound: fall back to static x509 + return !!cds.env.requires.telemetry?.x509?.cert +} + +function getZTIConfig() { + if (!process.env.VCAP_SERVICES) return null + + const vcap = JSON.parse(process.env.VCAP_SERVICES) + const zti = vcap['zero-trust-identity'] + if (!zti || zti.length === 0) return null + + const svidName = zti[0].credentials?.parameters?.['svid-store']?.file?.name + if (!svidName) { + LOG._warn && LOG.warn('zero-trust-identity binding missing svid-store.file.name') + return null + } + + const svidDir = process.env.TELEMETRY_ZTI_DIR || SVID_DIR + return { svidDir, svidName } +} + +function initializeZTI() { + const config = getZTIConfig() + if (!config) return false + + _paths = { + cert: `${config.svidDir}/${config.svidName}.svid.pem`, + key: `${config.svidDir}/${config.svidName}.svid.key`, + bundle: `${config.svidDir}/${config.svidName}.bundle.pem` + } + return true +} + +function isZTIEnabled() { + return process.env.TELEMETRY_USE_ZTI !== 'false' +} + +function loadInitialCerts() { + if (!_paths) return false + try { + const creds = _getSVIDCertificate() + if (!cds.env.requires.telemetry) cds.env.requires.telemetry = {} + cds.env.requires.telemetry.x509 = { cert: creds.cert, key: creds.key } + return true + } catch { + return false + } +} + +function startSVIDWatcher() { + if (_watcher) return // already watching + if (!_paths) throw new Error('ZTI not initialized') + + let debounce = null + const dir = require('path').dirname(_paths.cert) + + try { + _watcher = fs.watch(dir, (_eventType, filename) => { + if (filename && !filename.endsWith('.svid.pem')) return + if (debounce) clearTimeout(debounce) + debounce = setTimeout(() => { + try { + _cached = null + const creds = _getSVIDCertificate() + if (!cds.env.requires.telemetry) cds.env.requires.telemetry = {} + cds.env.requires.telemetry.x509 = { cert: creds.cert, key: creds.key } + cds.emit('svid', { cert: creds.cert, key: creds.key }) + LOG._debug && LOG.debug('SVID certificate rotated') + } catch (err) { + LOG._error && LOG.error('Failed to reload SVID on rotation:', err) + } + }, 1000) + }) + } catch (err) { + LOG._warn && LOG.warn('Failed to setup SVID watcher:', err) + } +} + +function stopSVIDWatcher() { + if (_watcher) { + _watcher.close() + _watcher = null + } +} + +let _RotatingCertAgent = null + +function getRotatingCertAgentClass() { + if (_RotatingCertAgent) return _RotatingCertAgent + const https = require('https') + + _RotatingCertAgent = class RotatingCertAgent extends https.Agent { + constructor() { + const x509 = cds.env.requires.telemetry?.x509 + super({ keepAlive: true, cert: x509?.cert, key: x509?.key }) + this._boundRotate = this._rotate.bind(this) + cds.on('svid', this._boundRotate) + } + + _rotate(payload) { + try { + const { cert, key } = payload ?? cds.env.requires.telemetry?.x509 ?? {} + if (!cert || !key) { + LOG._warn && LOG.warn('RotatingCertAgent: no credentials in event payload or cds.env') + return + } + this.options.cert = cert + this.options.key = key + this.destroy() + LOG._debug && LOG.debug('Certificate rotated') + } catch (err) { + LOG._error && LOG.error('Failed to rotate certificate:', err) + } + } + + _cleanup() { + cds.off('svid', this._boundRotate) + this.destroy() + } + } + return _RotatingCertAgent +} + +let _rotatingAgent = null + +function createRotatingAgent() { + if (!_rotatingAgent) { + const RotatingCertAgent = getRotatingCertAgentClass() + _rotatingAgent = new RotatingCertAgent() + } + return _rotatingAgent +} + +function getRotatingAgentFactory() { + return () => createRotatingAgent() +} + +function reset() { + _paths = null + _cached = null + stopSVIDWatcher() + if (_rotatingAgent) { + _rotatingAgent._cleanup() + _rotatingAgent = null + } +} + +module.exports = { + initializeZTI, + isZTIEnabled, + loadInitialCerts, + certsAvailable, + getRotatingAgentFactory, + // exported for testing only (used internally within this module in production) + getZTIConfig, + getRotatingCertAgentClass, + reset +} diff --git a/package.json b/package.json index 11f1693e..5d5bf088 100644 --- a/package.json +++ b/package.json @@ -175,6 +175,25 @@ "metrics": { "exporter": "env" } + }, + "telemetry-to-caas": { + "vcap": [ + { + "label": "caas-service" + } + ], + "tracing": { + "exporter": { + "module": "@opentelemetry/exporter-trace-otlp-proto", + "class": "OTLPTraceExporter" + } + }, + "metrics": { + "exporter": { + "module": "@opentelemetry/exporter-metrics-otlp-proto", + "class": "OTLPMetricExporter" + } + } } } } diff --git a/test/buffer.test.js b/test/buffer.test.js new file mode 100644 index 00000000..92f01e91 --- /dev/null +++ b/test/buffer.test.js @@ -0,0 +1,149 @@ +/** + * Tests for wrapExporterWithBuffer - isolated in separate file for clean process isolation. + * These tests manipulate the cds.env singleton which persists across vi.resetModules() calls. + * Tests use static x509 certs (ZTI disabled) to test buffering behavior. + */ +const cds = require('@sap/cds') + +describe('wrapExporterWithBuffer', () => { + beforeEach(() => { + vi.resetModules() + // Clear env vars that affect ZTI detection + delete process.env.VCAP_SERVICES + delete process.env.TELEMETRY_ZTI_DIR + // Disable ZTI to test static x509 cert path + process.env.TELEMETRY_USE_ZTI = 'false' + // Clear cds.env singleton - especially x509 certs from prior tests + cds.env.requires = { telemetry: {} } + }) + + afterEach(() => { + delete process.env.TELEMETRY_USE_ZTI + }) + + test('buffers items until certs are available', async () => { + const { wrapExporterWithBuffer } = await import('../lib/utils.js') + + const exportedItems = [] + const mockExporter = { + export: vi.fn((items, cb) => { + exportedItems.push(...items) + cb({ code: 0 }) + }), + shutdown: vi.fn(() => Promise.resolve()) + } + + const wrapped = wrapExporterWithBuffer(mockExporter) + + // Export while not ready - should buffer + wrapped.export(['item1'], () => {}) + wrapped.export(['item2'], () => {}) + expect(exportedItems.length).toBe(0) + + // Make certs available + cds.env.requires.telemetry.x509 = { + cert: '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----', + key: '-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----' + } + + // Next export triggers flush + new export + wrapped.export(['item3'], () => {}) + expect(exportedItems).toEqual(['item1', 'item2', 'item3']) + }) + + test('drops oldest when buffer full', async () => { + const { wrapExporterWithBuffer } = await import('../lib/utils.js') + + const exportedItems = [] + const mockExporter = { + export: vi.fn((items, cb) => { + exportedItems.push(...items) + cb({ code: 0 }) + }) + } + + const wrapped = wrapExporterWithBuffer(mockExporter) + + // Fill buffer beyond max (10) + for (let i = 0; i < 15; i++) { + wrapped.export([`item${i}`], () => {}) + } + + // Make certs available and trigger flush + cds.env.requires.telemetry.x509 = { + cert: '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----', + key: '-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----' + } + wrapped.export(['final'], () => {}) + + // Buffer was capped at 10, so oldest 5 items were dropped + // Exported: 10 buffered + 1 final = 11 + expect(exportedItems.length).toBe(11) + expect(exportedItems[0]).toBe('item5') // item0-4 were dropped + expect(exportedItems[9]).toBe('item14') + expect(exportedItems[10]).toBe('final') + }) + + test('exports directly when certs already available', async () => { + cds.env.requires = { + telemetry: { + x509: { + cert: '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----', + key: '-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----' + } + } + } + + const { wrapExporterWithBuffer } = await import('../lib/utils.js') + + const exportedItems = [] + const mockExporter = { + export: vi.fn((items, cb) => { + exportedItems.push(...items) + cb({ code: 0 }) + }) + } + + const wrapped = wrapExporterWithBuffer(mockExporter) + + // Should export directly, no buffering + wrapped.export(['item1'], () => {}) + expect(exportedItems).toEqual(['item1']) + + wrapped.export(['item2'], () => {}) + expect(exportedItems).toEqual(['item1', 'item2']) + }) + + test('after certs available, exports go directly to original (no buffer)', async () => { + const { wrapExporterWithBuffer } = await import('../lib/utils.js') + + const exportCalls = [] + const mockExporter = { + export: (items, cb) => { + exportCalls.push(items) + cb({ code: 0 }) + } + } + + const wrapped = wrapExporterWithBuffer(mockExporter) + + // Buffer while not ready + wrapped.export(['item1'], () => {}) + expect(exportCalls.length).toBe(0) + + // Make certs available + cds.env.requires.telemetry.x509 = { + cert: '-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----', + key: '-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----' + } + + // First export after certs: flushes buffer + exports new item + wrapped.export(['item2'], () => {}) + expect(exportCalls).toEqual([['item1'], ['item2']]) + + // Subsequent exports go directly (no re-checking certsAvailable) + wrapped.export(['item3'], () => {}) + wrapped.export(['item4'], () => {}) + expect(exportCalls).toEqual([['item1'], ['item2'], ['item3'], ['item4']]) + }) +}) diff --git a/test/caas.test.js b/test/caas.test.js new file mode 100644 index 00000000..911c99a7 --- /dev/null +++ b/test/caas.test.js @@ -0,0 +1,519 @@ +const cds = require('@sap/cds') +const fs = require('fs') +const os = require('os') +const path = require('path') +const { trace } = require('@opentelemetry/api') + +// Mock VCAP_SERVICES for CaaS +const MOCK_CAAS_VCAP = { + 'caas-service': [ + { + name: 'test-caas', + credentials: { + otlp: { + http: 'https://caas.example.com/otlp', + grpc: 'grpc://caas.example.com:4317' + } + } + } + ] +} + +// Mock VCAP_SERVICES with ZTI binding +const MOCK_ZTI_VCAP = { + 'caas-service': [ + { + name: 'test-caas', + credentials: { + otlp: { + http: 'https://caas.example.com/otlp' + } + } + } + ], + 'zero-trust-identity': [ + { + name: 'test-zti', + credentials: { + parameters: { + 'svid-store': { + file: { name: 'test-svid' } + } + } + } + } + ] +} + +// Test certificates +const CERT_V1 = '-----BEGIN CERTIFICATE-----\nCERT_VERSION_1\n-----END CERTIFICATE-----' +const KEY_V1 = '-----BEGIN PRIVATE KEY-----\nKEY_VERSION_1\n-----END PRIVATE KEY-----' +const BUNDLE_V1 = '-----BEGIN CERTIFICATE-----\nBUNDLE_V1\n-----END CERTIFICATE-----' +const CERT_V2 = '-----BEGIN CERTIFICATE-----\nCERT_VERSION_2\n-----END CERTIFICATE-----' +const KEY_V2 = '-----BEGIN PRIVATE KEY-----\nKEY_VERSION_2\n-----END PRIVATE KEY-----' + +// Shared test helper for ZTI setup +function createZTITestContext() { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'zti-test-')) + const svidDir = path.join(tmpDir, 'spire-svids') + fs.mkdirSync(svidDir) + + return { + tmpDir, + svidDir, + writeSVIDFiles(cert = CERT_V1, key = KEY_V1, bundle = BUNDLE_V1) { + fs.writeFileSync(path.join(svidDir, 'test-svid.svid.pem'), cert) + fs.writeFileSync(path.join(svidDir, 'test-svid.svid.key'), key) + fs.writeFileSync(path.join(svidDir, 'test-svid.bundle.pem'), bundle) + }, + touchCertFile() { + const now = new Date() + now.setSeconds(now.getSeconds() + 2) + fs.utimesSync(path.join(svidDir, 'test-svid.svid.pem'), now, now) + }, + setupEnv() { + process.env.VCAP_SERVICES = JSON.stringify(MOCK_ZTI_VCAP) + process.env.TELEMETRY_ZTI_DIR = svidDir + delete process.env.TELEMETRY_USE_ZTI + cds.env.requires = { telemetry: {} } + }, + clearModuleCache() { + vi.resetModules() + }, + cleanup() { + fs.rmSync(tmpDir, { recursive: true, force: true }) + delete process.env.VCAP_SERVICES + delete process.env.TELEMETRY_ZTI_DIR + delete process.env.TELEMETRY_USE_ZTI + vi.resetModules() + } + } +} + +describe('augmentCaaSCreds', () => { + let originalVcap + + beforeAll(() => { + originalVcap = process.env.VCAP_SERVICES + }) + + afterAll(() => { + if (originalVcap) process.env.VCAP_SERVICES = originalVcap + else delete process.env.VCAP_SERVICES + }) + + beforeEach(() => { + vi.resetModules() + cds.env.requires = cds.env.requires || {} + cds.env.requires.telemetry = { + x509: { + cert: Buffer.from('-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----').toString('base64'), + key: Buffer.from('-----BEGIN PRIVATE KEY-----\ntest\n-----END PRIVATE KEY-----').toString('base64') + } + } + }) + + test('sets baseUrl from otlp.http', async () => { + process.env.VCAP_SERVICES = JSON.stringify(MOCK_CAAS_VCAP) + const { augmentCaaSCreds } = await import('../lib/utils.js') + + const credentials = { + otlp: { + http: 'https://caas.example.com/otlp', + grpc: 'grpc://caas.example.com:4317' + } + } + + augmentCaaSCreds(credentials) + + expect(credentials.baseUrl).toBe('https://caas.example.com/otlp') + }) + + test('sets httpAgentOptions when mTLS credentials found', async () => { + process.env.VCAP_SERVICES = JSON.stringify(MOCK_CAAS_VCAP) + const { augmentCaaSCreds } = await import('../lib/utils.js') + + const credentials = { + otlp: { http: 'https://caas.example.com/otlp' } + } + + augmentCaaSCreds(credentials) + + expect(credentials.httpAgentOptions).toBeDefined() + expect(typeof credentials.httpAgentOptions).toBe('function') + }) + + test('throws when no OTLP endpoints', async () => { + process.env.VCAP_SERVICES = JSON.stringify(MOCK_CAAS_VCAP) + const { augmentCaaSCreds } = await import('../lib/utils.js') + + expect(() => augmentCaaSCreds({})).toThrow('No OTLP HTTP endpoint in CaaS credentials') + }) + + test('does not augment twice', async () => { + process.env.VCAP_SERVICES = JSON.stringify(MOCK_CAAS_VCAP) + const { augmentCaaSCreds } = await import('../lib/utils.js') + + const credentials = { + otlp: { http: 'https://caas.example.com/otlp' } + } + + augmentCaaSCreds(credentials) + const originalBaseUrl = credentials.baseUrl + + credentials.otlp.http = 'https://different.com' + augmentCaaSCreds(credentials) + + expect(credentials.baseUrl).toBe(originalBaseUrl) + }) + + test('httpAgentOptions always set (agent works once certs available)', async () => { + cds.env.requires.telemetry = {} // No x509 credentials + process.env.VCAP_SERVICES = JSON.stringify(MOCK_CAAS_VCAP) + const { augmentCaaSCreds } = await import('../lib/utils.js') + + const credentials = { + otlp: { http: 'https://caas.example.com/otlp' } + } + + augmentCaaSCreds(credentials) + + // Agent factory is always set — agent works once certs become available + expect(credentials.httpAgentOptions).toBeDefined() + expect(typeof credentials.httpAgentOptions).toBe('function') + }) +}) + +describe('ZTI', () => { + let ctx + + beforeEach(() => { + ctx = createZTITestContext() + ctx.setupEnv() + }) + + afterEach(() => { + ctx.cleanup() + }) + + describe('getZTIConfig', () => { + test('detects ZTI config from VCAP_SERVICES', async () => { + const { getZTIConfig } = await import('../lib/zti.js') + const config = getZTIConfig() + + expect(config).not.toBeNull() + expect(config.svidName).toBe('test-svid') + expect(config.svidDir).toBe(ctx.svidDir) + }) + + test('returns null when no ZTI binding', async () => { + process.env.VCAP_SERVICES = JSON.stringify(MOCK_CAAS_VCAP) + vi.resetModules() + + const { getZTIConfig } = await import('../lib/zti.js') + expect(getZTIConfig()).toBeNull() + }) + }) + + describe('RotatingCertAgent', () => { + test('can be created without certs', async () => { + // No certs in cds.env + cds.env.requires.telemetry = {} + + const { getRotatingCertAgentClass, reset } = await import('../lib/zti.js') + + // Should not throw + const RotatingCertAgent = getRotatingCertAgentClass() + const agent = new RotatingCertAgent() + expect(agent).toBeDefined() + expect(agent.options.cert).toBeUndefined() + expect(agent.options.key).toBeUndefined() + + agent._cleanup() + reset() + }) + + test('uses certs when available at construction', async () => { + ctx.writeSVIDFiles() + + const { getRotatingCertAgentClass, initializeZTI, loadInitialCerts, reset } = await import('../lib/zti.js') + + initializeZTI() + loadInitialCerts() + + const RotatingCertAgent = getRotatingCertAgentClass() + const agent = new RotatingCertAgent() + expect(agent.options.cert).toBe(CERT_V1) + expect(agent.options.key).toBe(KEY_V1) + + agent._cleanup() + reset() + }) + }) + + describe('certsAvailable', () => { + test('returns false when no certs and no SVID files', async () => { + cds.env.requires.telemetry = {} + const { certsAvailable } = await import('../lib/zti.js') + expect(certsAvailable()).toBe(false) + }) + + test('returns true when SVID files exist (ZTI enabled)', async () => { + ctx.writeSVIDFiles() + vi.resetModules() + const { certsAvailable, initializeZTI } = await import('../lib/zti.js') + initializeZTI() + expect(certsAvailable()).toBe(true) + }) + + test('returns true when certs in cds.env and ZTI disabled', async () => { + process.env.TELEMETRY_USE_ZTI = 'false' + cds.env.requires = { + telemetry: { + x509: { cert: CERT_V1, key: KEY_V1 } + } + } + vi.resetModules() + const { certsAvailable } = await import('../lib/zti.js') + expect(certsAvailable()).toBe(true) + }) + }) + + // Certificate rotation works for both interval-based exports (production, metrics) and + // on-demand exports (tracing/logging in development). The test verifies this by checking + // that the factory returns a singleton: after rotation via 'svid' event updates the agent's certs, + // any subsequent factory() call - whether from a scheduled interval or an immediate + // span.end() - returns the same agent instance with the rotated certificates. + describe('certificate rotation', () => { + test('agent rotates certificate when svid event is emitted', async () => { + ctx.writeSVIDFiles() + + const { getRotatingAgentFactory, initializeZTI, loadInitialCerts, reset } = await import('../lib/zti.js') + + initializeZTI() + loadInitialCerts() + + const factory = getRotatingAgentFactory() + const agent = factory() + expect(agent.options.cert).toBe(CERT_V1) + expect(agent.options.key).toBe(KEY_V1) + + // Simulate certificate rotation via event (as SVID watcher does in production) + cds.emit('svid', { cert: CERT_V2, key: KEY_V2 }) + + expect(agent.options.cert).toBe(CERT_V2) + expect(agent.options.key).toBe(KEY_V2) + + // Factory returns same singleton — on-demand exports use rotated certs + expect(factory()).toBe(agent) + + reset() + }) + + test('agent rotates by re-reading cds.env when svid event has no payload', async () => { + ctx.writeSVIDFiles() + + const { getRotatingAgentFactory, initializeZTI, loadInitialCerts, reset } = await import('../lib/zti.js') + + initializeZTI() + loadInitialCerts() + + const factory = getRotatingAgentFactory() + const agent = factory() + expect(agent.options.cert).toBe(CERT_V1) + + // Simulate manual rotation: update cds.env and emit event without payload + cds.env.requires.telemetry.x509 = { cert: CERT_V2, key: KEY_V2 } + cds.emit('svid') + + expect(agent.options.cert).toBe(CERT_V2) + expect(agent.options.key).toBe(KEY_V2) + + reset() + }) + + test('agent keeps cached cert when rotation event has invalid payload', async () => { + ctx.writeSVIDFiles() + + const { getRotatingAgentFactory, initializeZTI, loadInitialCerts, reset } = await import('../lib/zti.js') + + initializeZTI() + loadInitialCerts() + + const factory = getRotatingAgentFactory() + const agent = factory() + expect(agent.options.cert).toBe(CERT_V1) + + // Emit event with invalid payload — agent should keep old certs + cds.emit('svid', { cert: null, key: null }) + expect(agent.options.cert).toBe(CERT_V1) + + // Factory returns same singleton — on-demand exports still work with cached certs + expect(factory()).toBe(agent) + + reset() + }) + }) + + describe('x509 fallback', () => { + test('augmentCaaSCreds uses ZTI when available', async () => { + ctx.writeSVIDFiles() + + const { augmentCaaSCreds } = await import('../lib/utils.js') + const { reset } = await import('../lib/zti.js') + + const credentials = { otlp: { http: 'https://caas.example.com/otlp' } } + augmentCaaSCreds(credentials) + + expect(credentials.httpAgentOptions).toBeDefined() + expect(typeof credentials.httpAgentOptions).toBe('function') + expect(credentials.useZTI).toBe(true) + + reset() + }) + + test('augmentCaaSCreds uses x509 when ZTI disabled', async () => { + process.env.VCAP_SERVICES = JSON.stringify(MOCK_CAAS_VCAP) + process.env.TELEMETRY_USE_ZTI = 'false' + cds.env.requires.telemetry.x509 = { + cert: Buffer.from('-----BEGIN CERTIFICATE-----\nenvvar-cert\n-----END CERTIFICATE-----').toString('base64'), + key: Buffer.from('-----BEGIN PRIVATE KEY-----\nenvvar-key\n-----END PRIVATE KEY-----').toString('base64') + } + + ctx.clearModuleCache() + const { augmentCaaSCreds } = await import('../lib/utils.js') + + const credentials = { otlp: { http: 'https://caas.example.com/otlp' } } + augmentCaaSCreds(credentials) + + expect(credentials.httpAgentOptions).toBeDefined() + expect(typeof credentials.httpAgentOptions).toBe('function') + expect(credentials.useZTI).toBe(false) + }) + + test('augmentCaaSCreds uses x509 when ZTI enabled-by-default but not bound', async () => { + // Manual-cert path as documented in the README: static x509 credentials, no + // zero-trust-identity binding, and TELEMETRY_USE_ZTI left at its default (enabled). + // certsAvailable() must still recognize the static certs so createCaaSExporter does + // not reject the setup as "missing mTLS". + process.env.VCAP_SERVICES = JSON.stringify(MOCK_CAAS_VCAP) + delete process.env.TELEMETRY_USE_ZTI + cds.env.requires.telemetry.x509 = { + cert: Buffer.from('-----BEGIN CERTIFICATE-----\nenvvar-cert\n-----END CERTIFICATE-----').toString('base64'), + key: Buffer.from('-----BEGIN PRIVATE KEY-----\nenvvar-key\n-----END PRIVATE KEY-----').toString('base64') + } + + ctx.clearModuleCache() + const { augmentCaaSCreds } = await import('../lib/utils.js') + const { certsAvailable } = await import('../lib/zti.js') + + const credentials = { otlp: { http: 'https://caas.example.com/otlp' } } + augmentCaaSCreds(credentials) + + expect(credentials.useZTI).toBe(false) + expect(certsAvailable()).toBe(true) + }) + }) +}) + +describe('CaaS integration', () => { + let ctx + + beforeEach(() => { + ctx = createZTITestContext() + ctx.setupEnv() + process.env.cds_requires_telemetry_kind = 'to-caas' + vi.resetModules() + }) + + afterEach(() => { + ctx.cleanup() + delete process.env.cds_requires_telemetry_kind + delete process.env.cds_requires_telemetry_tracing_exporter + }) + + test('TracerProvider works with ZTI credentials', async () => { + ctx.writeSVIDFiles() + + cds.env.requires = { + telemetry: { + kind: 'telemetry-to-caas', + credentials: { + otlp: { http: 'https://caas.example.com/otlp' } + }, + tracing: { + exporter: { + module: '@opentelemetry/sdk-trace-base', + class: 'InMemorySpanExporter' + }, + sampler: { kind: 'AlwaysOnSampler' }, + propagators: [], + processor: { kind: 'SimpleSpanProcessor' } + }, + instrumentations: {} + } + } + + const setup = await import('../lib/index.js') + setup.default() + + // Verify tracer works (not a NoopTracer) + const tracer = trace.getTracer('test') + const span = tracer.startSpan('test-span') + expect(span.spanContext().traceId).toBeDefined() + expect(span.spanContext().traceId).not.toBe('00000000000000000000000000000000') + span.end() + }) + + test('Metrics use DELTA aggregation temporality with ZTI', async () => { + ctx.writeSVIDFiles() + + cds.env.requires = { + telemetry: { + kind: 'telemetry-to-caas', + credentials: { + otlp: { http: 'https://caas.example.com/otlp' } + }, + metrics: { + exporter: { + module: '@opentelemetry/exporter-metrics-otlp-proto', + class: 'OTLPMetricExporter' + } + }, + instrumentations: {} + } + } + + const { AggregationTemporality, InstrumentType } = await import('@opentelemetry/sdk-metrics') + const { getResource } = await import('../lib/utils.js') + const metricsSetup = await import('../lib/metrics/index.js') + + // Setup metrics with resource + const resource = getResource() + const meterProvider = metricsSetup.default(resource) + + // Verify metrics provider was created + expect(meterProvider).toBeDefined() + + // Get the metric collectors (contains the reader) + const collectors = meterProvider._sharedState.metricCollectors + expect(collectors).toBeDefined() + expect(collectors.length).toBeGreaterThan(0) + + const collector = collectors[0] + expect(collector.selectAggregationTemporality).toBeDefined() + + // Verify DELTA temporality is used (not CUMULATIVE) for COUNTER + const counterTemporality = collector.selectAggregationTemporality(InstrumentType.COUNTER) + expect(counterTemporality).toBe(AggregationTemporality.DELTA) + expect(counterTemporality).not.toBe(AggregationTemporality.CUMULATIVE) + + // Verify for HISTOGRAM as well + const histogramTemporality = collector.selectAggregationTemporality(InstrumentType.HISTOGRAM) + expect(histogramTemporality).toBe(AggregationTemporality.DELTA) + + // UP_DOWN_COUNTER should be CUMULATIVE by design (this is correct OTel behavior) + const upDownCounterTemporality = collector.selectAggregationTemporality(InstrumentType.UP_DOWN_COUNTER) + expect(upDownCounterTemporality).toBe(AggregationTemporality.CUMULATIVE) + }) +}) From 1b1bf6626201737a52d589c62c3e5101126a19ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 00:32:27 +0200 Subject: [PATCH 23/31] chore(deps-dev): bump the dev-dependencies group across 1 directory with 12 updates (#505) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the dev-dependencies group with 12 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@cap-js/cds-test](https://github.com/cap-js/cds-test) | `1.0.1` | `1.0.2` | | [@cap-js/sqlite](https://github.com/cap-js/cds-dbs) | `3.0.2` | `3.1.0` | | [@opentelemetry/exporter-metrics-otlp-grpc](https://github.com/open-telemetry/opentelemetry-js) | `0.219.0` | `0.222.0` | | [@opentelemetry/exporter-metrics-otlp-proto](https://github.com/open-telemetry/opentelemetry-js) | `0.219.0` | `0.222.0` | | [@opentelemetry/exporter-trace-otlp-grpc](https://github.com/open-telemetry/opentelemetry-js) | `0.219.0` | `0.222.0` | | [@opentelemetry/exporter-trace-otlp-proto](https://github.com/open-telemetry/opentelemetry-js) | `0.219.0` | `0.222.0` | | [@opentelemetry/instrumentation-host-metrics](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/instrumentation-host-metrics) | `0.2.0` | `0.5.0` | | [@opentelemetry/instrumentation-runtime-node](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/instrumentation-runtime-node) | `0.32.0` | `0.35.0` | | [@sap/cds-mtxs](https://cap.cloud.sap/) | `4.0.1` | `4.1.0` | | [axios](https://github.com/axios/axios) | `1.18.1` | `1.20.0` | | [eslint](https://github.com/eslint/eslint) | `10.6.0` | `10.10.0` | | [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest) | `30.4.2` | `30.5.1` | Updates `@cap-js/cds-test` from 1.0.1 to 1.0.2
Release notes

Sourced from @​cap-js/cds-test's releases.

v1.0.2

Fixed

  • Buffer request bodies are no longer serialized via JSON.stringify()
  • In chest-powered tests, inspectPort configuration will be passed to node:test.run(...) when debugging
  • In chest-powered tests, concurrency of node:test.run(...) will be limitted to 1 to enable debugging
Changelog

Sourced from @​cap-js/cds-test's changelog.

[1.0.2] - 2026-08-04

Fixed

  • Buffer request bodies are no longer serialized via JSON.stringify()
  • In chest-powered tests, inspectPort configuration will be passed to node:test.run(...) when debugging
  • In chest-powered tests, concurrency of node:test.run(...) will be limitted to 1 to enable debugging
Commits
  • 1304e9d fix: pass debug flag to runner (#96)
  • 540bc36 Bump @​cap-js/sqlite from 2.4.0 to 3.0.2 (#102)
  • 4f150df fix(naxios): send Buffer request bodies as raw bytes (#103)
  • 4d6ca38 feat: remove sleep API (#98)
  • 6d8eac9 chore: open dependency to cap-js/sqlite 3 (#94)
  • 7456cb5 fix: fulfilled and rejected can be awaited (#93)
  • fa7721a Bump qs from 6.15.1 to 6.15.2 in the npm_and_yarn group across 1 directory (#91)
  • 7c833de fix: .to.be.fullfilled/rejected must return this (#92)
  • 0f1a65c Bump @​cap-js/sqlite from 2.2.0 to 2.4.0 (#88)
  • 90e6cf5 Temporarily reverting cds.test --with-mocks
  • Additional commits viewable in compare view

Updates `@cap-js/sqlite` from 3.0.2 to 3.1.0
Release notes

Sourced from @​cap-js/sqlite's releases.

db-service: v3.1.0

3.1.0 (2026-09-07)

Added

Fixed

  • cqn4sql: reject overly nested query expressions instead of crashing (#1712) (29a45b7)
  • don't throw when releasing/destroying a resource that's no longer in the pool (#1696) (5d3b277)
  • exclude cds.Vector from wildcard select expansion (#1716) (398bacc)
  • INSERT.from(SELECT) used non-existent value (#1565) (fd98844)
  • pool: recover lost connection after stale acquire timeout (#1724) (cf6af99)
  • pql groupby path expression (#1700) (82b5e03)

hana: v3.1.0

3.1.0 (2026-09-07)

Added

Fixed

Dependencies

  • The following workspace dependencies were updated
    • dependencies
      • @​cap-js/db-service bumped from ^3.0.1 to ^3.1.0

postgres: v3.1.0

3.1.0 (2026-09-07)

Added

  • Add support for cds.Vector on SQLite and Postgres (#1676) (f0d2203)

... (truncated)

Commits
  • dba03e9 chore: release main (#1703)
  • bc93160 feat: rank $search results by relevance (#1725)
  • 633db11 test(cqn4sql): refactor calculated elements test suite (#1643)
  • cf6af99 fix(pool): recover lost connection after stale acquire timeout (#1724)
  • c9228a6 chore(deps-dev): bump @​sap/hana-client from 2.29.25 to 2.29.27 in the depende...
  • 1443f85 chore(deps): bump qs from 6.15.3 to 6.16.0 (#1722)
  • 398bacc fix: exclude cds.Vector from wildcard select expansion (#1716)
  • fd98844 fix: INSERT.from(SELECT) used non-existent value (#1565)
  • 01cb981 feat: cache prepare statement for reuse (#1496)
  • 29a45b7 fix(cqn4sql): reject overly nested query expressions instead of crashing (#1712)
  • Additional commits viewable in compare view

Updates `@opentelemetry/exporter-metrics-otlp-grpc` from 0.219.0 to 0.222.0
Release notes

Sourced from @​opentelemetry/exporter-metrics-otlp-grpc's releases.

experimental/v0.222.0

0.222.0

:boom: Breaking Changes

  • fix(sdk-node)!: fail-fast on Propagator creation from config file #6930 @​trentm
  • fix(sdk-node)!: fail-fast on MeterProvider creation from config file #6954 @​trentm
  • fix(sdk-node)!: fail-fast on TracerProvider creation from config file #6962 @​trentm
  • fix(sdk-node)!: fail-fast on Resource creation from config file #6989 @​trentm
    • This also breaks some usage of startNodeSDK() for environment-based config, i.e. when not using a config file. For example with OTEL_NODE_RESOURCE_DETECTORS=all, it results in an error message and a no-op SDK. (This does not impact users of new NodeSDK() -- the currently recommended mechanism to start an SDK using this package.)

      Could not create OpenTelemetry SDK from
      configuration, SDK will not be setup: unknown
      ExperimentalResourceDetector name in configuration:
      "container"
      

:rocket: Features

:bug: Bug Fixes

  • fix(instrumentation-http): redact sensitive query parameters on incoming (server) spans; add redactedQueryParamsServer config option @​dyladan
  • fix(sdk-node): support headers_list when creating OTLP exporters from declarative configuration #6953 @​JacksonWeber

:books: Documentation

:house: Internal

  • refactor(sampler-jaeger-remote): remove axios dependency and use fetch to get the sampler configuration from Jaeger API #6963 @​david-luna

experimental/v0.221.0

0.221.0

:boom: Breaking Changes

  • feat(sdk-logs)!: configure the force flush timeout per call #6931 @​LarryHu0217
    • (user-facing): LoggerProviderOptions.forceFlushTimeoutMillis has been removed; pass timeoutMillis to LoggerProvider.forceFlush() instead.
  • feat(instrumentation-http)!: emit only stable HTTP semantic conventions. The OTEL_SEMCONV_STABILITY_OPT_IN environment variable no longer changes HTTP attribute or metric emission — old (v1.7.0) and duplicate (http/http/dup) semconv outputs have been removed. #6819 @​maryliag
  • feat(instrumentation-fetch)!: emit only stable HTTP semantic conventions. The semconvStabilityOptIn instrumentation config option has been removed; old (v1.7.0) and duplicate semconv outputs are no longer emitted. #6819 @​maryliag
  • feat(instrumentation-xml-http-request)!: emit only stable HTTP semantic conventions. The semconvStabilityOptIn instrumentation config option has been removed; old (v1.7.0) and duplicate semconv outputs are no longer emitted. #6819 @​maryliag
  • feat(instrumentation-grpc)!: emit only stable network semantic conventions. The OTEL_SEMCONV_STABILITY_OPT_IN environment variable no longer changes attribute emission — net.peer.name and net.peer.port (old) are no longer set; only server.address and server.port (stable). #6819 @​maryliag

:rocket: Features

  • feat(sdk-logs): allow modifying ReadWriteLogRecord properties (including hrTime, hrTimeObserved, and spanContext) in accordance with the OpenTelemetry Logs specification #6923 @​Babul422

... (truncated)

Commits
  • 0b72a81 chore: prepare next release (#7044)
  • a9c5338 ci: roll prerelease changelog into one final release changelog (#7045)
  • f41805e chore: prepare next release (#7042)
  • b85eb28 chore(instrumentation-http): fix lint errors (#7039)
  • 3f92530 ci: support pre-releases and major version bumps in release workflow (#7035)
  • 82a5831 docs(otlp-exporter-base): document HTTP exporter options (#6735)
  • e086dec Merge commit from fork
  • 59dac70 chore(deps): update jamesives/github-pages-deploy-action action to v4.9.0 (#7...
  • d0ce753 chore: add @​maryliag to maintainers (#7024)
  • 03469a1 chore(deps): update open-telemetry/shared-workflows action to v0.10.0 (#7032)
  • Additional commits viewable in compare view

Updates `@opentelemetry/exporter-metrics-otlp-proto` from 0.219.0 to 0.222.0
Release notes

Sourced from @​opentelemetry/exporter-metrics-otlp-proto's releases.

experimental/v0.222.0

0.222.0

:boom: Breaking Changes

  • fix(sdk-node)!: fail-fast on Propagator creation from config file #6930 @​trentm
  • fix(sdk-node)!: fail-fast on MeterProvider creation from config file #6954 @​trentm
  • fix(sdk-node)!: fail-fast on TracerProvider creation from config file #6962 @​trentm
  • fix(sdk-node)!: fail-fast on Resource creation from config file #6989 @​trentm
    • This also breaks some usage of startNodeSDK() for environment-based config, i.e. when not using a config file. For example with OTEL_NODE_RESOURCE_DETECTORS=all, it results in an error message and a no-op SDK. (This does not impact users of new NodeSDK() -- the currently recommended mechanism to start an SDK using this package.)

      Could not create OpenTelemetry SDK from
      configuration, SDK will not be setup: unknown
      ExperimentalResourceDetector name in configuration:
      "container"
      

:rocket: Features

:bug: Bug Fixes

  • fix(instrumentation-http): redact sensitive query parameters on incoming (server) spans; add redactedQueryParamsServer config option @​dyladan
  • fix(sdk-node): support headers_list when creating OTLP exporters from declarative configuration #6953 @​JacksonWeber

:books: Documentation

:house: Internal

  • refactor(sampler-jaeger-remote): remove axios dependency and use fetch to get the sampler configuration from Jaeger API #6963 @​david-luna

experimental/v0.221.0

0.221.0

:boom: Breaking Changes

  • feat(sdk-logs)!: configure the force flush timeout per call #6931 @​LarryHu0217
    • (user-facing): LoggerProviderOptions.forceFlushTimeoutMillis has been removed; pass timeoutMillis to LoggerProvider.forceFlush() instead.
  • feat(instrumentation-http)!: emit only stable HTTP semantic conventions. The OTEL_SEMCONV_STABILITY_OPT_IN environment variable no longer changes HTTP attribute or metric emission — old (v1.7.0) and duplicate (http/http/dup) semconv outputs have been removed. #6819 @​maryliag
  • feat(instrumentation-fetch)!: emit only stable HTTP semantic conventions. The semconvStabilityOptIn instrumentation config option has been removed; old (v1.7.0) and duplicate semconv outputs are no longer emitted. #6819 @​maryliag
  • feat(instrumentation-xml-http-request)!: emit only stable HTTP semantic conventions. The semconvStabilityOptIn instrumentation config option has been removed; old (v1.7.0) and duplicate semconv outputs are no longer emitted. #6819 @​maryliag
  • feat(instrumentation-grpc)!: emit only stable network semantic conventions. The OTEL_SEMCONV_STABILITY_OPT_IN environment variable no longer changes attribute emission — net.peer.name and net.peer.port (old) are no longer set; only server.address and server.port (stable). #6819 @​maryliag

:rocket: Features

  • feat(sdk-logs): allow modifying ReadWriteLogRecord properties (including hrTime, hrTimeObserved, and spanContext) in accordance with the OpenTelemetry Logs specification #6923 @​Babul422

... (truncated)

Commits
  • 0b72a81 chore: prepare next release (#7044)
  • a9c5338 ci: roll prerelease changelog into one final release changelog (#7045)
  • f41805e chore: prepare next release (#7042)
  • b85eb28 chore(instrumentation-http): fix lint errors (#7039)
  • 3f92530 ci: support pre-releases and major version bumps in release workflow (#7035)
  • 82a5831 docs(otlp-exporter-base): document HTTP exporter options (#6735)
  • e086dec Merge commit from fork
  • 59dac70 chore(deps): update jamesives/github-pages-deploy-action action to v4.9.0 (#7...
  • d0ce753 chore: add @​maryliag to maintainers (#7024)
  • 03469a1 chore(deps): update open-telemetry/shared-workflows action to v0.10.0 (#7032)
  • Additional commits viewable in compare view

Updates `@opentelemetry/exporter-trace-otlp-grpc` from 0.219.0 to 0.222.0
Release notes

Sourced from @​opentelemetry/exporter-trace-otlp-grpc's releases.

experimental/v0.222.0

0.222.0

:boom: Breaking Changes

  • fix(sdk-node)!: fail-fast on Propagator creation from config file #6930 @​trentm
  • fix(sdk-node)!: fail-fast on MeterProvider creation from config file #6954 @​trentm
  • fix(sdk-node)!: fail-fast on TracerProvider creation from config file #6962 @​trentm
  • fix(sdk-node)!: fail-fast on Resource creation from config file #6989 @​trentm
    • This also breaks some usage of startNodeSDK() for environment-based config, i.e. when not using a config file. For example with OTEL_NODE_RESOURCE_DETECTORS=all, it results in an error message and a no-op SDK. (This does not impact users of new NodeSDK() -- the currently recommended mechanism to start an SDK using this package.)

      Could not create OpenTelemetry SDK from
      configuration, SDK will not be setup: unknown
      ExperimentalResourceDetector name in configuration:
      "container"
      

:rocket: Features

:bug: Bug Fixes

  • fix(instrumentation-http): redact sensitive query parameters on incoming (server) spans; add redactedQueryParamsServer config option @​dyladan
  • fix(sdk-node): support headers_list when creating OTLP exporters from declarative configuration #6953 @​JacksonWeber

:books: Documentation

:house: Internal

  • refactor(sampler-jaeger-remote): remove axios dependency and use fetch to get the sampler configuration from Jaeger API #6963 @​david-luna

experimental/v0.221.0

0.221.0

:boom: Breaking Changes

  • feat(sdk-logs)!: configure the force flush timeout per call #6931 @​LarryHu0217
    • (user-facing): LoggerProviderOptions.forceFlushTimeoutMillis has been removed; pass timeoutMillis to LoggerProvider.forceFlush() instead.
  • feat(instrumentation-http)!: emit only stable HTTP semantic conventions. The OTEL_SEMCONV_STABILITY_OPT_IN environment variable no longer changes HTTP attribute or metric emission — old (v1.7.0) and duplicate (http/http/dup) semconv outputs have been removed. #6819 @​maryliag
  • feat(instrumentation-fetch)!: emit only stable HTTP semantic conventions. The semconvStabilityOptIn instrumentation config option has been removed; old (v1.7.0) and duplicate semconv outputs are no longer emitted. #6819 @​maryliag
  • feat(instrumentation-xml-http-request)!: emit only stable HTTP semantic conventions. The semconvStabilityOptIn instrumentation config option has been removed; old (v1.7.0) and duplicate semconv outputs are no longer emitted. #6819 @​maryliag
  • feat(instrumentation-grpc)!: emit only stable network semantic conventions. The OTEL_SEMCONV_STABILITY_OPT_IN environment variable no longer changes attribute emission — net.peer.name and net.peer.port (old) are no longer set; only server.address and server.port (stable). #6819 @​maryliag

:rocket: Features

  • feat(sdk-logs): allow modifying ReadWriteLogRecord properties (including hrTime, hrTimeObserved, and spanContext) in accordance with the OpenTelemetry Logs specification #6923 @​Babul422

... (truncated)

Commits
  • 0b72a81 chore: prepare next release (#7044)
  • a9c5338 ci: roll prerelease changelog into one final release changelog (#7045)
  • f41805e chore: prepare next release (#7042)
  • b85eb28 chore(instrumentation-http): fix lint errors (#7039)
  • 3f92530 ci: support pre-releases and major version bumps in release workflow (#7035)
  • 82a5831 docs(otlp-exporter-base): document HTTP exporter options (#6735)
  • e086dec Merge commit from fork
  • 59dac70 chore(deps): update jamesives/github-pages-deploy-action action to v4.9.0 (#7...
  • d0ce753 chore: add @​maryliag to maintainers (#7024)
  • 03469a1 chore(deps): update open-telemetry/shared-workflows action to v0.10.0 (#7032)
  • Additional commits viewable in compare view

Updates `@opentelemetry/exporter-trace-otlp-proto` from 0.219.0 to 0.222.0
Release notes

Sourced from @​opentelemetry/exporter-trace-otlp-proto's releases.

experimental/v0.222.0

0.222.0

:boom: Breaking Changes

  • fix(sdk-node)!: fail-fast on Propagator creation from config file #6930 @​trentm
  • fix(sdk-node)!: fail-fast on MeterProvider creation from config file #6954 @​trentm
  • fix(sdk-node)!: fail-fast on TracerProvider creation from config file #6962 @​trentm
  • fix(sdk-node)!: fail-fast on Resource creation from config file #6989 @​trentm
    • This also breaks some usage of startNodeSDK() for environment-based config, i.e. when not using a config file. For example with OTEL_NODE_RESOURCE_DETECTORS=all, it results in an error message and a no-op SDK. (This does not impact users of new NodeSDK() -- the currently recommended mechanism to start an SDK using this package.)

      Could not create OpenTelemetry SDK from
      configuration, SDK will not be setup: unknown
      ExperimentalResourceDetector name in configuration:
      "container"
      

:rocket: Features

:bug: Bug Fixes

  • fix(instrumentation-http): redact sensitive query parameters on incoming (server) spans; add redactedQueryParamsServer config option @​dyladan
  • fix(sdk-node): support headers_list when creating OTLP exporters from declarative configuration #6953 @​JacksonWeber

:books: Documentation

:house: Internal

  • refactor(sampler-jaeger-remote): remove axios dependency and use fetch to get the sampler configuration from Jaeger API #6963 @​david-luna

experimental/v0.221.0

0.221.0

:boom: Breaking Changes

  • feat(sdk-logs)!: configure the force flush timeout per call #6931 @​LarryHu0217
    • (user-facing): LoggerProviderOptions.forceFlushTimeoutMillis has been removed; pass timeoutMillis to LoggerProvider.forceFlush() instead.
  • feat(instrumentation-http)!: emit only stable HTTP semantic conventions. The OTEL_SEMCONV_STABILITY_OPT_IN environment variable no longer changes HTTP attribute or metric emission — old (v1.7.0) and duplicate (http/http/dup) semconv outputs have been removed. #6819 @​maryliag
  • feat(instrumentation-fetch)!: emit only stable HTTP semantic conventions. The semconvStabilityOptIn instrumentation config option has been removed; old (v1.7.0) and duplicate semconv outputs are no longer emitted. #6819 @​maryliag
  • feat(instrumentation-xml-http-request)!: emit only stable HTTP semantic conventions. The semconvStabilityOptIn instrumentation config option has been removed; old (v1.7.0) and duplicate semconv outputs are no longer emitted. #6819 @​maryliag
  • feat(instrumentation-grpc)!: emit only stable network semantic conventions. The OTEL_SEMCONV_STABILITY_OPT_IN environment variable no longer changes attribute emission — net.peer.name and net.peer.port (old) are no longer set; only server.address and server.port (stable). #6819 @​maryliag

:rocket: Features

  • feat(sdk-logs): allow modifying ReadWriteLogRecord properties (including hrTime, hrTimeObserved, and spanContext) in accordance with the OpenTelemetry Logs specification #6923 @​Babul422

... (truncated)

Commits
  • 0b72a81 chore: prepare next release (#7044)
  • a9c5338 ci: roll prerelease changelog into one final release changelog (#7045)
  • f41805e chore: prepare next release (#7042)
  • b85eb28 chore(instrumentation-http): fix lint errors (#7039)
  • 3f92530 ci: support pre-releases and major version bumps in release workflow (#7035)
  • 82a5831 docs(otlp-exporter-base): document HTTP exporter options (#6735)
  • e086dec Merge commit from fork
  • 59dac70 chore(deps): update jamesives/github-pages-deploy-action action to v4.9.0 (#7...
  • d0ce753 chore: add @​maryliag to maintainers (#7024)
  • 03469a1 chore(deps): update open-telemetry/shared-workflows action to v0.10.0 (#7032)
  • Additional commits viewable in compare view

Updates `@opentelemetry/instrumentation-host-metrics` from 0.2.0 to 0.5.0
Release notes

Sourced from @​opentelemetry/instrumentation-host-metrics's releases.

instrumentation-host-metrics: v0.5.0

0.5.0 (2026-08-31)

Features

  • deps: update deps matching '@opentelemetry/*' (#3716) (015582a)

instrumentation-console: v0.4.0

0.4.0 (2026-08-31)

Features

  • deps: update deps matching '@opentelemetry/*' (#3716) (015582a)

Bug Fixes

  • instrumentation-console: restore console methods on disable when constructed with { enabled: true } (#3641) (97c7f21)

instrumentation-host-metrics: v0.4.0

0.4.0 (2026-07-23)

Features

  • deps: update deps matching '@opentelemetry/*' (#3629) (466d5de)

sampler-aws-xray: v0.3.0

0.3.0 (2026-07-23)

⚠ BREAKING CHANGES

  • only emit stable http, network and database attributes (#3585)

Features

  • only emit stable http, network and database attributes (#3585) (5b7dd0e)
Changelog

Sourced from @​opentelemetry/instrumentation-host-metrics's changelog.

0.5.0 (2026-08-31)

Features

  • deps: update deps matching '@opentelemetry/*' (#3716) (015582a)

0.4.0 (2026-07-23)

Features

  • deps: update deps matching '@opentelemetry/*' (#3629) (466d5de)

0.3.0 (2026-07-03)

Features

  • deps: update deps matching '@opentelemetry/*' (#3593) (6dfb532)
Commits
Maintainer changes

This version was pushed to npm by GitHub Actions, a new releaser for @​opentelemetry/instrumentation-host-metrics since your current version.


Updates `@opentelemetry/instrumentation-runtime-node` from 0.32.0 to 0.35.0
Changelog

Sourced from @​opentelemetry/instrumentation-runtime-node's changelog.

0.35.0 (2026-08-31)

Features

  • deps: update deps matching '@opentelemetry/*' (#3716) (015582a)

0.34.0 (2026-07-23)

⚠ BREAKING CHANGES

  • instrumentation-runtime-node: remove deprecated v8js.memory.heap.limit metric (#3632)

Features

  • deps: update deps matching '@opentelemetry/*' (#3629) (466d5de)
  • instrumentation-runtime-node: remove deprecated v8js.memory.heap.limit metric (#3632) (f698292)

0.33.0 (2026-07-03)

Features

  • deps: update deps matching '@opentelemetry/*' (#3593) (6dfb532)
  • instrumentation-runtime-node: add active resource gauge (#3361) (f3d38e2)
Commits

Updates `@sap/cds-mtxs` from 4.0.1 to 4.1.0 Updates `axios` from 1.18.1 to 1.20.0
Release notes

Sourced from axios's releases.

v1.20.0 — August 19, 2026

This release hardens runtime option handling, adds RFC 9110 status-code aliases, fixes Node.js and XHR reliability issues, and refreshes project tooling and documentation.

⚠️ Breaking Changes & Deprecations

  • HTTP Status Naming: Added ContentTooLarge (413) and UnprocessableContent (422), while retaining PayloadTooLarge and UnprocessableEntity as backward-compatible deprecated aliases. (#11082)

🔒 Security Fixes

  • Runtime Option Handling: Hardened behavioral configuration reads against shared and foreign prototype pollution and normalized unsafe interceptor replacement objects. This also clarifies Fetch redirect and custom implementation behavior, HTTP/2 DNS and proxy handling, CIDR-based NO_PROXY matching, and malformed data URI rejection; see the PR for documented compatibility effects. (#11141)

🐛 Bug Fixes

  • Interceptor Lifecycle: Prevented unbounded handler-array growth by trimming trailing ejected interceptors without changing iteration semantics, and kept interceptor operations safe when the public handlers field is nullish. (#11087, #11118)
  • Request Error Preservation: Prevented custom Error.prepareStackTrace implementations that return non-string values from replacing the original request failure with an unrelated TypeError. (#11109)
  • XHR Reliability: Navigation-canceled requests now reject with ECONNABORTED instead of resolving with status 0, while successful downloads flush their final progress callback during the live loadend dispatch. (#11094, #11121)
  • Node.js Socket Memory: Removed request-context retention from per-socket error listeners, preventing completed response data from being pinned for the lifetime of pooled keep-alive sockets. (#11091)
  • Core Methods and HTTP Errors: Prevented structural method-header buckets from leaking into outgoing headers, standardized invalid DNS lookup and httpVersion failures as AxiosError.ERR_BAD_OPTION_VALUE, and corrected the timeoutErrorMessage merge strategy. (#11096)

🔧 Maintenance & Chores

  • Dependencies: Updated fast-uri, postcss, js-yaml, mocha, development-tooling groups, and GitHub Actions dependencies. (#11092, #11098, #11099, #11106, #11107, #11122, #11123, #11126, #11127, #11133, #11140, #11143, #11144)
  • Documentation: Applied the v1.19.0 documentation updates, added the missing fs import to the README stream example, introduced localized global search, and repaired the interceptor test link. (#11101, #11113, #11097, #11119)
  • Sponsorship: Updated sponsorship links and data and added ScrapingBee as a sponsor. (#11124, #11136, #11137)
  • CI and Release: Switched ESM smoke tests to locked dependencies and synchronized package and runtime version metadata for v1.20.0. (#11128, #11152)

🌟 New Contributors

We are thrilled to welcome our new contributors. Thank you for helping improve axios:

Full Changelog (=20.6.0" @@ -439,17 +439,17 @@ } }, "node_modules/@opentelemetry/exporter-metrics-otlp-http": { - "version": "0.221.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.221.0.tgz", - "integrity": "sha512-sRfCKbOzgy8xZQV2as0RzIZlnCmCseCKZGLfRcrpo2CBngJDr+rPtX0zkG0+oUCV5kfQPUoW3W3C96Ag3Y/Clg==", + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-http/-/exporter-metrics-otlp-http-0.222.0.tgz", + "integrity": "sha512-6Ko+0bgNP6V4G/RsmNBSk5lO1B/lo9z6tvLpFY3ykPPevXNzFmOW3C9hEmTt0zKCvHovyrol3TYlg4HVb5gwjg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.10.0", - "@opentelemetry/otlp-exporter-base": "0.221.0", - "@opentelemetry/otlp-transformer": "0.221.0", - "@opentelemetry/resources": "2.10.0", - "@opentelemetry/sdk-metrics": "2.10.0" + "@opentelemetry/core": "2.11.0", + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-metrics": "2.11.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -458,16 +458,49 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/core": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", + "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", + "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { - "version": "0.221.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.221.0.tgz", - "integrity": "sha512-YMF4LveY2I3yhw61rn6nmC9FE8U24IZHPeKU1Duc5+sbwjMd8FwZAwba318ImdThCg/HuVQvhm2y6bfgNPnfYg==", + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.222.0.tgz", + "integrity": "sha512-s1ZPwS8Ig7cpRmL3kCobPaFbemM81dJ03LNPrsuN32h3mCDvNF2NQ1pRHruOju0P0o1TZ24LkZA685rtQal1KA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/exporter-metrics-otlp-http": "0.221.0", - "@opentelemetry/otlp-exporter-base": "0.221.0", - "@opentelemetry/otlp-transformer": "0.221.0" + "@opentelemetry/exporter-metrics-otlp-http": "0.222.0", + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -477,16 +510,16 @@ } }, "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.221.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.221.0.tgz", - "integrity": "sha512-zXminlZedtq9LvOW64CnNkOqk15zV75k8JgtdTuWFge6+jk2m4GmAUm6L2eIiG1o2a2bZxXw2PDrszm+bps0IA==", + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.222.0.tgz", + "integrity": "sha512-uisilePEdOMa3hIfXW82XXU6FofF8gw3iv+PoboIMWEUBC7XbJB3XnIHSdrSDsqCw3njR/If08qD0yzOY1U9Sg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/otlp-exporter-base": "0.221.0", - "@opentelemetry/otlp-grpc-exporter-base": "0.221.0", - "@opentelemetry/otlp-transformer": "0.221.0", - "@opentelemetry/sdk-trace": "2.10.0" + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/otlp-grpc-exporter-base": "0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0", + "@opentelemetry/sdk-trace": "2.11.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -495,16 +528,67 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/core": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", + "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/resources": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", + "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-trace": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.11.0.tgz", + "integrity": "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.221.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.221.0.tgz", - "integrity": "sha512-Z9i2T7vgZbWe9rSLYxXVIbeW+XyzUq4rZanW3ZyVNwVDqCsh0EJKUgBWWQ0CZfeuUA+RQPzKgJQHMuWAUnKqXw==", + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.222.0.tgz", + "integrity": "sha512-imLrLzNhnABJt3tUyT63h6MqfZrG3Gdv3TE7+OUQTvwD088RfCUx3rF01lcgfu2KXBFk13vwi0yKZLvFEalkIA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/otlp-exporter-base": "0.221.0", - "@opentelemetry/otlp-transformer": "0.221.0", - "@opentelemetry/sdk-trace": "2.10.0" + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0", + "@opentelemetry/sdk-trace": "2.11.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -513,6 +597,57 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", + "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", + "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-trace": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.11.0.tgz", + "integrity": "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/instrumentation": { "version": "0.221.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.221.0.tgz", @@ -531,13 +666,13 @@ } }, "node_modules/@opentelemetry/instrumentation-host-metrics": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-host-metrics/-/instrumentation-host-metrics-0.4.0.tgz", - "integrity": "sha512-jnFyX2sTn2B+9mjsL3qgAHzpxdaia4/FV2GSlf9QrpaiMi6O0M0lA9JZTsf4FTvuOwrIngCk+MeVXjsxgBXXyg==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-host-metrics/-/instrumentation-host-metrics-0.5.0.tgz", + "integrity": "sha512-rAw5lmY2u3bwUFohRtR4ZYNDnhMTiSeQqq8BB4Bo+aJQ+7vIOOwW/3d4awLPMDQ69VeA5jsh+ICL5oJinrWIwA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/instrumentation": "^0.221.0", + "@opentelemetry/instrumentation": "^0.222.0", "systeminformation": "^5.31.6" }, "engines": { @@ -547,6 +682,37 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-host-metrics/node_modules/@opentelemetry/api-logs": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.222.0.tgz", + "integrity": "sha512-9mb1If+IF6u0ZVXkHQ6ogEae5HwA6ajIVUgpSDQyRASxft6BSXHvBvPooRle3yFN/fKnCdSOnuu0OC3PLcF6+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-host-metrics/node_modules/@opentelemetry/instrumentation": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.222.0.tgz", + "integrity": "sha512-fhbRDuAgzPKpq1k5+hX3uS+3QAYNca0on0Ngot/R8dDsQtuFaADeZnG2uSgxABuvcI7S2phAaRhbct9jieeKZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.222.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-http": { "version": "0.221.0", "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.221.0.tgz", @@ -566,15 +732,46 @@ } }, "node_modules/@opentelemetry/instrumentation-runtime-node": { - "version": "0.34.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.34.0.tgz", - "integrity": "sha512-Yb3PcmuK/iIOWY49GSEcSGl7fR4r6UqhZnuy5EWvFaqKqqs9SYnQeyQUEAbnBnSougRpwFBDbdN1SSGcvMhbtQ==", + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-runtime-node/-/instrumentation-runtime-node-0.35.0.tgz", + "integrity": "sha512-/cOW4Qlv4nnr4QXDCkv0TEtOBa9R9lXHu//JuD5+nRPYR74g9iLmf5tadcYvn32oz2R8vQnV8FTnMLzjfAGMCQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "^0.221.0", + "@opentelemetry/api-logs": "^0.222.0", "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.221.0" + "@opentelemetry/instrumentation": "^0.222.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/api-logs": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.222.0.tgz", + "integrity": "sha512-9mb1If+IF6u0ZVXkHQ6ogEae5HwA6ajIVUgpSDQyRASxft6BSXHvBvPooRle3yFN/fKnCdSOnuu0OC3PLcF6+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/instrumentation": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.222.0.tgz", + "integrity": "sha512-fhbRDuAgzPKpq1k5+hX3uS+3QAYNca0on0Ngot/R8dDsQtuFaADeZnG2uSgxABuvcI7S2phAaRhbct9jieeKZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.222.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -601,14 +798,14 @@ } }, "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.221.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.221.0.tgz", - "integrity": "sha512-UFPIq80OH3Ns/oPFHRj14d4DTOxUo+MUFU8hUiCq5jTqFhdeJnfVSANHT+xp92409cA+oxzvlZCe6NM1wvCuBA==", + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.222.0.tgz", + "integrity": "sha512-YbywG3veEm2Fb6TbdxRkuquWob6eVWXuA8/Ba1tXz9jHfUqpdE3keilOHEtPboC4CvS1bjeeVfNkWGOOrLj+lw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.10.0", - "@opentelemetry/otlp-transformer": "0.221.0" + "@opentelemetry/core": "2.11.0", + "@opentelemetry/otlp-transformer": "0.222.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -617,17 +814,33 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", + "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.221.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.221.0.tgz", - "integrity": "sha512-rQDmNgyiGCTrescjnzH2ntVyUKVIq6I2UjuK8+stT/Xg0ZOT71FVJqwjFdspQl6Yol/Yqsut9bDo+ame8oTmDQ==", + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.222.0.tgz", + "integrity": "sha512-Kw5WqDBYYpCgY+edHWZS/Jp3bdguqNkCyEWOsqFhf8RmtcEuWYzPFA9i+L6UsNoJXTs8ngeQB6dwELH1czNYjA==", "dev": true, "license": "Apache-2.0", "dependencies": { "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.10.0", - "@opentelemetry/otlp-exporter-base": "0.221.0", - "@opentelemetry/otlp-transformer": "0.221.0" + "@opentelemetry/core": "2.11.0", + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -636,19 +849,35 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/core": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", + "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.221.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.221.0.tgz", - "integrity": "sha512-lg6lkOU08Az23jVcn/0Els9HP+V8PnR4Km6p0KgpTggS0n/WuhnmY64rSh83Of9iR9nD+dpWr6adlcX8KzAwjg==", + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.222.0.tgz", + "integrity": "sha512-/F3BZ89+CJQnZkMh2tCrtcdB+XT2Dxhj4FFE+WPQ//413hmFL0/RfEX6vgOIWGhiSzrkHWTK3+6SiT7K5/g/jQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.221.0", - "@opentelemetry/core": "2.10.0", - "@opentelemetry/resources": "2.10.0", - "@opentelemetry/sdk-logs": "0.221.0", - "@opentelemetry/sdk-metrics": "2.10.0", - "@opentelemetry/sdk-trace": "2.10.0" + "@opentelemetry/api-logs": "0.222.0", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-logs": "0.222.0", + "@opentelemetry/sdk-metrics": "2.11.0", + "@opentelemetry/sdk-trace": "2.11.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -657,6 +886,70 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/api-logs": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.222.0.tgz", + "integrity": "sha512-9mb1If+IF6u0ZVXkHQ6ogEae5HwA6ajIVUgpSDQyRASxft6BSXHvBvPooRle3yFN/fKnCdSOnuu0OC3PLcF6+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", + "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", + "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-trace": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.11.0.tgz", + "integrity": "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/resources": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz", @@ -674,15 +967,15 @@ } }, "node_modules/@opentelemetry/sdk-logs": { - "version": "0.221.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.221.0.tgz", - "integrity": "sha512-FaDcazjyMp7TZZZAsqbo4IkovP0UegoCu0EBkiNt+qCqvUf7FPAsfcrZ3+ZEkKgXZ/jHafop+JoGPDk3A0SmLg==", + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.222.0.tgz", + "integrity": "sha512-+19YHODIjaUCArxleaJtuufFZVpz/xvvK+VllQqE+W8hHolxdoRwHfK/s667zezwh1hkx6FFF+oYzetYgqK+Bg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.221.0", - "@opentelemetry/core": "2.10.0", - "@opentelemetry/resources": "2.10.0", + "@opentelemetry/api-logs": "0.222.0", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -692,14 +985,60 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/api-logs": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.222.0.tgz", + "integrity": "sha512-9mb1If+IF6u0ZVXkHQ6ogEae5HwA6ajIVUgpSDQyRASxft6BSXHvBvPooRle3yFN/fKnCdSOnuu0OC3PLcF6+g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", + "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", + "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-metrics": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.10.0.tgz", - "integrity": "sha512-t6r1VSvXNtSDnPXU1FbZeetJb7yyovHmgu0wRSoftxtE0g2rSNhQZQUy69sRUCL+iioJpX8SN/S6wq6ZtvLySQ==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.11.0.tgz", + "integrity": "sha512-7GXXcObyHyDUUSG+L+kJoquty01bzm7ivE7+SSgXXJcHuPzGviptxwARmI2c+bnnxjexGQbJnyNlN8HxBP/Y7A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.10.0", - "@opentelemetry/resources": "2.10.0" + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -708,6 +1047,37 @@ "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", + "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", + "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.11.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-trace": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", diff --git a/package.json b/package.json index 5d5bf088..cb53b1a1 100644 --- a/package.json +++ b/package.json @@ -39,12 +39,12 @@ "@cap-js/sqlite": "^3", "@cap-js/telemetry": "file:.", "@grpc/grpc-js": "^1.9.14", - "@opentelemetry/exporter-metrics-otlp-grpc": "^0.221", - "@opentelemetry/exporter-metrics-otlp-proto": "^0.221", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.221", - "@opentelemetry/exporter-trace-otlp-proto": "^0.221", - "@opentelemetry/instrumentation-host-metrics": "^0.4.0", - "@opentelemetry/instrumentation-runtime-node": "^0.34.0", + "@opentelemetry/exporter-metrics-otlp-grpc": "^0.222", + "@opentelemetry/exporter-metrics-otlp-proto": "^0.222", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.222", + "@opentelemetry/exporter-trace-otlp-proto": "^0.222", + "@opentelemetry/instrumentation-host-metrics": "^0.5.0", + "@opentelemetry/instrumentation-runtime-node": "^0.35.0", "@sap-cloud-sdk/http-client": "^4", "@sap/cds-mtxs": "^4", "axios": "^1.6.7", From f5c6a4b9b021ded032072aa6ed2fd5ade33bb44d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 00:44:27 +0200 Subject: [PATCH 24/31] chore(deps): bump the prod-dependencies group across 1 directory with 8 updates (#499) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the prod-dependencies group with 8 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@opentelemetry/core](https://github.com/open-telemetry/opentelemetry-js) | `2.10.0` | `2.11.0` | | [@opentelemetry/instrumentation](https://github.com/open-telemetry/opentelemetry-js) | `0.221.0` | `0.222.0` | | [@opentelemetry/instrumentation-http](https://github.com/open-telemetry/opentelemetry-js) | `0.221.0` | `0.222.0` | | [@opentelemetry/instrumentation-undici](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/HEAD/packages/instrumentation-undici) | `0.31.0` | `0.32.0` | | [@opentelemetry/resources](https://github.com/open-telemetry/opentelemetry-js) | `2.10.0` | `2.11.0` | | [@opentelemetry/sdk-metrics](https://github.com/open-telemetry/opentelemetry-js) | `2.10.0` | `2.11.0` | | [@opentelemetry/sdk-trace-base](https://github.com/open-telemetry/opentelemetry-js) | `2.10.0` | `2.11.0` | | [@opentelemetry/sdk-trace-node](https://github.com/open-telemetry/opentelemetry-js) | `2.10.0` | `2.11.0` | Updates `@opentelemetry/core` from 2.10.0 to 2.11.0

Release notes

Sourced from @​opentelemetry/core's releases.

v2.11.0

2.11.0

:rocket: Features

  • feat(context-async-hooks): implement attach() on AsyncLocalStorageContextManager #6845 @​pichlermarc
    • On Node.js 25.9+, delegates to AsyncLocalStorage.withScope() returning a native RunScope. On older Node.js, falls back to enterWith() with a manual disposable wrapper.
  • feat(sdk-trace): allow configuring the force flush timeout per call #6929 @​LarryHu0217

:bug: Bug Fixes

  • fix(sdk-metrics): ignore Infinity in exponential histograms #7015 @​mwear

:house: Internal

  • perf(sdk-metrics): reuse a single DataView for exponential histogram bit reads #6998 @​mwear
  • chore(ci): run documentation tests on a weekly schedule #6920 @​LarryHu0217
  • feat(ci): support pre-releases and major version bumps in the release workflow #6768 @​pichlermarc
  • chore(resources): Ensure that multiple uses of serviceInstanceIdDetector.detect() return the same value for service.instance.id
Changelog

Sourced from @​opentelemetry/core's changelog.

2.11.0

:rocket: Features

  • feat(context-async-hooks): implement attach() on AsyncLocalStorageContextManager #6845 @​pichlermarc
    • On Node.js 25.9+, delegates to AsyncLocalStorage.withScope() returning a native RunScope. On older Node.js, falls back to enterWith() with a manual disposable wrapper.
  • feat(sdk-trace): allow configuring the force flush timeout per call #6929 @​LarryHu0217

:bug: Bug Fixes

  • fix(sdk-trace-base): avoid a Webpack self-reference error in CommonJS output #6981 @​sansynx
  • fix(sdk-metrics): ignore Infinity in exponential histograms #7015 @​mwear

:house: Internal

  • perf(sdk-metrics): reuse a single DataView for exponential histogram bit reads #6998 @​mwear
  • chore(ci): run documentation tests on a weekly schedule #6920 @​LarryHu0217
  • feat(ci): support pre-releases and major version bumps in the release workflow #6768 @​pichlermarc
  • chore(resources): Ensure that multiple uses of serviceInstanceIdDetector.detect() return the same value for service.instance.id
Commits
  • 0b72a81 chore: prepare next release (#7044)
  • a9c5338 ci: roll prerelease changelog into one final release changelog (#7045)
  • f41805e chore: prepare next release (#7042)
  • b85eb28 chore(instrumentation-http): fix lint errors (#7039)
  • 3f92530 ci: support pre-releases and major version bumps in release workflow (#7035)
  • 82a5831 docs(otlp-exporter-base): document HTTP exporter options (#6735)
  • e086dec Merge commit from fork
  • 59dac70 chore(deps): update jamesives/github-pages-deploy-action action to v4.9.0 (#7...
  • d0ce753 chore: add @​maryliag to maintainers (#7024)
  • 03469a1 chore(deps): update open-telemetry/shared-workflows action to v0.10.0 (#7032)
  • Additional commits viewable in compare view

Updates `@opentelemetry/instrumentation` from 0.221.0 to 0.222.0
Release notes

Sourced from @​opentelemetry/instrumentation's releases.

experimental/v0.222.0

0.222.0

:boom: Breaking Changes

  • fix(sdk-node)!: fail-fast on Propagator creation from config file #6930 @​trentm
  • fix(sdk-node)!: fail-fast on MeterProvider creation from config file #6954 @​trentm
  • fix(sdk-node)!: fail-fast on TracerProvider creation from config file #6962 @​trentm
  • fix(sdk-node)!: fail-fast on Resource creation from config file #6989 @​trentm
    • This also breaks some usage of startNodeSDK() for environment-based config, i.e. when not using a config file. For example with OTEL_NODE_RESOURCE_DETECTORS=all, it results in an error message and a no-op SDK. (This does not impact users of new NodeSDK() -- the currently recommended mechanism to start an SDK using this package.)

      Could not create OpenTelemetry SDK from
      configuration, SDK will not be setup: unknown
      ExperimentalResourceDetector name in configuration:
      "container"
      

:rocket: Features

:bug: Bug Fixes

  • fix(instrumentation-http): redact sensitive query parameters on incoming (server) spans; add redactedQueryParamsServer config option @​dyladan
  • fix(sdk-node): support headers_list when creating OTLP exporters from declarative configuration #6953 @​JacksonWeber

:books: Documentation

:house: Internal

  • refactor(sampler-jaeger-remote): remove axios dependency and use fetch to get the sampler configuration from Jaeger API #6963 @​david-luna
Commits
  • 0b72a81 chore: prepare next release (#7044)
  • a9c5338 ci: roll prerelease changelog into one final release changelog (#7045)
  • f41805e chore: prepare next release (#7042)
  • b85eb28 chore(instrumentation-http): fix lint errors (#7039)
  • 3f92530 ci: support pre-releases and major version bumps in release workflow (#7035)
  • 82a5831 docs(otlp-exporter-base): document HTTP exporter options (#6735)
  • e086dec Merge commit from fork
  • 59dac70 chore(deps): update jamesives/github-pages-deploy-action action to v4.9.0 (#7...
  • d0ce753 chore: add @​maryliag to maintainers (#7024)
  • 03469a1 chore(deps): update open-telemetry/shared-workflows action to v0.10.0 (#7032)
  • Additional commits viewable in compare view

Updates `@opentelemetry/instrumentation-http` from 0.221.0 to 0.222.0
Release notes

Sourced from @​opentelemetry/instrumentation-http's releases.

experimental/v0.222.0

0.222.0

:boom: Breaking Changes

  • fix(sdk-node)!: fail-fast on Propagator creation from config file #6930 @​trentm
  • fix(sdk-node)!: fail-fast on MeterProvider creation from config file #6954 @​trentm
  • fix(sdk-node)!: fail-fast on TracerProvider creation from config file #6962 @​trentm
  • fix(sdk-node)!: fail-fast on Resource creation from config file #6989 @​trentm
    • This also breaks some usage of startNodeSDK() for environment-based config, i.e. when not using a config file. For example with OTEL_NODE_RESOURCE_DETECTORS=all, it results in an error message and a no-op SDK. (This does not impact users of new NodeSDK() -- the currently recommended mechanism to start an SDK using this package.)

      Could not create OpenTelemetry SDK from
      configuration, SDK will not be setup: unknown
      ExperimentalResourceDetector name in configuration:
      "container"
      

:rocket: Features

:bug: Bug Fixes

  • fix(instrumentation-http): redact sensitive query parameters on incoming (server) spans; add redactedQueryParamsServer config option @​dyladan
  • fix(sdk-node): support headers_list when creating OTLP exporters from declarative configuration #6953 @​JacksonWeber

:books: Documentation

:house: Internal

  • refactor(sampler-jaeger-remote): remove axios dependency and use fetch to get the sampler configuration from Jaeger API #6963 @​david-luna
Commits
  • 0b72a81 chore: prepare next release (#7044)
  • a9c5338 ci: roll prerelease changelog into one final release changelog (#7045)
  • f41805e chore: prepare next release (#7042)
  • b85eb28 chore(instrumentation-http): fix lint errors (#7039)
  • 3f92530 ci: support pre-releases and major version bumps in release workflow (#7035)
  • 82a5831 docs(otlp-exporter-base): document HTTP exporter options (#6735)
  • e086dec Merge commit from fork
  • 59dac70 chore(deps): update jamesives/github-pages-deploy-action action to v4.9.0 (#7...
  • d0ce753 chore: add @​maryliag to maintainers (#7024)
  • 03469a1 chore(deps): update open-telemetry/shared-workflows action to v0.10.0 (#7032)
  • Additional commits viewable in compare view

Updates `@opentelemetry/instrumentation-undici` from 0.31.0 to 0.32.0
Release notes

Sourced from @​opentelemetry/instrumentation-undici's releases.

instrumentation-undici: v0.32.0

0.32.0 (2026-08-31)

Features

  • deps: update deps matching '@opentelemetry/*' (#3716) (015582a)

Bug Fixes

  • instrumentation-undici: use low-cardinality error.type (#3659) (3756dfa)
Changelog

Sourced from @​opentelemetry/instrumentation-undici's changelog.

0.32.0 (2026-08-31)

Features

  • deps: update deps matching '@opentelemetry/*' (#3716) (015582a)

Bug Fixes

  • instrumentation-undici: use low-cardinality error.type (#3659) (3756dfa)
Commits

Updates `@opentelemetry/resources` from 2.10.0 to 2.11.0
Release notes

Sourced from @​opentelemetry/resources's releases.

v2.11.0

2.11.0

:rocket: Features

  • feat(context-async-hooks): implement attach() on AsyncLocalStorageContextManager #6845 @​pichlermarc
    • On Node.js 25.9+, delegates to AsyncLocalStorage.withScope() returning a native RunScope. On older Node.js, falls back to enterWith() with a manual disposable wrapper.
  • feat(sdk-trace): allow configuring the force flush timeout per call #6929 @​LarryHu0217

:bug: Bug Fixes

  • fix(sdk-metrics): ignore Infinity in exponential histograms #7015 @​mwear

:house: Internal

  • perf(sdk-metrics): reuse a single DataView for exponential histogram bit reads #6998 @​mwear
  • chore(ci): run documentation tests on a weekly schedule #6920 @​LarryHu0217
  • feat(ci): support pre-releases and major version bumps in the release workflow #6768 @​pichlermarc
  • chore(resources): Ensure that multiple uses of serviceInstanceIdDetector.detect() return the same value for service.instance.id
Changelog

Sourced from @​opentelemetry/resources's changelog.

2.11.0

:rocket: Features

  • feat(context-async-hooks): implement attach() on AsyncLocalStorageContextManager #6845 @​pichlermarc
    • On Node.js 25.9+, delegates to AsyncLocalStorage.withScope() returning a native RunScope. On older Node.js, falls back to enterWith() with a manual disposable wrapper.
  • feat(sdk-trace): allow configuring the force flush timeout per call #6929 @​LarryHu0217

:bug: Bug Fixes

  • fix(sdk-trace-base): avoid a Webpack self-reference error in CommonJS output #6981 @​sansynx
  • fix(sdk-metrics): ignore Infinity in exponential histograms #7015 @​mwear

:house: Internal

  • perf(sdk-metrics): reuse a single DataView for exponential histogram bit reads #6998 @​mwear
  • chore(ci): run documentation tests on a weekly schedule #6920 @​LarryHu0217
  • feat(ci): support pre-releases and major version bumps in the release workflow #6768 @​pichlermarc
  • chore(resources): Ensure that multiple uses of serviceInstanceIdDetector.detect() return the same value for service.instance.id
Commits
  • 0b72a81 chore: prepare next release (#7044)
  • a9c5338 ci: roll prerelease changelog into one final release changelog (#7045)
  • f41805e chore: prepare next release (#7042)
  • b85eb28 chore(instrumentation-http): fix lint errors (#7039)
  • 3f92530 ci: support pre-releases and major version bumps in release workflow (#7035)
  • 82a5831 docs(otlp-exporter-base): document HTTP exporter options (#6735)
  • e086dec Merge commit from fork
  • 59dac70 chore(deps): update jamesives/github-pages-deploy-action action to v4.9.0 (#7...
  • d0ce753 chore: add @​maryliag to maintainers (#7024)
  • 03469a1 chore(deps): update open-telemetry/shared-workflows action to v0.10.0 (#7032)
  • Additional commits viewable in compare view

Updates `@opentelemetry/sdk-metrics` from 2.10.0 to 2.11.0
Release notes

Sourced from @​opentelemetry/sdk-metrics's releases.

v2.11.0

2.11.0

:rocket: Features

  • feat(context-async-hooks): implement attach() on AsyncLocalStorageContextManager #6845 @​pichlermarc
    • On Node.js 25.9+, delegates to AsyncLocalStorage.withScope() returning a native RunScope. On older Node.js, falls back to enterWith() with a manual disposable wrapper.
  • feat(sdk-trace): allow configuring the force flush timeout per call #6929 @​LarryHu0217

:bug: Bug Fixes

  • fix(sdk-metrics): ignore Infinity in exponential histograms #7015 @​mwear

:house: Internal

  • perf(sdk-metrics): reuse a single DataView for exponential histogram bit reads #6998 @​mwear
  • chore(ci): run documentation tests on a weekly schedule #6920 @​LarryHu0217
  • feat(ci): support pre-releases and major version bumps in the release workflow #6768 @​pichlermarc
  • chore(resources): Ensure that multiple uses of serviceInstanceIdDetector.detect() return the same value for service.instance.id
Changelog

Sourced from @​opentelemetry/sdk-metrics's changelog.

2.11.0

:rocket: Features

  • feat(context-async-hooks): implement attach() on AsyncLocalStorageContextManager #6845 @​pichlermarc
    • On Node.js 25.9+, delegates to AsyncLocalStorage.withScope() returning a native RunScope. On older Node.js, falls back to enterWith() with a manual disposable wrapper.
  • feat(sdk-trace): allow configuring the force flush timeout per call #6929 @​LarryHu0217

:bug: Bug Fixes

  • fix(sdk-trace-base): avoid a Webpack self-reference error in CommonJS output #6981 @​sansynx
  • fix(sdk-metrics): ignore Infinity in exponential histograms #7015 @​mwear

:house: Internal

  • perf(sdk-metrics): reuse a single DataView for exponential histogram bit reads #6998 @​mwear
  • chore(ci): run documentation tests on a weekly schedule #6920 @​LarryHu0217
  • feat(ci): support pre-releases and major version bumps in the release workflow #6768 @​pichlermarc
  • chore(resources): Ensure that multiple uses of serviceInstanceIdDetector.detect() return the same value for service.instance.id
Commits
  • 0b72a81 chore: prepare next release (#7044)
  • a9c5338 ci: roll prerelease changelog into one final release changelog (#7045)
  • f41805e chore: prepare next release (#7042)
  • b85eb28 chore(instrumentation-http): fix lint errors (#7039)
  • 3f92530 ci: support pre-releases and major version bumps in release workflow (#7035)
  • 82a5831 docs(otlp-exporter-base): document HTTP exporter options (#6735)
  • e086dec Merge commit from fork
  • 59dac70 chore(deps): update jamesives/github-pages-deploy-action action to v4.9.0 (#7...
  • d0ce753 chore: add @​maryliag to maintainers (#7024)
  • 03469a1 chore(deps): update open-telemetry/shared-workflows action to v0.10.0 (#7032)
  • Additional commits viewable in compare view

Updates `@opentelemetry/sdk-trace-base` from 2.10.0 to 2.11.0
Release notes

Sourced from @​opentelemetry/sdk-trace-base's releases.

v2.11.0

2.11.0

:rocket: Features

  • feat(context-async-hooks): implement attach() on AsyncLocalStorageContextManager #6845 @​pichlermarc
    • On Node.js 25.9+, delegates to AsyncLocalStorage.withScope() returning a native RunScope. On older Node.js, falls back to enterWith() with a manual disposable wrapper.
  • feat(sdk-trace): allow configuring the force flush timeout per call #6929 @​LarryHu0217

:bug: Bug Fixes

  • fix(sdk-metrics): ignore Infinity in exponential histograms #7015 @​mwear

:house: Internal

  • perf(sdk-metrics): reuse a single DataView for exponential histogram bit reads #6998 @​mwear
  • chore(ci): run documentation tests on a weekly schedule #6920 @​LarryHu0217
  • feat(ci): support pre-releases and major version bumps in the release workflow #6768 @​pichlermarc
  • chore(resources): Ensure that multiple uses of serviceInstanceIdDetector.detect() return the same value for service.instance.id
Changelog

Sourced from @​opentelemetry/sdk-trace-base's changelog.

2.11.0

:rocket: Features

  • feat(context-async-hooks): implement attach() on AsyncLocalStorageContextManager #6845 @​pichlermarc
    • On Node.js 25.9+, delegates to AsyncLocalStorage.withScope() returning a native RunScope. On older Node.js, falls back to enterWith() with a manual disposable wrapper.
  • feat(sdk-trace): allow configuring the force flush timeout per call #6929 @​LarryHu0217

:bug: Bug Fixes

  • fix(sdk-trace-base): avoid a Webpack self-reference error in CommonJS output #6981 @​sansynx
  • fix(sdk-metrics): ignore Infinity in exponential histograms #7015 @​mwear

:house: Internal

  • perf(sdk-metrics): reuse a single DataView for exponential histogram bit reads #6998 @​mwear
  • chore(ci): run documentation tests on a weekly schedule #6920 @​LarryHu0217
  • feat(ci): support pre-releases and major version bumps in the release workflow #6768 @​pichlermarc
  • chore(resources): Ensure that multiple uses of serviceInstanceIdDetector.detect() return the same value for service.instance.id
Commits
  • 0b72a81 chore: prepare next release (#7044)
  • a9c5338 ci: roll prerelease changelog into one final release changelog (#7045)
  • f41805e chore: prepare next release (#7042)
  • b85eb28 chore(instrumentation-http): fix lint errors (#7039)
  • 3f92530 ci: support pre-releases and major version bumps in release workflow (#7035)
  • 82a5831 docs(otlp-exporter-base): document HTTP exporter options (#6735)
  • e086dec Merge commit from fork
  • 59dac70 chore(deps): update jamesives/github-pages-deploy-action action to v4.9.0 (#7...
  • d0ce753 chore: add @​maryliag to maintainers (#7024)
  • 03469a1 chore(deps): update open-telemetry/shared-workflows action to v0.10.0 (#7032)
  • Additional commits viewable in compare view

Updates `@opentelemetry/sdk-trace-node` from 2.10.0 to 2.11.0
Release notes

Sourced from @​opentelemetry/sdk-trace-node's releases.

v2.11.0

2.11.0

:rocket: Features

  • feat(context-async-hooks): implement attach() on AsyncLocalStorageContextManager #6845 @​pichlermarc
    • On Node.js 25.9+, delegates to AsyncLocalStorage.withScope() returning a native RunScope. On older Node.js, falls back to enterWith() with a manual disposable wrapper.
  • feat(sdk-trace): allow configuring the force flush timeout per call #6929 @​LarryHu0217

:bug: Bug Fixes

  • fix(sdk-metrics): ignore Infinity in exponential histograms #7015 @​mwear

:house: Internal

  • perf(sdk-metrics): reuse a single DataView for exponential histogram bit reads #6998 @​mwear
  • chore(ci): run documentation tests on a weekly schedule #6920 @​LarryHu0217
  • feat(ci): support pre-releases and major version bumps in the release workflow #6768 @​pichlermarc
  • chore(resources): Ensure that multiple uses of serviceInstanceIdDetector.detect() return the same value for service.instance.id
Changelog

Sourced from @​opentelemetry/sdk-trace-node's changelog.

2.11.0

:rocket: Features

  • feat(context-async-hooks): implement attach() on AsyncLocalStorageContextManager #6845 @​pichlermarc
    • On Node.js 25.9+, delegates to AsyncLocalStorage.withScope() returning a native RunScope. On older Node.js, falls back to enterWith() with a manual disposable wrapper.
  • feat(sdk-trace): allow configuring the force flush timeout per call #6929 @​LarryHu0217

:bug: Bug Fixes

  • fix(sdk-trace-base): avoid a Webpack self-reference error in CommonJS output #6981 @​sansynx
  • fix(sdk-metrics): ignore Infinity in exponential histograms #7015 @​mwear

:house: Internal

  • perf(sdk-metrics): reuse a single DataView for exponential histogram bit reads #6998 @​mwear
  • chore(ci): run documentation tests on a weekly schedule #6920 @​LarryHu0217
  • feat(ci): support pre-releases and major version bumps in the release workflow #6768 @​pichlermarc
  • chore(resources): Ensure that multiple uses of serviceInstanceIdDetector.detect() return the same value for service.instance.id
Commits
  • 0b72a81 chore: prepare next release (#7044)
  • a9c5338 ci: roll prerelease changelog into one final release changelog (#7045)
  • f41805e chore: prepare next release (#7042)
  • b85eb28 chore(instrumentation-http): fix lint errors (#7039)
  • 3f92530 ci: support pre-releases and major version bumps in release workflow (#7035)
  • 82a5831 docs(otlp-exporter-base): document HTTP exporter options (#6735)
  • e086dec Merge commit from fork
  • 59dac70 chore(deps): update jamesives/github-pages-deploy-action action to v4.9.0 (#7...
  • d0ce753 chore: add @​maryliag to maintainers (#7024)
  • 03469a1 chore(deps): update open-telemetry/shared-workflows action to v0.10.0 (#7032)
  • Additional commits viewable in compare view

--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Sebastian Van Syckel --- package-lock.json | 1546 +++++++++++++++++++-------------------------- package.json | 6 +- 2 files changed, 644 insertions(+), 908 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0dc76855..2b172bfe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3,7 +3,6 @@ "version": "2.1.0", "lockfileVersion": 3, "requires": true, - "dev": true, "packages": { "": { "name": "@cap-js/telemetry", @@ -12,9 +11,9 @@ "dependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/core": "^2.8", - "@opentelemetry/instrumentation": "^0.221", - "@opentelemetry/instrumentation-http": "^0.221", - "@opentelemetry/instrumentation-undici": "^0.31", + "@opentelemetry/instrumentation": "^0.222", + "@opentelemetry/instrumentation-http": "^0.222", + "@opentelemetry/instrumentation-undici": "^0.32", "@opentelemetry/resources": "^2.8", "@opentelemetry/sdk-metrics": "^2.8", "@opentelemetry/sdk-trace-base": "^2.8", @@ -43,6 +42,30 @@ "@sap/cds": "^10 || ^9" } }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, "node_modules/@cap-js/cds-test": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@cap-js/cds-test/-/cds-test-1.0.2.tgz", @@ -63,9 +86,9 @@ } }, "node_modules/@cap-js/db-service": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@cap-js/db-service/-/db-service-3.0.1.tgz", - "integrity": "sha512-sWy+EYyfY7YzJspKcGqln4gWNVffcRwd/vm47T+tMa85+LWtOsmJgdfH8i1KVCR3o8zaywmPwSflnnSOKoZJkg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@cap-js/db-service/-/db-service-3.1.0.tgz", + "integrity": "sha512-CabYQlrT8O2n8aWnvjBhk94KlkpwowUzqnxwJFtokEBiHIVemu1+Kfvoo1djwpX6MB/4TV2j3xQf0DnLiLMUeA==", "dev": true, "license": "Apache-2.0", "peerDependencies": { @@ -79,17 +102,17 @@ } }, "node_modules/@cap-js/sqlite": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@cap-js/sqlite/-/sqlite-3.0.2.tgz", - "integrity": "sha512-C+wiMzRxgmNF919ZcIjXPlAGlgunXUzPAldp5WMZ4fmb+komfADMmdUzYqS1LklnBXBkdxrKlP30giNTutPwpg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@cap-js/sqlite/-/sqlite-3.1.0.tgz", + "integrity": "sha512-wcTMQqaGlIq+WkuvQ/p7+I9xoCUGxzYF8TFnrtl78MClBukTrRBWc29xvwznFlifJUVdF3NVhcIabep5+ancHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@cap-js/db-service": "^3.0.1" + "@cap-js/db-service": "^3.1.0" }, "peerDependencies": { "@sap/cds": "^10", - "better-sqlite3": "^12.0.0", + "better-sqlite3": "^12.0.0 || ^13.0.0", "sql.js": "^1.13.0" }, "peerDependenciesMeta": { @@ -116,9 +139,9 @@ } }, "node_modules/@dabh/diagnostics": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", - "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.9.tgz", + "integrity": "sha512-R6siwR65Hm+3yfgP7o8DKhNvputQAwfoz9zTc3kyDudnomj2/BcLmD+uGQQPICjuFUp8ounPBU+jmKsocwVVAg==", "dev": true, "license": "MIT", "dependencies": { @@ -242,9 +265,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", - "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", "devOptional": true, "license": "Apache-2.0", "dependencies": { @@ -355,9 +378,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "dev": true, "license": "MIT" }, @@ -372,6 +395,30 @@ "url": "https://opencollective.com/js-sdsl" } }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@opentelemetry/api": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", @@ -382,9 +429,9 @@ } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.221.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.221.0.tgz", - "integrity": "sha512-OlanaW1vv7ufTqQ3/fPLI4arGt5ZoM+P8abOMki6uEYnpRazepSWDwDnnw+la7kE26SHVC18//SMccrDvLKOXQ==", + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.222.0.tgz", + "integrity": "sha512-9mb1If+IF6u0ZVXkHQ6ogEae5HwA6ajIVUgpSDQyRASxft6BSXHvBvPooRle3yFN/fKnCdSOnuu0OC3PLcF6+g==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -394,9 +441,9 @@ } }, "node_modules/@opentelemetry/context-async-hooks": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.10.0.tgz", - "integrity": "sha512-bvyMcgLEkozzSzpEEEo1OMoeQ97bxj6Qs2uN3mPrSdDvObMI1myffD/BPqcLlzZO9//d1SqQA/WPw7Cz2AiqhA==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.11.0.tgz", + "integrity": "sha512-Tr79DyWI8itsBdg+jH+opjfrwLzX+erk1/ExkIwhWoAVjVrJIn2y5+cGjTC0Vy8fyNIA/y8wuJPZwr1T3xCZeQ==", "license": "Apache-2.0", "engines": { "node": "^18.19.0 || >=20.6.0" @@ -406,9 +453,9 @@ } }, "node_modules/@opentelemetry/core": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.10.0.tgz", - "integrity": "sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", + "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" @@ -458,39 +505,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/core": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", - "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", - "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.11.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-metrics-otlp-proto": { "version": "0.222.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-metrics-otlp-proto/-/exporter-metrics-otlp-proto-0.222.0.tgz", @@ -528,57 +542,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/core": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", - "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/resources": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", - "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.11.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/sdk-trace": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.11.0.tgz", - "integrity": "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.11.0", - "@opentelemetry/resources": "2.11.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-trace-otlp-proto": { "version": "0.222.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.222.0.tgz", @@ -597,64 +560,13 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", - "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/resources": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", - "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.11.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/sdk-trace": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.11.0.tgz", - "integrity": "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.11.0", - "@opentelemetry/resources": "2.11.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, "node_modules/@opentelemetry/instrumentation": { - "version": "0.221.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.221.0.tgz", - "integrity": "sha512-cCk80Z/iRDf/5gfsKMB4f74LqVA5yKETB/9ojPzVW/6/f70iu89nJvGxsFCxx4XfSohaOofkU19kiYm84AiAlw==", + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.222.0.tgz", + "integrity": "sha512-fhbRDuAgzPKpq1k5+hX3uS+3QAYNca0on0Ngot/R8dDsQtuFaADeZnG2uSgxABuvcI7S2phAaRhbct9jieeKZw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.221.0", + "@opentelemetry/api-logs": "0.222.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, @@ -682,45 +594,14 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-host-metrics/node_modules/@opentelemetry/api-logs": { - "version": "0.222.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.222.0.tgz", - "integrity": "sha512-9mb1If+IF6u0ZVXkHQ6ogEae5HwA6ajIVUgpSDQyRASxft6BSXHvBvPooRle3yFN/fKnCdSOnuu0OC3PLcF6+g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-host-metrics/node_modules/@opentelemetry/instrumentation": { - "version": "0.222.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.222.0.tgz", - "integrity": "sha512-fhbRDuAgzPKpq1k5+hX3uS+3QAYNca0on0Ngot/R8dDsQtuFaADeZnG2uSgxABuvcI7S2phAaRhbct9jieeKZw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.222.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, "node_modules/@opentelemetry/instrumentation-http": { - "version": "0.221.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.221.0.tgz", - "integrity": "sha512-oIP91CPIANuYr09tGFElPFKAh6JUar+awJf1kBRYlaeo9b0gDwZHEB2zBfFlvdNFHm0wAVutMZODVi5smKT30g==", + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.222.0.tgz", + "integrity": "sha512-HhMHW32LyGzIJEmUElxaML4oUe6JiCq0Hvjt/Awxb60SlMsSFetFLFjed45g/0wrPr1n7z+xW+x1B/584WBAkg==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.10.0", - "@opentelemetry/instrumentation": "0.221.0", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/instrumentation": "0.222.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, @@ -749,45 +630,14 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/api-logs": { - "version": "0.222.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.222.0.tgz", - "integrity": "sha512-9mb1If+IF6u0ZVXkHQ6ogEae5HwA6ajIVUgpSDQyRASxft6BSXHvBvPooRle3yFN/fKnCdSOnuu0OC3PLcF6+g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/instrumentation-runtime-node/node_modules/@opentelemetry/instrumentation": { - "version": "0.222.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.222.0.tgz", - "integrity": "sha512-fhbRDuAgzPKpq1k5+hX3uS+3QAYNca0on0Ngot/R8dDsQtuFaADeZnG2uSgxABuvcI7S2phAaRhbct9jieeKZw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.222.0", - "import-in-the-middle": "^3.0.0", - "require-in-the-middle": "^8.0.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, "node_modules/@opentelemetry/instrumentation-undici": { - "version": "0.31.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.31.0.tgz", - "integrity": "sha512-qunCfgSFV+bjRdAYkWIjVX38jIN/Xj80CiERXXdzYAmdigCFvJPB3AY3j43bjJlkhgbJJSCGdbvQUSut8QIiyQ==", + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.32.0.tgz", + "integrity": "sha512-Ms8oPkrCHa6hJOYodcFC8QOt1AFlns3KD1CqXBEihXyTFeLXDO84tIXYyKlXNHr8jMrl1Gm2velDvsSDXAuh9w==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "^2.9.0", - "@opentelemetry/instrumentation": "^0.221.0", + "@opentelemetry/instrumentation": "^0.222.0", "@opentelemetry/semantic-conventions": "^1.24.0" }, "engines": { @@ -800,292 +650,119 @@ "node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.222.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.222.0.tgz", - "integrity": "sha512-YbywG3veEm2Fb6TbdxRkuquWob6eVWXuA8/Ba1tXz9jHfUqpdE3keilOHEtPboC4CvS1bjeeVfNkWGOOrLj+lw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.11.0", - "@opentelemetry/otlp-transformer": "0.222.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", - "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.222.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.222.0.tgz", - "integrity": "sha512-Kw5WqDBYYpCgY+edHWZS/Jp3bdguqNkCyEWOsqFhf8RmtcEuWYzPFA9i+L6UsNoJXTs8ngeQB6dwELH1czNYjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@grpc/grpc-js": "^1.14.3", - "@opentelemetry/core": "2.11.0", - "@opentelemetry/otlp-exporter-base": "0.222.0", - "@opentelemetry/otlp-transformer": "0.222.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/core": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", - "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.222.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.222.0.tgz", - "integrity": "sha512-/F3BZ89+CJQnZkMh2tCrtcdB+XT2Dxhj4FFE+WPQ//413hmFL0/RfEX6vgOIWGhiSzrkHWTK3+6SiT7K5/g/jQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.222.0", - "@opentelemetry/core": "2.11.0", - "@opentelemetry/resources": "2.11.0", - "@opentelemetry/sdk-logs": "0.222.0", - "@opentelemetry/sdk-metrics": "2.11.0", - "@opentelemetry/sdk-trace": "2.11.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/api-logs": { - "version": "0.222.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.222.0.tgz", - "integrity": "sha512-9mb1If+IF6u0ZVXkHQ6ogEae5HwA6ajIVUgpSDQyRASxft6BSXHvBvPooRle3yFN/fKnCdSOnuu0OC3PLcF6+g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", - "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", - "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.11.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-trace": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.11.0.tgz", - "integrity": "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.11.0", - "@opentelemetry/resources": "2.11.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/resources": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.10.0.tgz", - "integrity": "sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "2.10.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-logs": { - "version": "0.222.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.222.0.tgz", - "integrity": "sha512-+19YHODIjaUCArxleaJtuufFZVpz/xvvK+VllQqE+W8hHolxdoRwHfK/s667zezwh1hkx6FFF+oYzetYgqK+Bg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.222.0", - "@opentelemetry/core": "2.11.0", - "@opentelemetry/resources": "2.11.0", - "@opentelemetry/semantic-conventions": "^1.29.0" - }, - "engines": { - "node": "^18.19.0 || >=20.6.0" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/api-logs": { - "version": "0.222.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.222.0.tgz", - "integrity": "sha512-9mb1If+IF6u0ZVXkHQ6ogEae5HwA6ajIVUgpSDQyRASxft6BSXHvBvPooRle3yFN/fKnCdSOnuu0OC3PLcF6+g==", + "integrity": "sha512-YbywG3veEm2Fb6TbdxRkuquWob6eVWXuA8/Ba1tXz9jHfUqpdE3keilOHEtPboC4CvS1bjeeVfNkWGOOrLj+lw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api": "^1.3.0" + "@opentelemetry/core": "2.11.0", + "@opentelemetry/otlp-transformer": "0.222.0" }, "engines": { - "node": ">=8.0.0" + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", - "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", + "node_modules/@opentelemetry/otlp-grpc-exporter-base": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.222.0.tgz", + "integrity": "sha512-Kw5WqDBYYpCgY+edHWZS/Jp3bdguqNkCyEWOsqFhf8RmtcEuWYzPFA9i+L6UsNoJXTs8ngeQB6dwELH1czNYjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@opentelemetry/semantic-conventions": "^1.29.0" + "@grpc/grpc-js": "^1.14.3", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/otlp-exporter-base": "0.222.0", + "@opentelemetry/otlp-transformer": "0.222.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", - "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", + "node_modules/@opentelemetry/otlp-transformer": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.222.0.tgz", + "integrity": "sha512-/F3BZ89+CJQnZkMh2tCrtcdB+XT2Dxhj4FFE+WPQ//413hmFL0/RfEX6vgOIWGhiSzrkHWTK3+6SiT7K5/g/jQ==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@opentelemetry/api-logs": "0.222.0", "@opentelemetry/core": "2.11.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-logs": "0.222.0", + "@opentelemetry/sdk-metrics": "2.11.0", + "@opentelemetry/sdk-trace": "2.11.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/sdk-metrics": { + "node_modules/@opentelemetry/resources": { "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.11.0.tgz", - "integrity": "sha512-7GXXcObyHyDUUSG+L+kJoquty01bzm7ivE7+SSgXXJcHuPzGviptxwARmI2c+bnnxjexGQbJnyNlN8HxBP/Y7A==", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", + "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.11.0", - "@opentelemetry/resources": "2.11.0" + "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.9.0 <1.10.0" + "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz", - "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==", + "node_modules/@opentelemetry/sdk-logs": { + "version": "0.222.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.222.0.tgz", + "integrity": "sha512-+19YHODIjaUCArxleaJtuufFZVpz/xvvK+VllQqE+W8hHolxdoRwHfK/s667zezwh1hkx6FFF+oYzetYgqK+Bg==", + "dev": true, "license": "Apache-2.0", "dependencies": { + "@opentelemetry/api-logs": "0.222.0", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" + "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { + "node_modules/@opentelemetry/sdk-metrics": { "version": "2.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz", - "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.11.0.tgz", + "integrity": "sha512-7GXXcObyHyDUUSG+L+kJoquty01bzm7ivE7+SSgXXJcHuPzGviptxwARmI2c+bnnxjexGQbJnyNlN8HxBP/Y7A==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/core": "2.11.0", - "@opentelemetry/semantic-conventions": "^1.29.0" + "@opentelemetry/resources": "2.11.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "node_modules/@opentelemetry/sdk-trace": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.10.0.tgz", - "integrity": "sha512-MfQGq3GRmTh5fM/y+OjaO0vj6+luCB1XO2gfXCalKCfgKw0eHL++sm75DNweC6ohlp+aFvACqeE0fYayqdRaoQ==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.11.0.tgz", + "integrity": "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.10.0", - "@opentelemetry/resources": "2.10.0", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -1096,14 +773,14 @@ } }, "node_modules/@opentelemetry/sdk-trace-base": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.10.0.tgz", - "integrity": "sha512-GuYQQT7QD2EeO8lcZLRQzcbOyhqAzL+6WWTKTU9mSUBYBazkEDl+VrQcXQhbB08OWM9anD1aHleVadzulpOaUQ==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.11.0.tgz", + "integrity": "sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.10.0", - "@opentelemetry/resources": "2.10.0", - "@opentelemetry/sdk-trace": "2.10.0", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/resources": "2.11.0", + "@opentelemetry/sdk-trace": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -1114,14 +791,14 @@ } }, "node_modules/@opentelemetry/sdk-trace-node": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.10.0.tgz", - "integrity": "sha512-GZK/G6oZyBLGlH1pUgeDch7D91KoHd2uotUGIkWCPi9GI5T9X0p4L7nNAMDR1BQjkRYoDqo+ddfVx9t5Uhys+Q==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-2.11.0.tgz", + "integrity": "sha512-CuvCMJmZxswhNLlM2LfuLOW3h3fZujA4hsG4B+Sz4dX2zvaXO8Ng74cnDHWD64gLszTlhiG3c0iNUjj4g+0/sA==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/context-async-hooks": "2.10.0", - "@opentelemetry/core": "2.10.0", - "@opentelemetry/sdk-trace-base": "2.10.0" + "@opentelemetry/context-async-hooks": "2.11.0", + "@opentelemetry/core": "2.11.0", + "@opentelemetry/sdk-trace-base": "2.11.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -1140,13 +817,13 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.143.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", - "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "version": "0.149.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.149.0.tgz", + "integrity": "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==", "dev": true, "license": "MIT", "funding": { - "url": "https://github.com/sponsors/Boshen" + "url": "https://github.com/sponsors/oxc-project" } }, "node_modules/@oxfmt/binding-android-arm-eabi": { @@ -1562,10 +1239,27 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.8.tgz", + "integrity": "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", - "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.8.tgz", + "integrity": "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==", "cpu": [ "arm64" ], @@ -1580,9 +1274,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", - "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.8.tgz", + "integrity": "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==", "cpu": [ "arm64" ], @@ -1597,9 +1291,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", - "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.8.tgz", + "integrity": "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==", "cpu": [ "x64" ], @@ -1614,9 +1308,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", - "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.8.tgz", + "integrity": "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==", "cpu": [ "x64" ], @@ -1631,9 +1325,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", - "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.8.tgz", + "integrity": "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==", "cpu": [ "arm" ], @@ -1648,9 +1342,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", - "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.8.tgz", + "integrity": "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==", "cpu": [ "arm64" ], @@ -1668,9 +1362,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", - "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.8.tgz", + "integrity": "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==", "cpu": [ "arm64" ], @@ -1688,9 +1382,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", - "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.8.tgz", + "integrity": "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==", "cpu": [ "ppc64" ], @@ -1708,9 +1402,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", - "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.8.tgz", + "integrity": "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==", "cpu": [ "s390x" ], @@ -1728,9 +1422,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", - "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.8.tgz", + "integrity": "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==", "cpu": [ "x64" ], @@ -1748,9 +1442,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", - "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.8.tgz", + "integrity": "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==", "cpu": [ "x64" ], @@ -1768,9 +1462,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", - "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.8.tgz", + "integrity": "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==", "cpu": [ "arm64" ], @@ -1785,9 +1479,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", - "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.8.tgz", + "integrity": "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==", "cpu": [ "arm64" ], @@ -1802,9 +1496,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", - "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.8.tgz", + "integrity": "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==", "cpu": [ "x64" ], @@ -1826,14 +1520,14 @@ "license": "MIT" }, "node_modules/@sap-cloud-sdk/connectivity": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/connectivity/-/connectivity-4.8.0.tgz", - "integrity": "sha512-y8H0AKgDecm7+A8wxx+J1oxJTzYEs+4Hy7ghfrBD57e9MdRRFI5FryYqokZbDcMjEnsbvZ+giShm/A2UXmeHyg==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/connectivity/-/connectivity-4.9.1.tgz", + "integrity": "sha512-0deJBmdUd1LD1O3CX/wqYjTEKmnrnLsxnrSE8CyGtzbyomjjL1ePEXB3g6QUpQKmClVyOgJG/IbpyJFGGXXL5A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@sap-cloud-sdk/resilience": "^4.8.0", - "@sap-cloud-sdk/util": "^4.8.0", + "@sap-cloud-sdk/resilience": "^4.9.1", + "@sap-cloud-sdk/util": "^4.9.1", "@sap/xsenv": "^6.2.0", "@sap/xssec": "^4.13.0", "async-retry": "^1.3.3", @@ -1844,35 +1538,35 @@ } }, "node_modules/@sap-cloud-sdk/http-client": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/http-client/-/http-client-4.8.0.tgz", - "integrity": "sha512-ruQd7d0nObshm0fNkhUE1fp1qxevEYxblhM/X0rG1goN9r4nQiT6LgZUG5s+QeR1R2LdIHBXtsIycBfz/4kOog==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/http-client/-/http-client-4.9.1.tgz", + "integrity": "sha512-bsF8T/syitm+GI8eInOB1AfvHsTYJ8Qvg5dfGt5SGXEQjqV2amFEulPp7edNTzCLo0+BBbBa/jhvqLCvXSoV6A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@sap-cloud-sdk/connectivity": "^4.8.0", - "@sap-cloud-sdk/resilience": "^4.8.0", - "@sap-cloud-sdk/util": "^4.8.0", + "@sap-cloud-sdk/connectivity": "^4.9.1", + "@sap-cloud-sdk/resilience": "^4.9.1", + "@sap-cloud-sdk/util": "^4.9.1", "axios": "^1.15.0" } }, "node_modules/@sap-cloud-sdk/resilience": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/resilience/-/resilience-4.8.0.tgz", - "integrity": "sha512-/XclOtUHhdN39acKH1otJdMILAm+HJkQ6wjpUvqBx7RNiQ1GQwe34qSqy8wGDJr5MbcxuhENYGWJRLE4P4OHqw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/resilience/-/resilience-4.9.1.tgz", + "integrity": "sha512-14Hdd2VqEd3BarikR/ChZjquQuelC+CP69HKdMycADmmsuQEwaJkSXURW8cDl19bfx8Re1ldM/4Ig0gbCY4x/A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@sap-cloud-sdk/util": "^4.8.0", + "@sap-cloud-sdk/util": "^4.9.1", "async-retry": "^1.3.3", "axios": "^1.15.0", "opossum": "^10.0.0" } }, "node_modules/@sap-cloud-sdk/util": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/util/-/util-4.8.0.tgz", - "integrity": "sha512-wLWxgxYwAL1N5dh+m8XGZTZ2vzooDHXKXrkay3rBpYSFHhZjsD2V37ezbAhbBKlFIELZA03C+Eb9o4YgHpvTKg==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@sap-cloud-sdk/util/-/util-4.9.1.tgz", + "integrity": "sha512-1tuv+c6o2Y8/1F3UEdqiv3TPWzZDpCjxuDpAFtFBM8C+weum8ixgwc2YXEWvBzWvtSEl7C5L/o8jBqiuhcXF4w==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1884,9 +1578,9 @@ } }, "node_modules/@sap/cds": { - "version": "10.0.5", - "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-10.0.5.tgz", - "integrity": "sha512-H5vTMVsznF4q24OVYkiWcReQezLOzKlVC3LBiHlrkg1D9x24V/OGWKCrCG+yd77hyIsrA2l2yxWE3RU4CElgmg==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@sap/cds/-/cds-10.1.0.tgz", + "integrity": "sha512-Eg8UwRcZ0iJp8JpYad6sFnlI2ByPzmSNWFlexyeo2I2XvsumLFc+NFsPOS45TPRrj8eRfoKgjaI+MyKcRk1AxQ==", "license": "SEE LICENSE IN LICENSE", "peer": true, "dependencies": { @@ -1913,9 +1607,9 @@ } }, "node_modules/@sap/cds-compiler": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-7.0.3.tgz", - "integrity": "sha512-scgBPK0TcobT0tXIQOEBTeVuGaxWthKeIQjhBoeLWcQXwVC3s61U3GFdbW56h01pXFwwUUSsxjzZN7all5lRyg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@sap/cds-compiler/-/cds-compiler-7.1.1.tgz", + "integrity": "sha512-ep7rzRJgZtDBs9vVcXyxLVy/q4p+K+a1Q+yq1qn1mb7dYHyH7m88K+Pn761Ofnf7HXXbTo/FKpM1z/kPTbwy3w==", "license": "SEE LICENSE IN LICENSE", "peer": true, "bin": { @@ -1938,9 +1632,9 @@ } }, "node_modules/@sap/cds-mtxs": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@sap/cds-mtxs/-/cds-mtxs-4.0.2.tgz", - "integrity": "sha512-C8JzlIGPxh/p8vGGyQMOsSJPWGJlOtrakaoCOb+mxDXbuLPDNccbRlrqon0a9aqgqyf4iQCrTuRNMJpd05blTg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@sap/cds-mtxs/-/cds-mtxs-4.1.0.tgz", + "integrity": "sha512-CWkHjmEiWchyIgaW5jGnrMGNbuJJSLSlC1cYZJaBBQVzm4p4j3AI10BKHm13scTumpYgRNti2KlWSUNE4WcpTA==", "dev": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { @@ -2010,9 +1704,9 @@ } }, "node_modules/@sap/xsenv": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@sap/xsenv/-/xsenv-6.2.1.tgz", - "integrity": "sha512-R1p7VdD3N3jvdkL8av4vLqF+cTQihTz9mCqqF+oa9rVZvgLaCb4ODyZ1dln5/fBgg1OSuch0ESxu3AqZrXVknw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@sap/xsenv/-/xsenv-6.2.2.tgz", + "integrity": "sha512-SGn+yaVKeXND/rQaQhqTZC+M6g5ASW1YnqAdQiZqNL74E1T1kUsDHttgOjQNkLObMZ0fsxgQxRPMnuVcAgIODg==", "dev": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { @@ -2025,9 +1719,9 @@ } }, "node_modules/@sap/xssec": { - "version": "4.13.3", - "resolved": "https://registry.npmjs.org/@sap/xssec/-/xssec-4.13.3.tgz", - "integrity": "sha512-op5wFTpJGJFdwFcEFRDX30yKbB2Kknk+LJqXDcrHyPNLqvSqerTWXFdFnylhiIhoAdGFSccC6NimUhqFxIrUsA==", + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@sap/xssec/-/xssec-4.15.0.tgz", + "integrity": "sha512-I9TMMaWq4BNDNt9blJy8o2FV9uXjs2RyNTT7KBv9fK83bGoQxcipQ8e4DrxYb296GXg+HhE420kHuqmyXZSOuQ==", "dev": true, "license": "SAP DEVELOPER LICENSE AGREEMENT", "dependencies": { @@ -2096,13 +1790,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.1.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", - "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", + "version": "26.5.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.1.tgz", + "integrity": "sha512-CzNm2FezW4VR/LjG6yUdiEgLE/rAQ9Slj5gCu/C2VrdcW7I0ahNZ8DRbHT7zOZ6r3ONgd/bsQIeSaoDGrd1C6g==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~8.3.0" + "undici-types": "~8.9.0" } }, "node_modules/@types/triple-beam": { @@ -2113,16 +1807,16 @@ "license": "MIT" }, "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -2131,13 +1825,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.10", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2158,9 +1852,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -2171,13 +1865,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.10", + "@vitest/utils": "4.1.11", "pathe": "^2.0.3" }, "funding": { @@ -2185,14 +1879,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2201,9 +1895,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", "funding": { @@ -2211,13 +1905,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -2292,6 +1986,16 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2308,6 +2012,26 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansi-styles/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ansi-styles/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -2363,9 +2087,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.19.0.tgz", - "integrity": "sha512-ht/iuYZXEjFxLH/Hkezgd7m6JKlHHXEUSneaDz8uZe1Gj5QZtCnpyDsckvAiEnT89OEbCLmnte4R4sn7P0EKFw==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", "dev": true, "license": "MIT", "dependencies": { @@ -2411,9 +2135,9 @@ } }, "node_modules/body-parser/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "license": "MIT", "peer": true, "engines": { @@ -2467,6 +2191,20 @@ "node": ">= 0.8" } }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2533,9 +2271,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", - "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.1.tgz", + "integrity": "sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==", "license": "MIT" }, "node_modules/cliui": { @@ -2553,69 +2291,6 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -2641,39 +2316,19 @@ } }, "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/color-string": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", - "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", "dev": true, "license": "MIT", "dependencies": { "color-name": "^2.0.0" }, "engines": { - "node": ">=18" + "node": ">=14.6" } }, - "node_modules/color-string/node_modules/color-name": { + "node_modules/color-name": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", @@ -2683,27 +2338,17 @@ "node": ">=12.20" } }, - "node_modules/color/node_modules/color-convert": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", - "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", "dev": true, "license": "MIT", "dependencies": { "color-name": "^2.0.0" }, "engines": { - "node": ">=14.6" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.1.tgz", - "integrity": "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" + "node": ">=18" } }, "node_modules/combined-stream": { @@ -2890,6 +2535,13 @@ "license": "MIT", "peer": true }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", @@ -2926,9 +2578,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", - "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.2.tgz", + "integrity": "sha512-BuIB67FngDSyQ/dpQNOZybwdEBDUGJQvOqwWr4ha/ufYiqzuEwPkKO2zLhRAgay28tStRIHUeWmszZAJo3GCOg==", "license": "MIT" }, "node_modules/es-object-atoms": { @@ -2990,9 +2642,9 @@ } }, "node_modules/eslint": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.9.1.tgz", - "integrity": "sha512-9VaAkDURekixUQJy0oJYl2DcN6oKMfxay7XzaGYAWQwsb6qfKf+x76R2k1L8kb1boc+FyCAaTA9GmiKaaiaF+A==", + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.10.0.tgz", + "integrity": "sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==", "devOptional": true, "license": "MIT", "workspaces": [ @@ -3004,7 +2656,7 @@ "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.7.0", "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.2", + "@eslint/plugin-kit": "^0.7.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -3019,7 +2671,7 @@ "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", + "file-entry-cache": "11.1.5 || >11.1.6 <12", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", @@ -3249,24 +2901,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -3275,16 +2909,13 @@ "license": "MIT" }, "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", "devOptional": true, "license": "MIT", "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" + "flat-cache": "^6.1.23" } }, "node_modules/fill-range": { @@ -3340,17 +2971,15 @@ } }, "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", "devOptional": true, "license": "MIT", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" } }, "node_modules/flatted": { @@ -3600,6 +3229,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -3612,6 +3254,13 @@ "node": ">= 0.4" } }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -3675,13 +3324,13 @@ } }, "node_modules/import-in-the-middle": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.3.tgz", - "integrity": "sha512-AiohS3H80sXO6owEltjGX+glb7qXaDhBoJb9XcQVH4UI207xu/bDLUcadVKp7Qe576reg9yr/PXZjV5qx8gfbA==", + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.5.1.tgz", + "integrity": "sha512-mPKuL8bPQzecui2KK6Gb+M8JvJoHnhS1FeYGa22QopBmlevF5F0FE6ued/B5EgHDeIoMTONIpDWWFKUPOG0DBQ==", "license": "Apache-2.0", "dependencies": { "cjs-module-lexer": "^2.2.0", - "es-module-lexer": "^2.2.0", + "es-module-lexer": "^3.0.2", "module-details-from-path": "^1.0.4" }, "engines": { @@ -3796,13 +3445,6 @@ "node-rsa": "^1.1.1" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "devOptional": true, - "license": "MIT" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -3840,19 +3482,6 @@ "npm": ">=6" } }, - "node_modules/jsonwebtoken/node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -3887,13 +3516,13 @@ } }, "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "devOptional": true, "license": "MIT", "dependencies": { - "json-buffer": "3.0.1" + "@keyv/serialize": "^1.1.1" } }, "node_modules/kuler": { @@ -4347,19 +3976,6 @@ "node": ">=8.6" } }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -4426,9 +4042,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.17", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", - "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", "dev": true, "funding": [ { @@ -4452,13 +4068,34 @@ "license": "MIT" }, "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", "license": "MIT", "peer": true, + "dependencies": { + "content-type": "^2.1.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/neo-async": { @@ -4522,9 +4159,9 @@ } }, "node_modules/obug": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", - "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -4736,22 +4373,22 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", "dev": true, "funding": [ { @@ -4769,7 +4406,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.17", + "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4788,9 +4425,9 @@ } }, "node_modules/protobufjs": { - "version": "7.6.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", - "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", "dev": true, "hasInstallScript": true, "license": "BSD-3-Clause", @@ -4845,10 +4482,30 @@ "node": ">=6" } }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/qs": { - "version": "6.15.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", - "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "license": "BSD-3-Clause", "peer": true, "dependencies": { @@ -4941,13 +4598,13 @@ } }, "node_modules/rolldown": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", - "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.8.tgz", + "integrity": "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.143.0", + "@oxc-project/types": "=0.149.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -4957,20 +4614,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.2.3", - "@rolldown/binding-darwin-arm64": "1.2.3", - "@rolldown/binding-darwin-x64": "1.2.3", - "@rolldown/binding-freebsd-x64": "1.2.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.3", - "@rolldown/binding-linux-arm64-gnu": "1.2.3", - "@rolldown/binding-linux-arm64-musl": "1.2.3", - "@rolldown/binding-linux-ppc64-gnu": "1.2.3", - "@rolldown/binding-linux-s390x-gnu": "1.2.3", - "@rolldown/binding-linux-x64-gnu": "1.2.3", - "@rolldown/binding-linux-x64-musl": "1.2.3", - "@rolldown/binding-openharmony-arm64": "1.2.3", - "@rolldown/binding-win32-arm64-msvc": "1.2.3", - "@rolldown/binding-win32-x64-msvc": "1.2.3" + "@rolldown/binding-android-arm-eabi": "1.2.8", + "@rolldown/binding-android-arm64": "1.2.8", + "@rolldown/binding-darwin-arm64": "1.2.8", + "@rolldown/binding-darwin-x64": "1.2.8", + "@rolldown/binding-freebsd-x64": "1.2.8", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", + "@rolldown/binding-linux-arm64-gnu": "1.2.8", + "@rolldown/binding-linux-arm64-musl": "1.2.8", + "@rolldown/binding-linux-ppc64-gnu": "1.2.8", + "@rolldown/binding-linux-s390x-gnu": "1.2.8", + "@rolldown/binding-linux-x64-gnu": "1.2.8", + "@rolldown/binding-linux-x64-musl": "1.2.8", + "@rolldown/binding-openharmony-arm64": "1.2.8", + "@rolldown/binding-win32-arm64-msvc": "1.2.8", + "@rolldown/binding-win32-x64-msvc": "1.2.8" } }, "node_modules/router": { @@ -5027,6 +4685,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", @@ -5251,10 +4922,38 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/systeminformation": { - "version": "5.33.1", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.1.tgz", - "integrity": "sha512-DEN6ICHk3Tk0Uf/hrAHh7xlt7iL5CJFBtPZinA0H62DrGG/KPKqq/Nzj6lCXPS4Ay/sf/14zNnk9LpqKzBIc+w==", + "version": "5.33.10", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.33.10.tgz", + "integrity": "sha512-/NXbMVASt2UbSVgmWto4aBTIAeuYY7zOELpZybmNxqpsrbo5uYHpuEPf5nkyrPng0uG5YOb+Yv6s0FyKY4mDQg==", "dev": true, "license": "MIT", "os": [ @@ -5293,9 +4992,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", - "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", "dev": true, "license": "MIT", "engines": { @@ -5319,6 +5018,37 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinypool": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", @@ -5405,9 +5135,9 @@ } }, "node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", "license": "MIT", "peer": true, "engines": { @@ -5433,9 +5163,9 @@ } }, "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", "dev": true, "license": "MIT" }, @@ -5492,16 +5222,16 @@ } }, "node_modules/vite": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", - "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.0.tgz", + "integrity": "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.25", - "rolldown": "~1.2.1", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "bin": { @@ -5518,7 +5248,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.4.0", + "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -5569,20 +5299,33 @@ } } }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/vitest": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.10", - "@vitest/mocker": "4.1.10", - "@vitest/pretty-format": "4.1.10", - "@vitest/runner": "4.1.10", - "@vitest/snapshot": "4.1.10", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -5610,12 +5353,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.10", - "@vitest/browser-preview": "4.1.10", - "@vitest/browser-webdriverio": "4.1.10", - "@vitest/coverage-istanbul": "4.1.10", - "@vitest/coverage-v8": "4.1.10", - "@vitest/ui": "4.1.10", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -5659,6 +5402,26 @@ } } }, + "node_modules/vitest/node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/voca": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/voca/-/voca-1.4.1.tgz", @@ -5754,6 +5517,24 @@ "dev": true, "license": "MIT" }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -5772,9 +5553,9 @@ } }, "node_modules/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", "license": "ISC", "peer": true, "bin": { @@ -5816,51 +5597,6 @@ "node": ">=12" } }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index cb53b1a1..646d4566 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,9 @@ "dependencies": { "@opentelemetry/api": "^1.9", "@opentelemetry/core": "^2.8", - "@opentelemetry/instrumentation": "^0.221", - "@opentelemetry/instrumentation-http": "^0.221", - "@opentelemetry/instrumentation-undici": "^0.31", + "@opentelemetry/instrumentation": "^0.222", + "@opentelemetry/instrumentation-http": "^0.222", + "@opentelemetry/instrumentation-undici": "^0.32", "@opentelemetry/resources": "^2.8", "@opentelemetry/sdk-metrics": "^2.8", "@opentelemetry/sdk-trace-base": "^2.8", From e677c3e9b8fcf25afec5e9588b013bcfb0ed1d98 Mon Sep 17 00:00:00 2001 From: Simon Engel Date: Wed, 16 Sep 2026 15:56:22 -0700 Subject: [PATCH 25/31] feat: add tenant id and correlation id to logs (#506) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Add Tenant and Correlation IDs to Log Attributes ### New Features ✨ Enhances log records with tenant and correlation context to improve observability and align logging metadata with spans and metrics. ### Changes * `lib/logging/index.js`: Adds `sap.tenancy.tenant_id` and `sap.cds.correlation_id` attributes to structured log records when available from `cds.context`, enabling easier tracing and tenant-aware log analysis. - [ ] 🔄 Regenerate and Update Summary
PR Bot Information **Version:** `1.31.30` - File Content Strategy: Full file content - Event Trigger: `pull_request.opened` - LLM: `gpt-5.5` - Summary Prompt: [Default Prompt](https://github.tools.sap/Code-Change-Intelligence/pr-bot/blob/main/src/services/llm/prompts/summary_instructions_prompt.md) - Output Template: [Default Template](https://github.tools.sap/Code-Change-Intelligence/pr-bot/blob/main/src/services/llm/prompts/summary_default_output_template.md) - Correlation ID: `495f5870-ae2e-11f1-951c-19ee13dea867`
--------- Co-authored-by: sjvans <30337871+sjvans@users.noreply.github.com> Co-authored-by: Sebastian Van Syckel --- lib/logging/index.js | 1 + test/logging-mt.test.js | 48 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 test/logging-mt.test.js diff --git a/lib/logging/index.js b/lib/logging/index.js index 924b3c6f..c6bf4d97 100644 --- a/lib/logging/index.js +++ b/lib/logging/index.js @@ -134,6 +134,7 @@ module.exports = resource => { const attributes = { 'log.type': 'LogRecord' } + if (log.tenant_id) attributes['sap.tenancy.tenant_id'] = log.tenant_id let e = args.find(a => a instanceof Error) if (e) { attributes['exception.message'] = e.message diff --git a/test/logging-mt.test.js b/test/logging-mt.test.js new file mode 100644 index 00000000..24c910e3 --- /dev/null +++ b/test/logging-mt.test.js @@ -0,0 +1,48 @@ +/* eslint-disable no-console */ + +const cds = require('@sap/cds') +// prettier-ignore +const { expect, GET } = cds.test('serve', '--in-memory', '--project', __dirname + '/bookshop', '--profile', 'logging,multitenancy') + +describe('logging with multitenancy', () => { + const TENANT1 = 'tenant_1' + const TENANT2 = 'tenant_2' + const user1 = { auth: { username: `user_${TENANT1}` } } + const user2 = { auth: { username: `user_${TENANT2}` } } + + const { dir } = console + beforeAll(async () => { + const mts = await cds.connect.to('cds.xt.DeploymentService') + await mts.subscribe(TENANT1) + await mts.subscribe(TENANT2) + }) + beforeEach(() => { + console.dir = vi.fn() + }) + afterAll(() => { + console.dir = dir + }) + + test('log records carry the tenant id for tenant_1', async () => { + const { status } = await GET('/odata/v4/admin/Genres', user1) + expect(status).to.equal(200) + const logs = console.dir.mock.calls.map(([log]) => log) + // the request-scoped logs from the AdminService READ Genres handler + const scoped = logs.filter(l => l?.attributes?.['sap.tenancy.tenant_id']) + expect(scoped.length).to.be.greaterThan(0) + for (const log of scoped) { + expect(log.attributes['sap.tenancy.tenant_id']).to.equal(TENANT1) + } + }) + + test('log records carry the tenant id for tenant_2', async () => { + const { status } = await GET('/odata/v4/admin/Genres', user2) + expect(status).to.equal(200) + const logs = console.dir.mock.calls.map(([log]) => log) + const scoped = logs.filter(l => l?.attributes?.['sap.tenancy.tenant_id']) + expect(scoped.length).to.be.greaterThan(0) + for (const log of scoped) { + expect(log.attributes['sap.tenancy.tenant_id']).to.equal(TENANT2) + } + }) +}) From 16eaa415db80878d8195ef344edc654f5a15401d Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:19:07 +0200 Subject: [PATCH 26/31] refactor: modularize lib/utils and decouple mTLS cert sourcing (#508) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Restructure `lib/utils`, decouple mTLS cert sourcing, simplify ZTI gating Internal refactor of the CaaS/mTLS machinery that landed in v2.1.0. No behavioral change to the supported configurations — pure reorganization plus a simplification of how ZTI is enabled. Base: `develop`. ### Modularize `lib/utils` The 321-line monolithic `lib/utils.js` is split into cohesive, single-responsibility modules, each imported directly (no barrel): * `utils/common.js` — `_require`, `hasDependency`, `getDiagLogLevel` * `utils/resource.js` — OTel `Resource` / Dynatrace metadata * `utils/time.js` — timestamp helpers * `utils/credentials.js` — binding resolution + creds augmentation (`getCredsForDTAsUPS`, `getCredsForCLSAsUPS`, `augmentCLCreds`, `augmentCaaSCreds`) * `utils/mtls.js` — generic mTLS transport + static x509 cert provisioning (`certsAvailable`, `setupStaticCerts`, rotating agent) * `utils/caas.js` — CaaS exporter assembly + boot-race buffering (`createCaaSExporter`, `wrapExporterWithBuffer`) ### Decouple mTLS cert sourcing Clear layering: **cert source** (`zti` async SVID / `mtls` static x509) → **generic mTLS transport** (`mtls`) → **exporter wiring** (`caas`) → **signal modules** (tracing/metrics/logging). * `certsAvailable()` is now a pure predicate over `cds.env`. * Cert provisioning is an explicit one-shot startup step (`_provisionCerts` in `lib/index.js`), gated to the `to-caas` kind, running before exporters are built. ### Simplify ZTI gating * Dropped the `TELEMETRY_USE_ZTI` opt-out and `isZTIEnabled()` — a bound `zero-trust-identity` service is now the **sole** trigger for ZTI as the cert source. * Renamed the `_useZTI` handoff flag to `_asyncCerts` to name what it signals: ZTI writes SVID files asynchronously and they may arrive after startup, so a missing cert is transient (buffer until the watcher delivers) rather than a misconfiguration (fail fast). Static x509 is present-now-or-never, so the flag stays false. ### Misc * Trimmed verbose comments in `lib/logging/index.js`. * Tests reorganized to match the new module boundaries (`test/buffer.test.js`, `test/caas.test.js`); suite green (97 passed, 8 HANA skips). --- lib/exporter/ConsoleMetricExporter.js | 5 +- lib/index.js | 25 +- lib/logging/index.js | 17 +- lib/metrics/index.js | 12 +- lib/metrics/queue.js | 22 +- lib/tracing/index.js | 13 +- lib/tracing/trace.js | 2 +- lib/utils.js | 321 -------------------------- lib/utils/caas.js | 78 +++++++ lib/utils/common.js | 50 ++++ lib/utils/credentials.js | 76 ++++++ lib/utils/mtls.js | 109 +++++++++ lib/utils/resource.js | 91 ++++++++ lib/utils/time.js | 44 ++++ lib/zti.js | 149 ++++++------ test/buffer.test.js | 19 +- test/caas.test.js | 111 +++++---- 17 files changed, 620 insertions(+), 524 deletions(-) delete mode 100644 lib/utils.js create mode 100644 lib/utils/caas.js create mode 100644 lib/utils/common.js create mode 100644 lib/utils/credentials.js create mode 100644 lib/utils/mtls.js create mode 100644 lib/utils/resource.js create mode 100644 lib/utils/time.js diff --git a/lib/exporter/ConsoleMetricExporter.js b/lib/exporter/ConsoleMetricExporter.js index 402fe6c0..e4d1f425 100644 --- a/lib/exporter/ConsoleMetricExporter.js +++ b/lib/exporter/ConsoleMetricExporter.js @@ -119,9 +119,8 @@ class ConsoleMetricExporter extends StandardConsoleMetricExporter { // export other metrics for (const tenant of Object.keys(other)) { for (const [k, v] of Object.entries(other[tenant])) { - LOG.info( - `${k}${tenant !== 'undefined' ? ` of tenant "${tenant}"` : ''}: ${inspect(v.length === 1 ? v[0] : v)}` - ) + // prettier-ignore + LOG.info(`${k}${tenant !== 'undefined' ? ` of tenant "${tenant}"` : ''}: ${inspect(v.length === 1 ? v[0] : v)}`) } } } diff --git a/lib/index.js b/lib/index.js index 18a7b3cc..b49c289e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -13,7 +13,22 @@ const { registerInstrumentations } = require('@opentelemetry/instrumentation') const tracing = require('./tracing') const metrics = require('./metrics') const logging = require('./logging') -const { getDiagLogLevel, getResource, hasDependency, _require } = require('./utils') +const zti = require('./zti') +const { getDiagLogLevel, hasDependency, _require } = require('./utils/common') +const { getResource } = require('./utils/resource') +const { setupStaticCerts } = require('./utils/mtls') + +// Provision the client certificate for mTLS exporters once at startup: prefer ZTI (which also +// starts watching for rotations), else fall back to statically configured x509. ZTI is an +// asynchronous source — SVID files may land shortly after startup — whereas static x509 is +// present now or never. Record which kind we got so exporter wiring knows whether a missing +// cert is expected-and-transient (async) or a hard misconfiguration. Only relevant for CaaS. +function _provisionCerts() { + if (!/to-caas$/.test(cds.env.requires.telemetry.kind || '')) return + const asyncCerts = zti.setup() + if (!asyncCerts) setupStaticCerts() + cds.env.requires.telemetry._asyncCerts = asyncCerts +} function _getInstrumentations() { const _instrumentations = cds.env.requires.telemetry.instrumentations @@ -117,4 +132,10 @@ function setup_with_calm() { if (cds.env.requires.telemetry.logging) logging() } -module.exports = hasDependency('@sap/xotel-agent-ext-js') ? setup_with_calm : setup_standalone +module.exports = () => { + // provision mTLS certs before building exporters + _provisionCerts() + + if (hasDependency('@sap/xotel-agent-ext-js')) setup_with_calm() + else setup_standalone() +} diff --git a/lib/logging/index.js b/lib/logging/index.js index c6bf4d97..94644f0c 100644 --- a/lib/logging/index.js +++ b/lib/logging/index.js @@ -3,7 +3,9 @@ const LOG = cds.log('telemetry') const { getStringFromEnv } = require('@opentelemetry/core') -const { getCredsForCLSAsUPS, augmentCLCreds, createCaaSExporter, _require } = require('../utils') +const { getCredsForCLSAsUPS, augmentCLCreds } = require('../utils/credentials') +const { createCaaSExporter } = require('../utils/caas') +const { _require } = require('../utils/common') const _protocol2module = { grpc: '@opentelemetry/exporter-logs-otlp-grpc', @@ -11,12 +13,7 @@ const _protocol2module = { 'http/json': '@opentelemetry/exporter-logs-otlp-http' } -// @opentelemetry/sdk-logs 0.221 changed the log processor constructors (Simple/Batch, and hence -// any subclass) from a positional exporter argument `(exporter)` to an options object `({ exporter })`. -// Passing the wrong shape leaves the exporter undefined, so every export throws and — since our diag -// logger is wired to cds.log — recurses back through cds.log.format into an unbounded loop (see #482). -// To stay correct regardless of which sdk-logs a consumer's tree resolves, detect the installed -// version and build the constructor argument accordingly (no hard version floor needed). +// @opentelemetry/sdk-logs 0.221 changed the log processor constructors function logProcessorArg(exporter) { const version = require('@opentelemetry/sdk-logs/package.json').version const [major, minor] = version.split('.').map(Number) @@ -113,11 +110,7 @@ module.exports = resource => { const custom_fields = cds.env.log.cls_custom_fields || [] // intercept logs via format - // re-entrancy guard: while we are inside our own logger.emit(), skip re-emitting anything - // that emit() logs synchronously (e.g. via the export path or the SDK's diag logger, which - // is wired to cds.log('telemetry')). This prevents cds.log.format from recursing back into - // logger.emit() and looping through the logs SDK's export/diagnostic output (#482). - let emitting = false + let emitting = false //> re-entrancy guard const { format: _format } = cds.log const format = (cds.log.format = function (module, level, ...args) { const res = _format.call(this, module, level, ...args) diff --git a/lib/metrics/index.js b/lib/metrics/index.js index 91697f65..a9c93895 100644 --- a/lib/metrics/index.js +++ b/lib/metrics/index.js @@ -6,14 +6,10 @@ const { getStringFromEnv } = require('@opentelemetry/core') const { resourceFromAttributes } = require('@opentelemetry/resources') const { AggregationTemporality, MeterProvider, PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics') -const { - getDynatraceMetadata, - getCredsForDTAsUPS, - getCredsForCLSAsUPS, - augmentCLCreds, - createCaaSExporter, - _require -} = require('../utils') +const { getDynatraceMetadata } = require('../utils/resource') +const { getCredsForDTAsUPS, getCredsForCLSAsUPS, augmentCLCreds } = require('../utils/credentials') +const { createCaaSExporter } = require('../utils/caas') +const { _require } = require('../utils/common') const _protocol2module = { grpc: '@opentelemetry/exporter-metrics-otlp-grpc', diff --git a/lib/metrics/queue.js b/lib/metrics/queue.js index eb800b4d..f9599713 100644 --- a/lib/metrics/queue.js +++ b/lib/metrics/queue.js @@ -2,30 +2,12 @@ const cds = require('@sap/cds') const { metrics, ValueType } = require('@opentelemetry/api') +const { timestampToEpoch } = require('../utils/time') + const LOG = cds.log('telemetry') const PERSISTENT_QUEUE_DB_NAME = 'cds.outbox.Messages' -// Parse a queue `timestamp` value to epoch millis, robust to the DB driver's format. -// Direct column reads return an ISO-8601 UTC string ("...Z"), but HANA's min()/max() -// aggregates return a timezone-naive string ("2026-08-13 22:50:41.2270000" — space -// separator, sub-ms digits, no zone). Passing that straight to `new Date()` parses it as -// LOCAL time, so storage-time gauges were off by the machine's UTC offset on HANA (e.g. 7200s -// in CEST). Normalize naive strings to UTC before parsing; ISO/Date/number inputs pass through. -function timestampToEpoch(ts) { - if (ts == null) return null - if (ts instanceof Date) return ts.getTime() - if (typeof ts === 'number') return ts - let s = String(ts).trim() - // Already zoned (ends with Z or ±HH:MM / ±HHMM)? leave as-is; otherwise treat as UTC. - if (!/[zZ]$|[+-]\d\d:?\d\d$/.test(s)) { - // "YYYY-MM-DD HH:MM:SS.fffffff" -> "YYYY-MM-DDTHH:MM:SS.fffZ" (trim sub-ms to 3 digits) - s = s.replace(' ', 'T').replace(/(\.\d{3})\d+$/, '$1') + 'Z' - } - const ms = Date.parse(s) - return Number.isNaN(ms) ? null : ms -} - async function collectLatestQueueInfo(queueEntity, serviceName, maxAttempts) { const coldEntriesRow = await SELECT.one .columns([{ func: 'count', args: [{ val: 1 }], as: 'cold_count' }]) diff --git a/lib/tracing/index.js b/lib/tracing/index.js index 2404399e..18360eb2 100644 --- a/lib/tracing/index.js +++ b/lib/tracing/index.js @@ -6,15 +6,10 @@ const { resourceFromAttributes } = require('@opentelemetry/resources') const { BatchSpanProcessor, SimpleSpanProcessor, SamplingDecision } = require('@opentelemetry/sdk-trace-base') const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node') -const { - getDynatraceMetadata, - getCredsForDTAsUPS, - getCredsForCLSAsUPS, - augmentCLCreds, - createCaaSExporter, - hasDependency, - _require -} = require('../utils') +const { getDynatraceMetadata } = require('../utils/resource') +const { getCredsForDTAsUPS, getCredsForCLSAsUPS, augmentCLCreds } = require('../utils/credentials') +const { createCaaSExporter } = require('../utils/caas') +const { hasDependency, _require } = require('../utils/common') function _getSampler() { const { ignoreIncomingPaths } = cds.env.requires.telemetry.tracing?.sampler || {} diff --git a/lib/tracing/trace.js b/lib/tracing/trace.js index feb85fb7..1ece9323 100644 --- a/lib/tracing/trace.js +++ b/lib/tracing/trace.js @@ -27,7 +27,7 @@ const { SEMATTRS_DB_SQL_TABLE: ATTR_DB_SQL_TABLE } = require('@opentelemetry/semantic-conventions') -const { _hrnow } = require('../utils') +const { _hrnow } = require('../utils/time') const DB_KINDS = { hana: 'sap.hana' diff --git a/lib/utils.js b/lib/utils.js deleted file mode 100644 index 0ace4c93..00000000 --- a/lib/utils.js +++ /dev/null @@ -1,321 +0,0 @@ -const cds = require('@sap/cds') -const LOG = cds.log('telemetry') - -const fs = require('fs') -const { initializeZTI, isZTIEnabled, getRotatingAgentFactory, loadInitialCerts, certsAvailable } = require('./zti') - -const { DiagLogLevel } = require('@opentelemetry/api') -const { hrTimeToMilliseconds, getStringFromEnv } = require('@opentelemetry/core') -const { resourceFromAttributes } = require('@opentelemetry/resources') -const { - ATTR_SERVICE_NAME, - ATTR_SERVICE_VERSION, - SEMRESATTRS_SERVICE_NAMESPACE: ATTR_SERVICE_NAMESPACE, - SEMRESATTRS_SERVICE_INSTANCE_ID: ATTR_SERVICE_INSTANCE_ID -} = require('@opentelemetry/semantic-conventions') - -const MAX_BUFFER_SIZE = Number(process.env.TELEMETRY_MAX_BUFFER_SIZE ?? 10) - -function getDiagLogLevel() { - if (process.env.OTEL_LOG_LEVEL) { - let level = Number(process.env.OTEL_LOG_LEVEL) - if (Number.isInteger(level)) return level - level = DiagLogLevel[process.env.OTEL_LOG_LEVEL.toUpperCase()] - if (!level) LOG.warn(`Unknown OTEL_LOG_LEVEL value: "${process.env.OTEL_LOG_LEVEL}", defaulting to "INFO"`) - return level ?? DiagLogLevel.INFO - } - - if (LOG._trace) return DiagLogLevel.VERBOSE - if (LOG._debug) return DiagLogLevel.DEBUG - if (LOG._info) return DiagLogLevel.INFO - if (LOG._warn) return DiagLogLevel.WARN - if (LOG._error) return DiagLogLevel.ERROR - return DiagLogLevel.NONE -} - -function getResource() { - const VCAP_APPLICATION = process.env.VCAP_APPLICATION && JSON.parse(process.env.VCAP_APPLICATION) - - let PKG - try { - PKG = require(cds.root + '/package.json') - } catch (err) { - LOG._info && LOG.info('Unable to require package.json to resolve app name and version due to error:', err) - } - - const name = VCAP_APPLICATION?.name || PKG?.name || 'CAP Application' - const version = PKG?.version || VCAP_APPLICATION?.application_version || '1.0.0' - - const attributes = {} - - // Service - attributes[ATTR_SERVICE_NAME] = getStringFromEnv('OTEL_SERVICE_NAME') || name - attributes[ATTR_SERVICE_VERSION] = getStringFromEnv('OTEL_SERVICE_VERSION') || version - - // Service (Experimental) - if (getStringFromEnv('OTEL_SERVICE_NAMESPACE')) - attributes[ATTR_SERVICE_NAMESPACE] = getStringFromEnv('OTEL_SERVICE_NAMESPACE') - if (VCAP_APPLICATION) attributes[ATTR_SERVICE_INSTANCE_ID] = VCAP_APPLICATION.instance_id - - if (process.env.CF_INSTANCE_GUID) { - attributes[ATTR_SERVICE_INSTANCE_ID] = process.env.CF_INSTANCE_GUID - attributes['sap.cf.instance_id'] = process.env.CF_INSTANCE_GUID - } - - if (VCAP_APPLICATION) { - attributes['sap.cf.source_id'] = VCAP_APPLICATION.application_id - attributes['sap.cf.app_id'] = VCAP_APPLICATION.application_id - attributes['sap.cf.app_name'] = VCAP_APPLICATION.name - attributes['sap.cf.space_id'] = VCAP_APPLICATION.space_id - attributes['sap.cf.space_name'] = VCAP_APPLICATION.space_name - attributes['sap.cf.org_id'] = VCAP_APPLICATION.organization_id - attributes['sap.cf.org_name'] = VCAP_APPLICATION.organization_name - attributes['sap.cf.source_type'] = 'APP/PROC/WEB' - attributes['sap.cf.process.id'] = VCAP_APPLICATION.process_id - attributes['sap.cf.process.instance_id'] = VCAP_APPLICATION.instance_id - attributes['sap.cf.process.type'] = VCAP_APPLICATION.process_type - } - - if (cds.env.requires.telemetry?.resource?.attributes) - Object.assign(attributes, cds.env.requires.telemetry.resource.attributes) - - return resourceFromAttributes(attributes) -} - -let dtmetadata -function getDynatraceMetadata() { - if (dtmetadata) return dtmetadata - - dtmetadata = resourceFromAttributes({}) - for (let name of [ - 'dt_metadata_e617c525669e072eebe3d0f08212e8f2.json', - '/var/lib/dynatrace/enrichment/dt_metadata.json' - ]) { - try { - LOG._debug && LOG.debug(`Trying to read dtmetadata source "${name}" ...`) - const content = fs - .readFileSync(name.startsWith('/var') ? name : fs.readFileSync(name).toString('utf-8').trim()) - .toString('utf-8') - LOG._debug && LOG.debug('Successful') - dtmetadata = dtmetadata.merge(resourceFromAttributes(JSON.parse(content))) - break - } catch (err) { - LOG._debug && LOG.debug('Failed with error:', err) - } - } - return dtmetadata -} - -function getCredsForDTAsUPS() { - if (!process.env.VCAP_SERVICES) return - const vcap = JSON.parse(process.env.VCAP_SERVICES) - - // Legacy Compat: - // > APMs requirement is that the instance name contains "dynatrace" - // > In addition to matching predicate defined in package.json, also support - // > - name matching /dynatrace/ - // ... in case binding info is available from environment variable VCAP_SERVICES - const dt = vcap['user-provided']?.find(b => b.name.match(/dynatrace/)) - if (dt) return dt.credentials -} - -function getCredsForCLSAsUPS() { - if (!process.env.VCAP_SERVICES) return - const vcap = JSON.parse(process.env.VCAP_SERVICES) - let ups - - // Legacy Compat: - // > In addition to matching predicate defined in package.json, also support - // > - tag: "cloud-logging" - // > - name matching /cloud-logging/ - // ... in case binding info is available from environment variable VCAP_SERVICES - ups = vcap['user-provided']?.find(e => e.tags.includes('cloud-logging') || e.tags.includes('Cloud Logging')) - if (ups) return ups.credentials - - ups = vcap['user-provided']?.find(b => b.name.match(/cloud-logging/)) - if (ups) { - // prettier-ignore - LOG._warn && LOG.warn('User-provided service instances of SAP Cloud Logging should have the tag "Cloud Logging"') - return ups.credentials - } -} - -function decodeCredentials(creds) { - if (creds.cert.startsWith('LS0t') || !creds.cert.startsWith('-----BEGIN')) { - const cert = Buffer.from(creds.cert, 'base64').toString('utf-8') - const key = Buffer.from(creds.key, 'base64').toString('utf-8') - if (!cert.includes('-----BEGIN') || !key.includes('-----BEGIN')) { - throw new Error('Decoded x509 cert or key is not valid PEM data') - } - return { cert, key } - } - return { cert: creds.cert, key: creds.key } -} - -function setupZTI() { - if (!isZTIEnabled()) return false - if (!initializeZTI()) return false - loadInitialCerts() - return true -} - -function setupStaticCerts() { - const { x509 } = cds.env.requires.telemetry || {} - if (!x509?.cert || !x509?.key) return false - - const { cert, key } = decodeCredentials(x509) - cds.env.requires.telemetry.x509 = { cert, key } - return true -} - -function augmentCaaSCreds(credentials) { - if (credentials._augmented) return - credentials._augmented = true - - if (!credentials.otlp?.http) { - throw new Error('No OTLP HTTP endpoint in CaaS credentials') - } - - credentials.baseUrl = credentials.otlp.http - - const useZTI = setupZTI() - if (!useZTI) setupStaticCerts() - - credentials.httpAgentOptions = getRotatingAgentFactory() - credentials.useZTI = useZTI -} - -function wrapExporterWithBuffer(exporter) { - const buffer = [] - let ready = false - const originalExport = exporter.export.bind(exporter) - - exporter.export = function (items, callback) { - if (!ready) { - if (certsAvailable()) { - ready = true - if (buffer.length > 0) { - LOG._debug && LOG.debug(`Flushing ${buffer.length} buffered items`) - for (const buffered of buffer) { - originalExport(buffered, () => {}) - } - buffer.length = 0 - } - // Restore original export for all future calls - exporter.export = originalExport - return originalExport(items, callback) - } - // Not ready yet — buffer this export batch. MAX_BUFFER_SIZE caps the - // number of pending batches (not records). The batch is ack'd as success - // to the SDK now; if certs never arrive, buffered batches are dropped. - if (buffer.length >= MAX_BUFFER_SIZE) { - buffer.shift() - LOG._warn && LOG.warn('Buffer full, dropping oldest batch') - } - buffer.push(items) - callback({ code: 0 }) - return - } - return originalExport(items, callback) - } - - return exporter -} - -function createCaaSExporter(credentials, config, signalPath, createExporterFn) { - if (!credentials) throw new Error('No CaaS credentials found.') - - augmentCaaSCreds(credentials) - - if (!certsAvailable() && !credentials.useZTI) { - throw new Error('CaaS requires mTLS. Bind zero-trust-identity service or configure x509 credentials.') - } - - const exporterConfig = { - ...config, - url: credentials.baseUrl + signalPath, - httpAgentOptions: credentials.httpAgentOptions - } - - let exporter = createExporterFn(exporterConfig) - - // If ZTI and certs not yet available, wrap with buffering - if (credentials.useZTI && !certsAvailable()) exporter = wrapExporterWithBuffer(exporter) - - return exporter -} - -function augmentCLCreds(credentials) { - if (credentials._augmented) return - credentials._augmented = true - - // prettier-ignore - if (!credentials['ingest-otlp-endpoint']) - throw new Error('No OpenTelemetry credentials found in binding to SAP Cloud Logging. Make sure to create the service instance with config: "{ ingest_otlp: { enabled: true } }".') - - credentials.url = 'https://' + credentials['ingest-otlp-endpoint'] - - const grpc = _require('@grpc/grpc-js') - const secureContext = require('tls').createSecureContext({ - cert: credentials['ingest-otlp-cert'], - key: credentials['ingest-otlp-key'] - }) - credentials.credentials = grpc.credentials.createFromSecureContext(secureContext) -} - -let PKG -function hasDependency(name) { - if (!PKG) { - try { - PKG = require(cds.root + '/package.json') - } catch (err) { - LOG._info && LOG.info(`Unable to require package.json to check for dependency "${name}" due to error:`, err) - return false - } - } - return !!PKG.dependencies[name] -} - -const now = Date.now() -const hrTimeInMS = Number(`${hrTimeToMilliseconds(process.hrtime())}`.split('.')[0]) -const diff = now - hrTimeInMS -const EPOCH_OFFSET_S = Number(`${diff}`.slice(0, -3)) -const EPOCH_OFFSET_MS = Number(`${diff}`.slice(-3) + '000000') - -// returns [seconds, nanoseconds] since unix epoch -function _hrnow() { - const hrtime = process.hrtime() - let s = hrtime[0] + EPOCH_OFFSET_S - let ns = hrtime[1] + EPOCH_OFFSET_MS - if (ns >= 1000000000) { - s++ - ns -= 1000000000 - } - return [s, ns] -} - -function _require(name) { - name = Array.isArray(name) ? name[0] : name - try { - return require(name.startsWith('./') ? cds.utils.path.join(cds.root, name) : name) - } catch (err) { - err.message = `Cannot find module '${name}'. Make sure to install it with 'npm i ${name}'\n` + err.message - throw err - } -} - -module.exports = { - getDiagLogLevel, - getResource, - getDynatraceMetadata, - getCredsForDTAsUPS, - getCredsForCLSAsUPS, - augmentCLCreds, - createCaaSExporter, - hasDependency, - _hrnow, - _require, - // exported for testing only (reached in production via createCaaSExporter) - augmentCaaSCreds, - wrapExporterWithBuffer -} diff --git a/lib/utils/caas.js b/lib/utils/caas.js new file mode 100644 index 00000000..b35fac7b --- /dev/null +++ b/lib/utils/caas.js @@ -0,0 +1,78 @@ +const cds = require('@sap/cds') +const LOG = cds.log('telemetry') + +const { augmentCaaSCreds } = require('./credentials') +const { certsAvailable } = require('./mtls') + +const MAX_BUFFER_SIZE = Number(process.env.TELEMETRY_MAX_BUFFER_SIZE ?? 10) + +function wrapExporterWithBuffer(exporter) { + const buffer = [] + let ready = false + const originalExport = exporter.export.bind(exporter) + + exporter.export = function (items, callback) { + if (!ready) { + if (certsAvailable()) { + ready = true + if (buffer.length > 0) { + LOG._debug && LOG.debug(`Flushing ${buffer.length} buffered items`) + for (const buffered of buffer) { + originalExport(buffered, () => {}) + } + buffer.length = 0 + } + // Restore original export for all future calls + exporter.export = originalExport + return originalExport(items, callback) + } + // Not ready yet — buffer this export batch. MAX_BUFFER_SIZE caps the + // number of pending batches (not records). The batch is ack'd as success + // to the SDK now; if certs never arrive, buffered batches are dropped. + if (buffer.length >= MAX_BUFFER_SIZE) { + buffer.shift() + LOG._warn && LOG.warn('Buffer full, dropping oldest batch') + } + buffer.push(items) + callback({ code: 0 }) + return + } + return originalExport(items, callback) + } + + return exporter +} + +function createCaaSExporter(credentials, config, signalPath, createExporterFn) { + if (!credentials) throw new Error('No CaaS credentials found.') + + augmentCaaSCreds(credentials) + + // Whether certs arrive asynchronously (ZTI SVID files, possibly after startup) is decided once + // at startup (lib/index.js). If so, a missing cert now is expected — it will arrive via the + // watcher — so we buffer instead of failing. Otherwise a missing cert is a hard misconfiguration. + const asyncCerts = cds.env.requires.telemetry?._asyncCerts ?? false + + if (!certsAvailable() && !asyncCerts) { + throw new Error('CaaS requires mTLS. Bind zero-trust-identity service or configure x509 credentials.') + } + + const exporterConfig = { + ...config, + url: credentials.baseUrl + signalPath, + httpAgentOptions: credentials.httpAgentOptions + } + + let exporter = createExporterFn(exporterConfig) + + // Certs not yet available but arriving async: buffer exports until the watcher delivers them + if (asyncCerts && !certsAvailable()) exporter = wrapExporterWithBuffer(exporter) + + return exporter +} + +module.exports = { + createCaaSExporter, + // exported for testing only (reached in production via createCaaSExporter) + wrapExporterWithBuffer +} diff --git a/lib/utils/common.js b/lib/utils/common.js new file mode 100644 index 00000000..e8e8f0c1 --- /dev/null +++ b/lib/utils/common.js @@ -0,0 +1,50 @@ +const cds = require('@sap/cds') +const LOG = cds.log('telemetry') + +const { DiagLogLevel } = require('@opentelemetry/api') + +function getDiagLogLevel() { + if (process.env.OTEL_LOG_LEVEL) { + let level = Number(process.env.OTEL_LOG_LEVEL) + if (Number.isInteger(level)) return level + level = DiagLogLevel[process.env.OTEL_LOG_LEVEL.toUpperCase()] + if (!level) LOG.warn(`Unknown OTEL_LOG_LEVEL value: "${process.env.OTEL_LOG_LEVEL}", defaulting to "INFO"`) + return level ?? DiagLogLevel.INFO + } + + if (LOG._trace) return DiagLogLevel.VERBOSE + if (LOG._debug) return DiagLogLevel.DEBUG + if (LOG._info) return DiagLogLevel.INFO + if (LOG._warn) return DiagLogLevel.WARN + if (LOG._error) return DiagLogLevel.ERROR + return DiagLogLevel.NONE +} + +let PKG +function hasDependency(name) { + if (!PKG) { + try { + PKG = require(cds.root + '/package.json') + } catch (err) { + LOG._info && LOG.info(`Unable to require package.json to check for dependency "${name}" due to error:`, err) + return false + } + } + return !!PKG.dependencies[name] +} + +function _require(name) { + name = Array.isArray(name) ? name[0] : name + try { + return require(name.startsWith('./') ? cds.utils.path.join(cds.root, name) : name) + } catch (err) { + err.message = `Cannot find module '${name}'. Make sure to install it with 'npm i ${name}'\n` + err.message + throw err + } +} + +module.exports = { + getDiagLogLevel, + hasDependency, + _require +} diff --git a/lib/utils/credentials.js b/lib/utils/credentials.js new file mode 100644 index 00000000..5010977a --- /dev/null +++ b/lib/utils/credentials.js @@ -0,0 +1,76 @@ +const cds = require('@sap/cds') +const LOG = cds.log('telemetry') + +const { _require } = require('./common') +const { getRotatingAgentFactory } = require('./mtls') + +function getCredsForDTAsUPS() { + if (!process.env.VCAP_SERVICES) return + const vcap = JSON.parse(process.env.VCAP_SERVICES) + + // Legacy Compat: + // > APMs requirement is that the instance name contains "dynatrace" + // > In addition to matching predicate defined in package.json, also support + // > - name matching /dynatrace/ + // ... in case binding info is available from environment variable VCAP_SERVICES + const dt = vcap['user-provided']?.find(b => b.name.match(/dynatrace/)) + if (dt) return dt.credentials +} + +function getCredsForCLSAsUPS() { + if (!process.env.VCAP_SERVICES) return + const vcap = JSON.parse(process.env.VCAP_SERVICES) + let ups + + // Legacy Compat: + // > In addition to matching predicate defined in package.json, also support + // > - tag: "cloud-logging" + // > - name matching /cloud-logging/ + // ... in case binding info is available from environment variable VCAP_SERVICES + ups = vcap['user-provided']?.find(e => e.tags.includes('cloud-logging') || e.tags.includes('Cloud Logging')) + if (ups) return ups.credentials + + ups = vcap['user-provided']?.find(b => b.name.match(/cloud-logging/)) + if (ups) { + // prettier-ignore + LOG._warn && LOG.warn('User-provided service instances of SAP Cloud Logging should have the tag "Cloud Logging"') + return ups.credentials + } +} + +function augmentCaaSCreds(credentials) { + if (credentials._augmented) return + credentials._augmented = true + + if (!credentials.otlp?.http) { + throw new Error('No OTLP HTTP endpoint in CaaS credentials') + } + + credentials.baseUrl = credentials.otlp.http + credentials.httpAgentOptions = getRotatingAgentFactory() +} + +function augmentCLCreds(credentials) { + if (credentials._augmented) return + credentials._augmented = true + + // prettier-ignore + if (!credentials['ingest-otlp-endpoint']) + throw new Error('No OpenTelemetry credentials found in binding to SAP Cloud Logging. Make sure to create the service instance with config: "{ ingest_otlp: { enabled: true } }".') + + credentials.url = 'https://' + credentials['ingest-otlp-endpoint'] + + const grpc = _require('@grpc/grpc-js') + const secureContext = require('tls').createSecureContext({ + cert: credentials['ingest-otlp-cert'], + key: credentials['ingest-otlp-key'] + }) + credentials.credentials = grpc.credentials.createFromSecureContext(secureContext) +} + +module.exports = { + getCredsForDTAsUPS, + getCredsForCLSAsUPS, + augmentCLCreds, + augmentCaaSCreds +} diff --git a/lib/utils/mtls.js b/lib/utils/mtls.js new file mode 100644 index 00000000..d95e35b7 --- /dev/null +++ b/lib/utils/mtls.js @@ -0,0 +1,109 @@ +const cds = require('@sap/cds') +const LOG = cds.log('telemetry') + +// Generic HTTP mTLS transport for OTLP exporters. It is agnostic to where the client +// certificate comes from: it reads the current cert/key from cds.env and hot-swaps them +// whenever a cert source announces a rotation. ZTI is one such source, but static x509 or +// any future provider works the same way — the only contract is the 'svid' event + cds.env. + +// True once a usable client certificate is present in cds.env, regardless of its source +// (ZTI-loaded or statically configured). Both cert and key are required — a mismatched pair +// would only surface as a TLS handshake failure later, so the fail-fast path must reject it. +// Pure predicate: no I/O, no watcher side effects. +function certsAvailable() { + const x509 = cds.env.requires?.telemetry?.x509 + return !!(x509?.cert && x509?.key) +} + +function decodeCredentials(creds) { + if (creds.cert.startsWith('LS0t') || !creds.cert.startsWith('-----BEGIN')) { + const cert = Buffer.from(creds.cert, 'base64').toString('utf-8') + const key = Buffer.from(creds.key, 'base64').toString('utf-8') + if (!cert.includes('-----BEGIN') || !key.includes('-----BEGIN')) { + throw new Error('Decoded x509 cert or key is not valid PEM data') + } + return { cert, key } + } + return { cert: creds.cert, key: creds.key } +} + +// Provision statically configured x509 credentials into cds.env, decoding base64 if needed. +// The static counterpart to ZTI cert sourcing; both feed the same cds.env.x509 slot that +// certsAvailable() and the rotating agent read. Cert provisioning is orchestrated once at +// startup (see lib/index.js); this is the non-ZTI fallback. +function setupStaticCerts() { + const { x509 } = cds.env.requires.telemetry || {} + if (!x509?.cert || !x509?.key) return false + + const { cert, key } = decodeCredentials(x509) + cds.env.requires.telemetry.x509 = { cert, key } + return true +} + +let _RotatingCertAgent = null + +function getRotatingCertAgentClass() { + if (_RotatingCertAgent) return _RotatingCertAgent + const https = require('https') + + _RotatingCertAgent = class RotatingCertAgent extends https.Agent { + constructor() { + const x509 = cds.env.requires.telemetry?.x509 + super({ keepAlive: true, cert: x509?.cert, key: x509?.key }) + this._boundRotate = this._rotate.bind(this) + cds.on('svid', this._boundRotate) + } + + _rotate(payload) { + try { + const { cert, key } = payload ?? cds.env.requires.telemetry?.x509 ?? {} + if (!cert || !key) { + LOG._warn && LOG.warn('RotatingCertAgent: no credentials in event payload or cds.env') + return + } + this.options.cert = cert + this.options.key = key + this.destroy() + LOG._debug && LOG.debug('Certificate rotated') + } catch (err) { + LOG._error && LOG.error('Failed to rotate certificate:', err) + } + } + + _cleanup() { + cds.off('svid', this._boundRotate) + this.destroy() + } + } + return _RotatingCertAgent +} + +let _rotatingAgent = null + +function createRotatingAgent() { + if (!_rotatingAgent) { + const RotatingCertAgent = getRotatingCertAgentClass() + _rotatingAgent = new RotatingCertAgent() + } + return _rotatingAgent +} + +function getRotatingAgentFactory() { + return () => createRotatingAgent() +} + +function reset() { + if (_rotatingAgent) { + _rotatingAgent._cleanup() + _rotatingAgent = null + } +} + +module.exports = { + certsAvailable, + setupStaticCerts, + getRotatingAgentFactory, + // exported for testing only (used internally within this module in production) + getRotatingCertAgentClass, + reset +} diff --git a/lib/utils/resource.js b/lib/utils/resource.js new file mode 100644 index 00000000..45e83a19 --- /dev/null +++ b/lib/utils/resource.js @@ -0,0 +1,91 @@ +const cds = require('@sap/cds') +const LOG = cds.log('telemetry') + +const fs = require('fs') + +const { getStringFromEnv } = require('@opentelemetry/core') +const { resourceFromAttributes } = require('@opentelemetry/resources') +const { + ATTR_SERVICE_NAME, + ATTR_SERVICE_VERSION, + SEMRESATTRS_SERVICE_NAMESPACE: ATTR_SERVICE_NAMESPACE, + SEMRESATTRS_SERVICE_INSTANCE_ID: ATTR_SERVICE_INSTANCE_ID +} = require('@opentelemetry/semantic-conventions') + +function getResource() { + const VCAP_APPLICATION = process.env.VCAP_APPLICATION && JSON.parse(process.env.VCAP_APPLICATION) + + let PKG + try { + PKG = require(cds.root + '/package.json') + } catch (err) { + LOG._info && LOG.info('Unable to require package.json to resolve app name and version due to error:', err) + } + + const name = VCAP_APPLICATION?.name || PKG?.name || 'CAP Application' + const version = PKG?.version || VCAP_APPLICATION?.application_version || '1.0.0' + + const attributes = {} + + // Service + attributes[ATTR_SERVICE_NAME] = getStringFromEnv('OTEL_SERVICE_NAME') || name + attributes[ATTR_SERVICE_VERSION] = getStringFromEnv('OTEL_SERVICE_VERSION') || version + + // Service (Experimental) + if (getStringFromEnv('OTEL_SERVICE_NAMESPACE')) + attributes[ATTR_SERVICE_NAMESPACE] = getStringFromEnv('OTEL_SERVICE_NAMESPACE') + if (VCAP_APPLICATION) attributes[ATTR_SERVICE_INSTANCE_ID] = VCAP_APPLICATION.instance_id + + if (process.env.CF_INSTANCE_GUID) { + attributes[ATTR_SERVICE_INSTANCE_ID] = process.env.CF_INSTANCE_GUID + attributes['sap.cf.instance_id'] = process.env.CF_INSTANCE_GUID + } + + if (VCAP_APPLICATION) { + attributes['sap.cf.source_id'] = VCAP_APPLICATION.application_id + attributes['sap.cf.app_id'] = VCAP_APPLICATION.application_id + attributes['sap.cf.app_name'] = VCAP_APPLICATION.name + attributes['sap.cf.space_id'] = VCAP_APPLICATION.space_id + attributes['sap.cf.space_name'] = VCAP_APPLICATION.space_name + attributes['sap.cf.org_id'] = VCAP_APPLICATION.organization_id + attributes['sap.cf.org_name'] = VCAP_APPLICATION.organization_name + attributes['sap.cf.source_type'] = 'APP/PROC/WEB' + attributes['sap.cf.process.id'] = VCAP_APPLICATION.process_id + attributes['sap.cf.process.instance_id'] = VCAP_APPLICATION.instance_id + attributes['sap.cf.process.type'] = VCAP_APPLICATION.process_type + } + + if (cds.env.requires.telemetry?.resource?.attributes) + Object.assign(attributes, cds.env.requires.telemetry.resource.attributes) + + return resourceFromAttributes(attributes) +} + +let dtmetadata +function getDynatraceMetadata() { + if (dtmetadata) return dtmetadata + + dtmetadata = resourceFromAttributes({}) + for (let name of [ + 'dt_metadata_e617c525669e072eebe3d0f08212e8f2.json', + '/var/lib/dynatrace/enrichment/dt_metadata.json' + ]) { + try { + LOG._debug && LOG.debug(`Trying to read dtmetadata source "${name}" ...`) + const content = fs + .readFileSync(name.startsWith('/var') ? name : fs.readFileSync(name).toString('utf-8').trim()) + .toString('utf-8') + LOG._debug && LOG.debug('Successful') + dtmetadata = dtmetadata.merge(resourceFromAttributes(JSON.parse(content))) + break + } catch (err) { + LOG._debug && LOG.debug('Failed with error:', err) + } + } + return dtmetadata +} + +module.exports = { + getResource, + getDynatraceMetadata +} diff --git a/lib/utils/time.js b/lib/utils/time.js new file mode 100644 index 00000000..c9c313f4 --- /dev/null +++ b/lib/utils/time.js @@ -0,0 +1,44 @@ +const { hrTimeToMilliseconds } = require('@opentelemetry/core') + +const now = Date.now() +const hrTimeInMS = Number(`${hrTimeToMilliseconds(process.hrtime())}`.split('.')[0]) +const diff = now - hrTimeInMS +const EPOCH_OFFSET_S = Number(`${diff}`.slice(0, -3)) +const EPOCH_OFFSET_MS = Number(`${diff}`.slice(-3) + '000000') + +// returns [seconds, nanoseconds] since unix epoch +function _hrnow() { + const hrtime = process.hrtime() + let s = hrtime[0] + EPOCH_OFFSET_S + let ns = hrtime[1] + EPOCH_OFFSET_MS + if (ns >= 1000000000) { + s++ + ns -= 1000000000 + } + return [s, ns] +} + +// Parse a queue `timestamp` value to epoch millis, robust to the DB driver's format. +// Direct column reads return an ISO-8601 UTC string ("...Z"), but HANA's min()/max() +// aggregates return a timezone-naive string ("2026-08-13 22:50:41.2270000" — space +// separator, sub-ms digits, no zone). Passing that straight to `new Date()` parses it as +// LOCAL time, so storage-time gauges were off by the machine's UTC offset on HANA (e.g. 7200s +// in CEST). Normalize naive strings to UTC before parsing; ISO/Date/number inputs pass through. +function timestampToEpoch(ts) { + if (ts == null) return null + if (ts instanceof Date) return ts.getTime() + if (typeof ts === 'number') return ts + let s = String(ts).trim() + // Already zoned (ends with Z or ±HH:MM / ±HHMM)? leave as-is; otherwise treat as UTC. + if (!/[zZ]$|[+-]\d\d:?\d\d$/.test(s)) { + // "YYYY-MM-DD HH:MM:SS.fffffff" -> "YYYY-MM-DDTHH:MM:SS.fffZ" (trim sub-ms to 3 digits) + s = s.replace(' ', 'T').replace(/(\.\d{3})\d+$/, '$1') + 'Z' + } + const ms = Date.parse(s) + return Number.isNaN(ms) ? null : ms +} + +module.exports = { + _hrnow, + timestampToEpoch +} diff --git a/lib/zti.js b/lib/zti.js index 4efaa463..f6e0a00d 100644 --- a/lib/zti.js +++ b/lib/zti.js @@ -7,6 +7,7 @@ const SVID_DIR = '/home/vcap/app/spire-svids' let _paths = null let _cached = null let _watcher = null +let _bootstrapWatcher = null function _getSVIDCertificate() { if (!_paths) throw new Error('ZTI paths not initialized') @@ -42,15 +43,6 @@ function _getSVIDCertificate() { } } -function certsAvailable() { - if (isZTIEnabled() && loadInitialCerts()) { - startSVIDWatcher() - return true - } - // ZTI disabled, or enabled-by-default but not bound: fall back to static x509 - return !!cds.env.requires.telemetry?.x509?.cert -} - function getZTIConfig() { if (!process.env.VCAP_SERVICES) return null @@ -80,10 +72,6 @@ function initializeZTI() { return true } -function isZTIEnabled() { - return process.env.TELEMETRY_USE_ZTI !== 'false' -} - function loadInitialCerts() { if (!_paths) return false try { @@ -96,112 +84,107 @@ function loadInitialCerts() { } } +// One-shot startup provisioning: if a zero-trust-identity service is bound, ZTI is the cert +// source — load the initial SVID into cds.env and start watching for rotations. Returns whether +// ZTI is active, independent of whether certs are present yet, since they may arrive +// asynchronously (SPIRE may write the SVID files shortly after startup). +function setup() { + if (!initializeZTI()) return false + loadInitialCerts() + startSVIDWatcher() + return true +} + function startSVIDWatcher() { if (_watcher) return // already watching if (!_paths) throw new Error('ZTI not initialized') - let debounce = null const dir = require('path').dirname(_paths.cert) + let debounce = null try { _watcher = fs.watch(dir, (_eventType, filename) => { if (filename && !filename.endsWith('.svid.pem')) return if (debounce) clearTimeout(debounce) - debounce = setTimeout(() => { - try { - _cached = null - const creds = _getSVIDCertificate() - if (!cds.env.requires.telemetry) cds.env.requires.telemetry = {} - cds.env.requires.telemetry.x509 = { cert: creds.cert, key: creds.key } - cds.emit('svid', { cert: creds.cert, key: creds.key }) - LOG._debug && LOG.debug('SVID certificate rotated') - } catch (err) { - LOG._error && LOG.error('Failed to reload SVID on rotation:', err) - } - }, 1000) + debounce = setTimeout(reloadAndEmitSVID, 1000) }) } catch (err) { - LOG._warn && LOG.warn('Failed to setup SVID watcher:', err) + // The SVID directory may not exist yet during the boot race (SPIRE writes it shortly after + // startup). Watch the parent so we can arm the real watcher once the directory appears — + // otherwise setup() reports async certs with no watcher left to ever deliver them, and + // buffering exporters queue forever. + if (err.code === 'ENOENT') startBootstrapWatcher(dir) + else LOG._warn && LOG.warn('Failed to setup SVID watcher:', err) } } -function stopSVIDWatcher() { - if (_watcher) { - _watcher.close() - _watcher = null +// Reload the SVID from disk into cds.env and announce it via the 'svid' event so the rotating +// agent hot-swaps and buffering exporters flush. Used for rotations and for the delayed initial +// load once the SVID files first appear. +function reloadAndEmitSVID() { + try { + _cached = null + const creds = _getSVIDCertificate() + if (!cds.env.requires.telemetry) cds.env.requires.telemetry = {} + cds.env.requires.telemetry.x509 = { cert: creds.cert, key: creds.key } + cds.emit('svid', { cert: creds.cert, key: creds.key }) + LOG._debug && LOG.debug('SVID certificate loaded') + } catch (err) { + LOG._error && LOG.error('Failed to reload SVID:', err) } } -let _RotatingCertAgent = null - -function getRotatingCertAgentClass() { - if (_RotatingCertAgent) return _RotatingCertAgent - const https = require('https') - - _RotatingCertAgent = class RotatingCertAgent extends https.Agent { - constructor() { - const x509 = cds.env.requires.telemetry?.x509 - super({ keepAlive: true, cert: x509?.cert, key: x509?.key }) - this._boundRotate = this._rotate.bind(this) - cds.on('svid', this._boundRotate) - } - - _rotate(payload) { - try { - const { cert, key } = payload ?? cds.env.requires.telemetry?.x509 ?? {} - if (!cert || !key) { - LOG._warn && LOG.warn('RotatingCertAgent: no credentials in event payload or cds.env') - return - } - this.options.cert = cert - this.options.key = key - this.destroy() - LOG._debug && LOG.debug('Certificate rotated') - } catch (err) { - LOG._error && LOG.error('Failed to rotate certificate:', err) - } - } +// Boot-race fallback: watch the (always-present) parent directory until the SVID directory is +// created, then hand off to the real watcher and pick up any files already written. +function startBootstrapWatcher(svidDir) { + if (_bootstrapWatcher) return + const path = require('path') + const parent = path.dirname(svidDir) + const base = path.basename(svidDir) + let debounce = null - _cleanup() { - cds.off('svid', this._boundRotate) - this.destroy() - } + try { + _bootstrapWatcher = fs.watch(parent, (_eventType, filename) => { + if (filename && filename !== base) return + if (!fs.existsSync(svidDir)) return + if (debounce) clearTimeout(debounce) + debounce = setTimeout(() => { + stopBootstrapWatcher() + startSVIDWatcher() // directory now exists — arm the real watcher + if (fs.existsSync(_paths.cert)) reloadAndEmitSVID() // files may already be present + }, 500) + }) + } catch (err) { + LOG._warn && LOG.warn('Failed to setup SVID bootstrap watcher:', err) } - return _RotatingCertAgent } -let _rotatingAgent = null - -function createRotatingAgent() { - if (!_rotatingAgent) { - const RotatingCertAgent = getRotatingCertAgentClass() - _rotatingAgent = new RotatingCertAgent() +function stopSVIDWatcher() { + if (_watcher) { + _watcher.close() + _watcher = null } - return _rotatingAgent + stopBootstrapWatcher() } -function getRotatingAgentFactory() { - return () => createRotatingAgent() +function stopBootstrapWatcher() { + if (_bootstrapWatcher) { + _bootstrapWatcher.close() + _bootstrapWatcher = null + } } function reset() { _paths = null _cached = null stopSVIDWatcher() - if (_rotatingAgent) { - _rotatingAgent._cleanup() - _rotatingAgent = null - } } module.exports = { - initializeZTI, - isZTIEnabled, + setup, loadInitialCerts, - certsAvailable, - getRotatingAgentFactory, // exported for testing only (used internally within this module in production) + initializeZTI, getZTIConfig, - getRotatingCertAgentClass, reset } diff --git a/test/buffer.test.js b/test/buffer.test.js index 92f01e91..24fcf428 100644 --- a/test/buffer.test.js +++ b/test/buffer.test.js @@ -1,28 +1,19 @@ /** * Tests for wrapExporterWithBuffer - isolated in separate file for clean process isolation. * These tests manipulate the cds.env singleton which persists across vi.resetModules() calls. - * Tests use static x509 certs (ZTI disabled) to test buffering behavior. + * They drive buffering via cds.env x509 directly (certsAvailable), no cert source involved. */ const cds = require('@sap/cds') describe('wrapExporterWithBuffer', () => { beforeEach(() => { vi.resetModules() - // Clear env vars that affect ZTI detection - delete process.env.VCAP_SERVICES - delete process.env.TELEMETRY_ZTI_DIR - // Disable ZTI to test static x509 cert path - process.env.TELEMETRY_USE_ZTI = 'false' // Clear cds.env singleton - especially x509 certs from prior tests cds.env.requires = { telemetry: {} } }) - afterEach(() => { - delete process.env.TELEMETRY_USE_ZTI - }) - test('buffers items until certs are available', async () => { - const { wrapExporterWithBuffer } = await import('../lib/utils.js') + const { wrapExporterWithBuffer } = await import('../lib/utils/caas.js') const exportedItems = [] const mockExporter = { @@ -52,7 +43,7 @@ describe('wrapExporterWithBuffer', () => { }) test('drops oldest when buffer full', async () => { - const { wrapExporterWithBuffer } = await import('../lib/utils.js') + const { wrapExporterWithBuffer } = await import('../lib/utils/caas.js') const exportedItems = [] const mockExporter = { @@ -94,7 +85,7 @@ describe('wrapExporterWithBuffer', () => { } } - const { wrapExporterWithBuffer } = await import('../lib/utils.js') + const { wrapExporterWithBuffer } = await import('../lib/utils/caas.js') const exportedItems = [] const mockExporter = { @@ -115,7 +106,7 @@ describe('wrapExporterWithBuffer', () => { }) test('after certs available, exports go directly to original (no buffer)', async () => { - const { wrapExporterWithBuffer } = await import('../lib/utils.js') + const { wrapExporterWithBuffer } = await import('../lib/utils/caas.js') const exportCalls = [] const mockExporter = { diff --git a/test/caas.test.js b/test/caas.test.js index 911c99a7..51795724 100644 --- a/test/caas.test.js +++ b/test/caas.test.js @@ -74,7 +74,6 @@ function createZTITestContext() { setupEnv() { process.env.VCAP_SERVICES = JSON.stringify(MOCK_ZTI_VCAP) process.env.TELEMETRY_ZTI_DIR = svidDir - delete process.env.TELEMETRY_USE_ZTI cds.env.requires = { telemetry: {} } }, clearModuleCache() { @@ -84,7 +83,6 @@ function createZTITestContext() { fs.rmSync(tmpDir, { recursive: true, force: true }) delete process.env.VCAP_SERVICES delete process.env.TELEMETRY_ZTI_DIR - delete process.env.TELEMETRY_USE_ZTI vi.resetModules() } } @@ -115,7 +113,7 @@ describe('augmentCaaSCreds', () => { test('sets baseUrl from otlp.http', async () => { process.env.VCAP_SERVICES = JSON.stringify(MOCK_CAAS_VCAP) - const { augmentCaaSCreds } = await import('../lib/utils.js') + const { augmentCaaSCreds } = await import('../lib/utils/credentials.js') const credentials = { otlp: { @@ -131,7 +129,7 @@ describe('augmentCaaSCreds', () => { test('sets httpAgentOptions when mTLS credentials found', async () => { process.env.VCAP_SERVICES = JSON.stringify(MOCK_CAAS_VCAP) - const { augmentCaaSCreds } = await import('../lib/utils.js') + const { augmentCaaSCreds } = await import('../lib/utils/credentials.js') const credentials = { otlp: { http: 'https://caas.example.com/otlp' } @@ -145,14 +143,14 @@ describe('augmentCaaSCreds', () => { test('throws when no OTLP endpoints', async () => { process.env.VCAP_SERVICES = JSON.stringify(MOCK_CAAS_VCAP) - const { augmentCaaSCreds } = await import('../lib/utils.js') + const { augmentCaaSCreds } = await import('../lib/utils/credentials.js') expect(() => augmentCaaSCreds({})).toThrow('No OTLP HTTP endpoint in CaaS credentials') }) test('does not augment twice', async () => { process.env.VCAP_SERVICES = JSON.stringify(MOCK_CAAS_VCAP) - const { augmentCaaSCreds } = await import('../lib/utils.js') + const { augmentCaaSCreds } = await import('../lib/utils/credentials.js') const credentials = { otlp: { http: 'https://caas.example.com/otlp' } @@ -170,7 +168,7 @@ describe('augmentCaaSCreds', () => { test('httpAgentOptions always set (agent works once certs available)', async () => { cds.env.requires.telemetry = {} // No x509 credentials process.env.VCAP_SERVICES = JSON.stringify(MOCK_CAAS_VCAP) - const { augmentCaaSCreds } = await import('../lib/utils.js') + const { augmentCaaSCreds } = await import('../lib/utils/credentials.js') const credentials = { otlp: { http: 'https://caas.example.com/otlp' } @@ -220,7 +218,7 @@ describe('ZTI', () => { // No certs in cds.env cds.env.requires.telemetry = {} - const { getRotatingCertAgentClass, reset } = await import('../lib/zti.js') + const { getRotatingCertAgentClass, reset } = await import('../lib/utils/mtls.js') // Should not throw const RotatingCertAgent = getRotatingCertAgentClass() @@ -236,7 +234,8 @@ describe('ZTI', () => { test('uses certs when available at construction', async () => { ctx.writeSVIDFiles() - const { getRotatingCertAgentClass, initializeZTI, loadInitialCerts, reset } = await import('../lib/zti.js') + const { getRotatingCertAgentClass, reset } = await import('../lib/utils/mtls.js') + const { initializeZTI, loadInitialCerts } = await import('../lib/zti.js') initializeZTI() loadInitialCerts() @@ -254,27 +253,28 @@ describe('ZTI', () => { describe('certsAvailable', () => { test('returns false when no certs and no SVID files', async () => { cds.env.requires.telemetry = {} - const { certsAvailable } = await import('../lib/zti.js') + const { certsAvailable } = await import('../lib/utils/mtls.js') expect(certsAvailable()).toBe(false) }) - test('returns true when SVID files exist (ZTI enabled)', async () => { + test('returns true once SVID certs are loaded into cds.env (ZTI bound)', async () => { ctx.writeSVIDFiles() vi.resetModules() - const { certsAvailable, initializeZTI } = await import('../lib/zti.js') + const { certsAvailable } = await import('../lib/utils/mtls.js') + const { initializeZTI, loadInitialCerts } = await import('../lib/zti.js') initializeZTI() + loadInitialCerts() expect(certsAvailable()).toBe(true) }) - test('returns true when certs in cds.env and ZTI disabled', async () => { - process.env.TELEMETRY_USE_ZTI = 'false' + test('returns true when static x509 certs are in cds.env', async () => { cds.env.requires = { telemetry: { x509: { cert: CERT_V1, key: KEY_V1 } } } vi.resetModules() - const { certsAvailable } = await import('../lib/zti.js') + const { certsAvailable } = await import('../lib/utils/mtls.js') expect(certsAvailable()).toBe(true) }) }) @@ -288,7 +288,8 @@ describe('ZTI', () => { test('agent rotates certificate when svid event is emitted', async () => { ctx.writeSVIDFiles() - const { getRotatingAgentFactory, initializeZTI, loadInitialCerts, reset } = await import('../lib/zti.js') + const { getRotatingAgentFactory, reset } = await import('../lib/utils/mtls.js') + const { initializeZTI, loadInitialCerts } = await import('../lib/zti.js') initializeZTI() loadInitialCerts() @@ -313,7 +314,8 @@ describe('ZTI', () => { test('agent rotates by re-reading cds.env when svid event has no payload', async () => { ctx.writeSVIDFiles() - const { getRotatingAgentFactory, initializeZTI, loadInitialCerts, reset } = await import('../lib/zti.js') + const { getRotatingAgentFactory, reset } = await import('../lib/utils/mtls.js') + const { initializeZTI, loadInitialCerts } = await import('../lib/zti.js') initializeZTI() loadInitialCerts() @@ -335,7 +337,8 @@ describe('ZTI', () => { test('agent keeps cached cert when rotation event has invalid payload', async () => { ctx.writeSVIDFiles() - const { getRotatingAgentFactory, initializeZTI, loadInitialCerts, reset } = await import('../lib/zti.js') + const { getRotatingAgentFactory, reset } = await import('../lib/utils/mtls.js') + const { initializeZTI, loadInitialCerts } = await import('../lib/zti.js') initializeZTI() loadInitialCerts() @@ -356,61 +359,63 @@ describe('ZTI', () => { }) describe('x509 fallback', () => { - test('augmentCaaSCreds uses ZTI when available', async () => { + test('setup activates ZTI when SVID files are available', async () => { ctx.writeSVIDFiles() - const { augmentCaaSCreds } = await import('../lib/utils.js') - const { reset } = await import('../lib/zti.js') + const { setup } = await import('../lib/zti.js') + const { certsAvailable, reset } = await import('../lib/utils/mtls.js') + const { augmentCaaSCreds } = await import('../lib/utils/credentials.js') + + expect(setup()).toBe(true) + expect(certsAvailable()).toBe(true) const credentials = { otlp: { http: 'https://caas.example.com/otlp' } } augmentCaaSCreds(credentials) - - expect(credentials.httpAgentOptions).toBeDefined() expect(typeof credentials.httpAgentOptions).toBe('function') - expect(credentials.useZTI).toBe(true) reset() }) - test('augmentCaaSCreds uses x509 when ZTI disabled', async () => { - process.env.VCAP_SERVICES = JSON.stringify(MOCK_CAAS_VCAP) - process.env.TELEMETRY_USE_ZTI = 'false' - cds.env.requires.telemetry.x509 = { - cert: Buffer.from('-----BEGIN CERTIFICATE-----\nenvvar-cert\n-----END CERTIFICATE-----').toString('base64'), - key: Buffer.from('-----BEGIN PRIVATE KEY-----\nenvvar-key\n-----END PRIVATE KEY-----').toString('base64') - } + test('setup arms a bootstrap watcher when the SVID dir does not exist yet, loading certs once it appears', async () => { + // Boot race: ZTI is bound but SPIRE has not created the SVID directory yet. setup() must + // still activate and arm a watcher, so certs are picked up when they land — rather than + // reporting async certs with no watcher, which would buffer exports forever. + fs.rmSync(ctx.svidDir, { recursive: true, force: true }) - ctx.clearModuleCache() - const { augmentCaaSCreds } = await import('../lib/utils.js') + const { setup, reset } = await import('../lib/zti.js') + const { certsAvailable } = await import('../lib/utils/mtls.js') - const credentials = { otlp: { http: 'https://caas.example.com/otlp' } } - augmentCaaSCreds(credentials) + expect(setup()).toBe(true) + expect(certsAvailable()).toBe(false) // nothing to load yet - expect(credentials.httpAgentOptions).toBeDefined() - expect(typeof credentials.httpAgentOptions).toBe('function') - expect(credentials.useZTI).toBe(false) + // SPIRE creates the directory and writes the SVID shortly after startup; the bootstrap + // watcher (on the parent dir) then hands off to the real watcher and loads the certs. + fs.mkdirSync(ctx.svidDir) + ctx.writeSVIDFiles() + + await vi.waitFor(() => expect(certsAvailable()).toBe(true), { timeout: 4000, interval: 100 }) + + reset() }) - test('augmentCaaSCreds uses x509 when ZTI enabled-by-default but not bound', async () => { - // Manual-cert path as documented in the README: static x509 credentials, no - // zero-trust-identity binding, and TELEMETRY_USE_ZTI left at its default (enabled). - // certsAvailable() must still recognize the static certs so createCaaSExporter does - // not reject the setup as "missing mTLS". + test('setup falls back to static x509 when ZTI is not bound', async () => { + // Manual-cert path as documented in the README: static x509 credentials and no + // zero-trust-identity binding. setup() reports ZTI inactive, and certsAvailable() must + // still recognize the static certs so createCaaSExporter does not reject the setup as + // "missing mTLS". process.env.VCAP_SERVICES = JSON.stringify(MOCK_CAAS_VCAP) - delete process.env.TELEMETRY_USE_ZTI cds.env.requires.telemetry.x509 = { cert: Buffer.from('-----BEGIN CERTIFICATE-----\nenvvar-cert\n-----END CERTIFICATE-----').toString('base64'), key: Buffer.from('-----BEGIN PRIVATE KEY-----\nenvvar-key\n-----END PRIVATE KEY-----').toString('base64') } ctx.clearModuleCache() - const { augmentCaaSCreds } = await import('../lib/utils.js') - const { certsAvailable } = await import('../lib/zti.js') + const { setup } = await import('../lib/zti.js') + const { setupStaticCerts } = await import('../lib/utils/mtls.js') + const { certsAvailable } = await import('../lib/utils/mtls.js') - const credentials = { otlp: { http: 'https://caas.example.com/otlp' } } - augmentCaaSCreds(credentials) - - expect(credentials.useZTI).toBe(false) + expect(setup()).toBe(false) + expect(setupStaticCerts()).toBe(true) expect(certsAvailable()).toBe(true) }) }) @@ -485,9 +490,13 @@ describe('CaaS integration', () => { } const { AggregationTemporality, InstrumentType } = await import('@opentelemetry/sdk-metrics') - const { getResource } = await import('../lib/utils.js') + const { getResource } = await import('../lib/utils/resource.js') + const { setup } = await import('../lib/zti.js') const metricsSetup = await import('../lib/metrics/index.js') + // Provision ZTI certs as lib/index.js does at startup (this test drives metrics() directly) + setup() + // Setup metrics with resource const resource = getResource() const meterProvider = metricsSetup.default(resource) From 142c59b590ba6873fba34fce13e419e91622be8a Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:50:46 +0200 Subject: [PATCH 27/31] fix(tracing): don't build a span processor without an exporter on the OneAgent path (#504) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Prevents a startup crash on the Dynatrace OneAgent path and clarifies how to get traces into Dynatrace. When Dynatrace OneAgent is active (`DT_NODE_PRELOAD_OPTIONS` set), the kind is `*-to-dynatrace`, and `@opentelemetry/exporter-trace-otlp-proto` is **not** a dependency, the 2.x tracing factory built a `NodeTracerProvider` whose only span processor was `undefined` (no exporter is configured on this path, so `processor` stayed unset). The provider constructs fine, but `MultiSpanProcessor.onStart` dereferences the undefined processor on the **first span** → the app crashes on startup (CF exit 137, crash loop). ## Fix On the OneAgent path the factory now sets up nothing and returns — no exporter, no processor, no provider, nothing to crash on. It does **not** register or clobber a global tracer provider. `@cap-js/telemetry` does not export traces on this path, and it logs a warning at startup pointing to the supported route: > **Until further notice, export traces to Dynatrace via the OTLP exporter.** Add `@opentelemetry/exporter-trace-otlp-proto` as a dependency (which flips `via_one_agent` off and takes the normal OTLP export path). See the [Dynatrace OneAgent](https://github.com/cap-js/telemetry/blob/develop/README.md#dynatrace-oneagent) section of the README. The earlier assumption — that OneAgent's already-registered global provider would capture `@cap-js/telemetry`'s spans automatically — did not hold in practice (OneAgent registers no in-process OTel provider on the tested runtime), so the code comment, README, and CHANGELOG no longer claim it. ## Changes - `lib/tracing/index.js`: OneAgent path returns early without building an exporter/processor/provider; warns. - `test/tracing-one-agent.test.js`: regression test — an undefined span processor crashes on the first span; the OneAgent path sets up nothing, returns `undefined`, leaves the global delegate untouched, and does not crash on span creation. - `README.md`: "Dynatrace OneAgent" section rewritten — OTLP export is required until further notice; dropped the "OneAgent transports the spans automatically" claim and the stale sampler/propagator caveats. - `CHANGELOG.md`: crash-fix entry under `### Fixed`. Closes #502 --- CHANGELOG.md | 1 + README.md | 17 +++----- lib/tracing/index.js | 32 ++++++++------ test/tracing-one-agent.test.js | 76 ++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+), 23 deletions(-) create mode 100644 test/tracing-one-agent.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 35b90746..1dc676b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Fixed +- No longer crashes on startup when Dynatrace OneAgent is active (`DT_NODE_PRELOAD_OPTIONS` set, kind `*-to-dynatrace`, no `@opentelemetry/exporter-trace-otlp-proto` dependency): the tracing factory no longer builds a tracer provider with an undefined span processor (which crashed on the first span in `MultiSpanProcessor.onStart`). On this path it now sets up no exporter and warns; export traces to Dynatrace via the OTLP exporter (`@opentelemetry/exporter-trace-otlp-proto`) - Logging no longer recurses through `@opentelemetry/sdk-logs` 0.221's export path: the log-processor construction now adapts to the installed sdk-logs version (0.221+ takes an `{ exporter }` options object, earlier versions the positional exporter), and a re-entrancy guard was added to the `cds.log.format` interception - Cloud SDK outbound requests are traced again (patch getter-only `@sap-cloud-sdk/http-client` exports via `Object.defineProperty`) - Raw SQL no longer leaks into HANA INSERT `prepare` span names (now uses operation + table, matching SELECT) diff --git a/README.md b/README.md index 5b0da11b..3126932a 100644 --- a/README.md +++ b/README.md @@ -96,8 +96,7 @@ Example trace in Dynatrace: An example trace printed to the console can be found in [`telemetry-to-console`](#telemetry-to-console). -In environments where Dynatrace OneAgent is installed (e.g., SAP BTP CF), no OpenTelemetry exporter is needed to transport the traces to Dynatrace. -`@cap-js/telemetry` recognizes this and ignores any exporter config if the predefined kind [`telemetry-to-dynatrace`](#telemetry-to-dynatrace) is used. +To export traces to Dynatrace, use the predefined kind [`telemetry-to-dynatrace`](#telemetry-to-dynatrace) with the OTLP trace exporter — see [Dynatrace OneAgent](#dynatrace-oneagent) for why this is required even when OneAgent is present. ### Metrics @@ -221,7 +220,7 @@ Hence, a Dynatrace instance is required and the app must be bound to that Dynatr Use via `cds.requires.telemetry.kind = 'to-dynatrace'`. Required additional dependencies: -- `@opentelemetry/exporter-trace-otlp-proto` (optional, see [Leveraging Dynatrace OneAgent](#leveraging-dynatrace-oneagent)) +- `@opentelemetry/exporter-trace-otlp-proto` - `@opentelemetry/exporter-metrics-otlp-proto` The necessary scopes for exporting traces (`openTelemetryTrace.ingest`) and metrics (`metrics.ingest`) are not part of the standard `apitoken` and must be requested. @@ -250,16 +249,12 @@ In Dynatrace itself, you need to ensure that the following two features are enab - From the Dynatrace menu, go to Settings > Server-side service monitoring > Deep monitoring > Distributed tracing. - Turn on Send W3C Trace Context HTTP headers. -#### Leveraging Dynatrace OneAgent - -If [Dynatrace OneAgent](https://www.dynatrace.com/platform/oneagent) is present, for example on SAP BTP CF, it will collect and transport the traces created by `@cap-js/telemetry` automatically. -(Your app still needs to be bound to a Dynatrace instance, of course. However, `@dynatrace/oneagent-sdk` is not required.) -Hence, additional dependency `@opentelemetry/exporter-trace-otlp-proto` and scope `openTelemetryTrace.ingest` are not required. +#### Dynatrace OneAgent -Please note, however, that Dynatrace only exports traces triggered by incoming HTTP requests. -That is, traces for background tasks started by `cds.spawn`, for example, would not be exported. +If [Dynatrace OneAgent](https://www.dynatrace.com/platform/oneagent) is present, for example on SAP BTP CF, do **not** rely on it to transport the spans created by `@cap-js/telemetry`. +**Until further notice, traces must be exported to Dynatrace via the OTLP exporter**: `@opentelemetry/exporter-trace-otlp-proto` must be a dependency (as listed under [Required additional dependencies](#telemetry-to-dynatrace) above) and the `openTelemetryTrace.ingest` scope must be granted. -If dependency `@opentelemetry/exporter-trace-otlp-proto` is present anyway, `@cap-js/telemetry` will export the traces via OpenTelemetry as well. +If OneAgent is active but the OTLP trace exporter is *not* installed, `@cap-js/telemetry` exports no traces and logs a warning at startup — only OneAgent's own instrumentation is captured, not the CDS spans. ### `telemetry-to-cloud-logging` diff --git a/lib/tracing/index.js b/lib/tracing/index.js index 18360eb2..eb704428 100644 --- a/lib/tracing/index.js +++ b/lib/tracing/index.js @@ -160,25 +160,33 @@ module.exports = resource => { require('./cds')() require('./cloud_sdk')() - /* - * create processor - */ - let processor + // Dynatrace OneAgent present, kind targets Dynatrace, and no OTLP trace exporter is installed. const via_one_agent = process.env.DT_NODE_PRELOAD_OPTIONS && cds.env.requires.telemetry.kind.match(/to-dynatrace$/) && !hasDependency('@opentelemetry/exporter-trace-otlp-proto') if (via_one_agent) { - // if Dynatrace OneAgent is present, no exporter is needed - LOG._info && LOG.info('Dynatrace OneAgent detected, disabling trace exporter') - } else { - const exporter = _getExporter() - const { kind: processorKind, config: processorConfig = {} } = cds.env.requires.telemetry.tracing.processor || {} - const processors = { BatchSpanProcessor, SimpleSpanProcessor } - if (!processors[processorKind]) throw new Error(`Unknown span processor ${processorKind}`) - processor = new processors[processorKind](exporter, processorConfig) + // No exporter is configured on this path, so there is nothing to build a span processor + // around — passing an undefined processor to the provider would crash on the first span. + // Set up nothing and return; @cap-js/telemetry does not export traces here. To get traces + // into Dynatrace, install @opentelemetry/exporter-trace-otlp-proto, which flips this flag + // off and takes the OTLP export path below. + LOG.warn( + 'Dynatrace OneAgent detected without an OTLP trace exporter: @cap-js/telemetry will not export traces. ' + + 'Add @opentelemetry/exporter-trace-otlp-proto to export traces to Dynatrace.' + ) + return } + /* + * create processor + */ + const exporter = _getExporter() + const { kind: processorKind, config: processorConfig = {} } = cds.env.requires.telemetry.tracing.processor || {} + const processors = { BatchSpanProcessor, SimpleSpanProcessor } + if (!processors[processorKind]) throw new Error(`Unknown span processor ${processorKind}`) + const processor = new processors[processorKind](exporter, processorConfig) + /* * either add processor as delegate in CALM... */ diff --git a/test/tracing-one-agent.test.js b/test/tracing-one-agent.test.js new file mode 100644 index 00000000..4444caaa --- /dev/null +++ b/test/tracing-one-agent.test.js @@ -0,0 +1,76 @@ +// Regression test: when Dynatrace OneAgent is active (`via_one_agent`), the tracing factory must +// NOT build a tracer provider with an undefined span processor. +// +// 2.x constructs the provider via the OTel 2.0 constructor `new NodeTracerProvider({ spanProcessors })`. +// On the OneAgent path no exporter/processor is created, so `processor` stayed `undefined` and +// `[undefined]` was handed to the provider — MultiSpanProcessor.onStart then dereferenced undefined +// on the FIRST span and the app crashed on startup (CF exit 137, crash loop). +// +// The fix: on the OneAgent path the factory sets up nothing and returns — no exporter, no processor, +// no provider, nothing to crash on. It deliberately does not export traces here (export to Dynatrace +// goes via the OTLP exporter instead) and must not register or clobber a global tracer provider. +// +// We drive the factory directly rather than through a full boot: exercising `via_one_agent` needs +// kind `*-to-dynatrace` (whose metrics/tracing exporters would otherwise demand real Dynatrace +// credentials at boot). Booting once with the in-memory tracing profile populates `cds.env` and +// caches lib/tracing; the tests then flip `cds.env.requires.telemetry.kind` + the env flag and call +// the factory in isolation. +const cds = require('@sap/cds') +const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') + +const otel = require('@opentelemetry/api') +const { resourceFromAttributes } = require('@opentelemetry/resources') +const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node') + +const { hasDependency } = require('../lib/utils/common') +const setupTracing = require('../lib/tracing') + +describe('tracing setup with Dynatrace OneAgent', () => { + const OTLP_PROTO = '@opentelemetry/exporter-trace-otlp-proto' + + test('a provider with an undefined span processor crashes on the first span', () => { + // This is the failure mode the OneAgent path must avoid: a provider whose only span processor + // is `undefined`. It builds fine, but the first span crashes in onStart. + const provider = new NodeTracerProvider({ + resource: resourceFromAttributes({}), + spanProcessors: [undefined] + }) + expect(() => provider.getTracer('probe').startSpan('boom')).toThrow(/onStart/) + }) + + test('OneAgent path: sets up nothing, returns, and never touches an undefined processor', () => { + // Precondition for `via_one_agent`: the otlp-proto exporter must NOT be a (production) dependency + // — it is only a devDependency here, and hasDependency() checks `dependencies` only. + expect(hasDependency(OTLP_PROTO)).toBe(false) + + const proxy = otel.trace.getTracerProvider() // the process-global ProxyTracerProvider + const originalDelegate = proxy.getDelegate() + + const savedKind = cds.env.requires.telemetry.kind + const savedEnv = process.env.DT_NODE_PRELOAD_OPTIONS + process.env.DT_NODE_PRELOAD_OPTIONS = '{}' + cds.env.requires.telemetry.kind = 'telemetry-to-dynatrace' + + try { + // Must not throw. We pass a resource so it is the standalone path (a falsy resource is the + // CALM path). + let returned + expect(() => { + returned = setupTracing(resourceFromAttributes({})) + }).not.toThrow() + + // The factory returns nothing and registers no provider of its own — the global delegate is + // left exactly as it was. + expect(returned).toBeUndefined() + expect(proxy.getDelegate()).toBe(originalDelegate) + + // And a span created via the global API (as lib/tracing/trace.js does for every CDS span) + // does not crash — with no real provider registered it is simply a non-recording span. + expect(() => otel.trace.getTracer('@cap-js/telemetry').startSpan('cds-span').end()).not.toThrow() + } finally { + cds.env.requires.telemetry.kind = savedKind + if (savedEnv === undefined) delete process.env.DT_NODE_PRELOAD_OPTIONS + else process.env.DT_NODE_PRELOAD_OPTIONS = savedEnv + } + }) +}) From 3ac0409a135e2b42ac1f0489bcf5471cf70dab7b Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:00:04 +0200 Subject: [PATCH 28/31] feat(tracing): support Dynatrace OneAgent in-process trace capture (#512) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #509. Follow-up to #502 / #504. ## What this changes On the OneAgent path — Dynatrace OneAgent present (`DT_NODE_PRELOAD_OPTIONS` set), kind `*-to-dynatrace`, and no `@opentelemetry/exporter-trace-otlp-proto` installed — the tracing factory now **registers a recording `NodeTracerProvider` with an empty span-processor list** instead of warning and bailing out. The provider records spans but exports nothing itself, so the CDS spans actually come into existence for OneAgent's in-process OpenTelemetry capture to pick up — without us exporting (and thus duplicating) them. The factory's branching was refactored around two orthogonal axes — `via_one_agent` (OneAgent present, no exporter) and `resource` (truthy = standalone, falsy = CALM under `@sap/xotel-agent-ext-js`): | | standalone (`resource`) | CALM (no `resource`) | | --- | --- | --- | | **not OneAgent** | register provider with the configured exporter's processor | delegate the processor to `@sap/xotel-agent-ext-js` | | **OneAgent** | register recording provider, no processor (in-process capture) | rejected — `throw` | The **CALM + OneAgent** combination is now rejected fail-fast: two agents owning tracing at once is a contradictory, unverified setup, so the factory throws rather than silently doing nothing. This is the plugin-side prerequisite for OneAgent in-process capture. It is correct and necessary — but, as the analysis below shows, it is **not sufficient on its own** with current OneAgent versions. ## Analysis / findings I live-verified this against a OneAgent-injected app on SAP BTP CF (classic-managed Dynatrace tenant, OneAgent 1.339). Findings: **OneAgent's in-process OTel capture is version-sensitive to the OpenTelemetry JS SDK layout.** OneAgent instruments OTel-JS by wrapping `getTracer` on the class defined in `@opentelemetry/sdk-trace-base`'s `BasicTracerProvider.js`, then applying its span transformer to the returned tracer. (Its other, deeper `Tracer`/`Span` patch is version-gated to SDK `^1.0.0` and is skipped on 2.x.) **OpenTelemetry JS SDK `sdk-trace-base` 2.9.0 restructured the packages:** `getTracer` moved out of `sdk-trace-base` into a new package `@opentelemetry/sdk-trace` (`TracerProvider.js`), and `BasicTracerProvider.js` became a thin re-export `BasicTracerProvider-shim.js`. On ≥2.9 there is no `BasicTracerProvider.js` for OneAgent to match, and it has no rule for the new `@opentelemetry/sdk-trace` package — so the `getTracer` wrap never attaches and the CDS `INTERNAL` spans are never captured. Exact, empirically confirmed boundary: | `@opentelemetry/sdk-trace-base` | Provider layout | OneAgent 1.339 in-process capture | | --- | --- | --- | | ≤ 2.8.0 | real `BasicTracerProvider.js` with `getTracer` | ✅ captures the full CDS span tree | | ≥ 2.9.0 | `BasicTracerProvider-shim.js` + split into `@opentelemetry/sdk-trace` | ❌ only the HTTP entry point (OneAgent's native sensor) | **Clean A/B on the same app + same OneAgent:** - With this fix and the plugin's declared `@opentelemetry/sdk-trace-* ^2.8` floating to **2.11.0** (post-split): only the HTTP PurePath appears — no CDS spans. - Pinning the trace SDK to **2.8.0** (last pre-split): the full CDS tree nests under the HTTP request — `CatalogService - tx` → `CatalogService - READ …` → `db - READ …` → `@cap-js/sqlite - prepare/stmt.all SELECT …` (scope `@cap-js/telemetry`, `SpanKind.INTERNAL`). The HTTP entry point is captured in both cases because that is OneAgent's own HTTP sensor, not OTel. **Conclusion.** This is an **OneAgent-side gap**: OneAgent has not yet been updated for the OTel-JS SDK 2.9 package restructure. The full fix is on the Dynatrace side (OneAgent needs to instrument the current layout — `@opentelemetry/sdk-trace`'s `TracerProvider.js` / `Tracer.js` / `Span.js`, or relax the deep-patch version gate to `^2.0.0`). A support ticket with Dynatrace references this PR. Dependencies are kept at `^2.8` (floating to 2.x latest) — pinning the plugin to the last pre-split patch to work around a OneAgent bug would freeze the OTel SDK and is not a maintainable trade-off. Once OneAgent adds support for the current layout, in-process capture works with no further change here. ## Docs - **README** (Dynatrace OneAgent section): documents in-process capture as the supported path — no exporter needed when OneAgent is present, or install the OTLP exporter to have the plugin export the spans itself (which takes precedence). - **CHANGELOG**: unreleased 2.1.0 entry describes the recording-provider behavior. The temporary pin that works around the current OneAgent gap is posted as a workaround comment on #509 rather than baked into the README, so it can simply be dropped once OneAgent catches up. ## Tests `test/tracing-one-agent.test.js` asserts, without a live tenant: - the illustrative crash mode — a provider whose only processor is `undefined` throws on the first span (why the OneAgent path uses `[]`, not `[processor]`); - `@opentelemetry/exporter-trace-otlp-proto` is not a production dependency (precondition for `via_one_agent`); - standalone + OneAgent — the factory returns a real `NodeTracerProvider` whose spans are **recording** (not `NonRecordingSpan`) and whose creation/end never crashes; - CALM + OneAgent — rejected as an unsupported combination (throws). --- CHANGELOG.md | 2 +- README.md | 7 +-- lib/tracing/index.js | 45 ++++++++------- test/tracing-one-agent.test.js | 100 +++++++++++++++++++-------------- 4 files changed, 85 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1dc676b1..c4112a94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Fixed -- No longer crashes on startup when Dynatrace OneAgent is active (`DT_NODE_PRELOAD_OPTIONS` set, kind `*-to-dynatrace`, no `@opentelemetry/exporter-trace-otlp-proto` dependency): the tracing factory no longer builds a tracer provider with an undefined span processor (which crashed on the first span in `MultiSpanProcessor.onStart`). On this path it now sets up no exporter and warns; export traces to Dynatrace via the OTLP exporter (`@opentelemetry/exporter-trace-otlp-proto`) +- No longer crashes on startup when Dynatrace OneAgent is active (`DT_NODE_PRELOAD_OPTIONS` set, kind `*-to-dynatrace`, no `@opentelemetry/exporter-trace-otlp-proto` dependency): the tracing factory no longer builds a tracer provider with an undefined span processor (which crashed on the first span in `MultiSpanProcessor.onStart`). On this path it now registers a recording tracer provider without an exporter, so the CDS spans exist for OneAgent's in-process OpenTelemetry capture to pick up (no export by us, no duplicates) - Logging no longer recurses through `@opentelemetry/sdk-logs` 0.221's export path: the log-processor construction now adapts to the installed sdk-logs version (0.221+ takes an `{ exporter }` options object, earlier versions the positional exporter), and a re-entrancy guard was added to the `cds.log.format` interception - Cloud SDK outbound requests are traced again (patch getter-only `@sap-cloud-sdk/http-client` exports via `Object.defineProperty`) - Raw SQL no longer leaks into HANA INSERT `prepare` span names (now uses operation + table, matching SELECT) diff --git a/README.md b/README.md index 3126932a..26a0737a 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Example trace in Dynatrace: An example trace printed to the console can be found in [`telemetry-to-console`](#telemetry-to-console). -To export traces to Dynatrace, use the predefined kind [`telemetry-to-dynatrace`](#telemetry-to-dynatrace) with the OTLP trace exporter — see [Dynatrace OneAgent](#dynatrace-oneagent) for why this is required even when OneAgent is present. +To export traces to Dynatrace, use the predefined kind [`telemetry-to-dynatrace`](#telemetry-to-dynatrace). If [Dynatrace OneAgent](#dynatrace-oneagent) is present, the CDS spans can alternatively be captured in-process by OneAgent, without the OTLP trace exporter. ### Metrics @@ -251,10 +251,9 @@ In Dynatrace itself, you need to ensure that the following two features are enab #### Dynatrace OneAgent -If [Dynatrace OneAgent](https://www.dynatrace.com/platform/oneagent) is present, for example on SAP BTP CF, do **not** rely on it to transport the spans created by `@cap-js/telemetry`. -**Until further notice, traces must be exported to Dynatrace via the OTLP exporter**: `@opentelemetry/exporter-trace-otlp-proto` must be a dependency (as listed under [Required additional dependencies](#telemetry-to-dynatrace) above) and the `openTelemetryTrace.ingest` scope must be granted. +If [Dynatrace OneAgent](https://www.dynatrace.com/platform/oneagent) is present, for example on SAP BTP CF, and the OTLP trace exporter is *not* installed, `@cap-js/telemetry` does not export traces itself. Instead it registers a recording tracer provider without an exporter and relies on OneAgent's in-process OpenTelemetry capture to pick up the CDS spans — no OTLP export, no duplicate spans. -If OneAgent is active but the OTLP trace exporter is *not* installed, `@cap-js/telemetry` exports no traces and logs a warning at startup — only OneAgent's own instrumentation is captured, not the CDS spans. +Installing the OTLP trace exporter (`@opentelemetry/exporter-trace-otlp-proto`, see [Required additional dependencies](#telemetry-to-dynatrace) above) takes precedence: `@cap-js/telemetry` then exports the spans itself instead of relying on in-process capture, and the `openTelemetryTrace.ingest` scope must be granted. ### `telemetry-to-cloud-logging` diff --git a/lib/tracing/index.js b/lib/tracing/index.js index eb704428..44a9b08c 100644 --- a/lib/tracing/index.js +++ b/lib/tracing/index.js @@ -160,53 +160,56 @@ module.exports = resource => { require('./cds')() require('./cloud_sdk')() - // Dynatrace OneAgent present, kind targets Dynatrace, and no OTLP trace exporter is installed. + // On the OneAgent path — Dynatrace OneAgent present (DT_NODE_PRELOAD_OPTIONS), kind *-to-dynatrace, + // no OTLP trace exporter installed — we don't export spans ourselves: OneAgent captures them + // in-process, and exporting would only duplicate them. So no span processor is built on this path; + // everywhere else we build one around the configured exporter. const via_one_agent = process.env.DT_NODE_PRELOAD_OPTIONS && cds.env.requires.telemetry.kind.match(/to-dynatrace$/) && !hasDependency('@opentelemetry/exporter-trace-otlp-proto') - if (via_one_agent) { - // No exporter is configured on this path, so there is nothing to build a span processor - // around — passing an undefined processor to the provider would crash on the first span. - // Set up nothing and return; @cap-js/telemetry does not export traces here. To get traces - // into Dynatrace, install @opentelemetry/exporter-trace-otlp-proto, which flips this flag - // off and takes the OTLP export path below. - LOG.warn( - 'Dynatrace OneAgent detected without an OTLP trace exporter: @cap-js/telemetry will not export traces. ' + - 'Add @opentelemetry/exporter-trace-otlp-proto to export traces to Dynatrace.' - ) - return - } + + if (!resource && via_one_agent) + throw new Error('Dynatrace OneAgent with @sap/xotel-agent-ext-js (CALM) is not supported') /* * create processor */ - const exporter = _getExporter() - const { kind: processorKind, config: processorConfig = {} } = cds.env.requires.telemetry.tracing.processor || {} - const processors = { BatchSpanProcessor, SimpleSpanProcessor } - if (!processors[processorKind]) throw new Error(`Unknown span processor ${processorKind}`) - const processor = new processors[processorKind](exporter, processorConfig) + let processor + if (via_one_agent) { + LOG._info && LOG.info('Dynatrace OneAgent detected: relying on in-process trace capture') + } else { + const exporter = _getExporter() + const { kind: processorKind, config: processorConfig = {} } = cds.env.requires.telemetry.tracing.processor || {} + const processors = { BatchSpanProcessor, SimpleSpanProcessor } + if (!processors[processorKind]) throw new Error(`Unknown span processor ${processorKind}`) + processor = new processors[processorKind](exporter, processorConfig) + } /* * either add processor as delegate in CALM... */ if (!resource) { - LOG.warn("@sap/xotel-agent-ext-js found, adding @cap-js/telemetry's span processor as delegate") + LOG._info && LOG.info("@sap/xotel-agent-ext-js found, adding @cap-js/telemetry's span processor as delegate") try { const { getCompositeSpanProcessor } = require('@sap/xotel-agent-ext-js') getCompositeSpanProcessor().addDelegate(processor) - return } catch (error) { LOG.error('Failed to add span processor as delegate:', error) throw error } + return } /* * ... or initialize and return provider */ resource = resourceFromAttributes({}).merge(resource).merge(getDynatraceMetadata()) - const tracerProvider = new NodeTracerProvider({ resource, spanProcessors: [processor], sampler: _getSampler() }) + const tracerProvider = new NodeTracerProvider({ + resource, + spanProcessors: processor ? [processor] : [], + sampler: _getSampler() + }) tracerProvider.register({ propagator: _getPropagator() }) return tracerProvider } diff --git a/test/tracing-one-agent.test.js b/test/tracing-one-agent.test.js index 4444caaa..a84bd08f 100644 --- a/test/tracing-one-agent.test.js +++ b/test/tracing-one-agent.test.js @@ -1,24 +1,23 @@ -// Regression test: when Dynatrace OneAgent is active (`via_one_agent`), the tracing factory must -// NOT build a tracer provider with an undefined span processor. +// When Dynatrace OneAgent is active (`via_one_agent`), OneAgent captures OpenTelemetry spans +// in-process — so instead of exporting traces ourselves (which would duplicate them), the tracing +// factory registers a real, recording tracer provider with an EMPTY span-processor list. That +// provider records spans (so the global tracer used by lib/tracing/trace.js is no longer a no-op) +// but adds no export path of our own; OneAgent's preload hooks observe the recorded spans. // -// 2.x constructs the provider via the OTel 2.0 constructor `new NodeTracerProvider({ spanProcessors })`. -// On the OneAgent path no exporter/processor is created, so `processor` stayed `undefined` and -// `[undefined]` was handed to the provider — MultiSpanProcessor.onStart then dereferenced undefined -// on the FIRST span and the app crashed on startup (CF exit 137, crash loop). -// -// The fix: on the OneAgent path the factory sets up nothing and returns — no exporter, no processor, -// no provider, nothing to crash on. It deliberately does not export traces here (export to Dynatrace -// goes via the OTLP exporter instead) and must not register or clobber a global tracer provider. +// Two properties must hold and are asserted here without a live Dynatrace tenant: +// 1. crash-safety — an EMPTY processor list `[]` must not crash on the first span, unlike the +// `[undefined]` a naive "no exporter -> no processor" path would have produced (which crashed +// in MultiSpanProcessor.onStart); +// 2. the provider is real and RECORDING — a provider that doesn't record would leave CDS spans +// non-existent, which is the regression this path must avoid. // // We drive the factory directly rather than through a full boot: exercising `via_one_agent` needs -// kind `*-to-dynatrace` (whose metrics/tracing exporters would otherwise demand real Dynatrace -// credentials at boot). Booting once with the in-memory tracing profile populates `cds.env` and -// caches lib/tracing; the tests then flip `cds.env.requires.telemetry.kind` + the env flag and call -// the factory in isolation. +// kind `*-to-dynatrace` (whose exporters would otherwise demand real Dynatrace credentials at boot). +// Booting once with the in-memory tracing profile populates `cds.env` and caches lib/tracing; the +// tests then flip `cds.env.requires.telemetry.kind` + the env flag and call the factory in isolation. const cds = require('@sap/cds') const { expect } = cds.test(__dirname + '/bookshop', '--profile', 'tracing-in-memory') -const otel = require('@opentelemetry/api') const { resourceFromAttributes } = require('@opentelemetry/resources') const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node') @@ -28,9 +27,10 @@ const setupTracing = require('../lib/tracing') describe('tracing setup with Dynatrace OneAgent', () => { const OTLP_PROTO = '@opentelemetry/exporter-trace-otlp-proto' + // Guard on the illustrative failure mode: a provider whose only span processor is `undefined` + // builds fine but crashes on the first span in onStart. It is the reason the OneAgent path passes + // an empty list `[]` rather than `[processor]` with a missing processor. test('a provider with an undefined span processor crashes on the first span', () => { - // This is the failure mode the OneAgent path must avoid: a provider whose only span processor - // is `undefined`. It builds fine, but the first span crashes in onStart. const provider = new NodeTracerProvider({ resource: resourceFromAttributes({}), spanProcessors: [undefined] @@ -38,39 +38,53 @@ describe('tracing setup with Dynatrace OneAgent', () => { expect(() => provider.getTracer('probe').startSpan('boom')).toThrow(/onStart/) }) - test('OneAgent path: sets up nothing, returns, and never touches an undefined processor', () => { - // Precondition for `via_one_agent`: the otlp-proto exporter must NOT be a (production) dependency - // — it is only a devDependency here, and hasDependency() checks `dependencies` only. + // Precondition for `via_one_agent`: the otlp-proto exporter must NOT be a (production) dependency + // — it is only a devDependency here, and hasDependency() checks `dependencies` only. With it + // present, `via_one_agent` would be false and the normal OTLP export path taken instead. + test('otlp-proto exporter is not a production dependency (so via_one_agent can be true)', () => { expect(hasDependency(OTLP_PROTO)).toBe(false) + }) - const proxy = otel.trace.getTracerProvider() // the process-global ProxyTracerProvider - const originalDelegate = proxy.getDelegate() + describe('OneAgent path', () => { + let savedKind, savedEnv + + beforeEach(() => { + savedKind = cds.env.requires.telemetry.kind + savedEnv = process.env.DT_NODE_PRELOAD_OPTIONS + process.env.DT_NODE_PRELOAD_OPTIONS = '{}' + cds.env.requires.telemetry.kind = 'telemetry-to-dynatrace' + }) - const savedKind = cds.env.requires.telemetry.kind - const savedEnv = process.env.DT_NODE_PRELOAD_OPTIONS - process.env.DT_NODE_PRELOAD_OPTIONS = '{}' - cds.env.requires.telemetry.kind = 'telemetry-to-dynatrace' + afterEach(() => { + cds.env.requires.telemetry.kind = savedKind + if (savedEnv === undefined) delete process.env.DT_NODE_PRELOAD_OPTIONS + else process.env.DT_NODE_PRELOAD_OPTIONS = savedEnv + }) - try { - // Must not throw. We pass a resource so it is the standalone path (a falsy resource is the - // CALM path). - let returned + test('standalone: registers a real, recording provider with no export path, and does not crash', () => { + // A truthy resource is the standalone path (a falsy resource is the CALM path). + let provider expect(() => { - returned = setupTracing(resourceFromAttributes({})) + provider = setupTracing(resourceFromAttributes({})) }).not.toThrow() - // The factory returns nothing and registers no provider of its own — the global delegate is - // left exactly as it was. - expect(returned).toBeUndefined() - expect(proxy.getDelegate()).toBe(originalDelegate) + // A real provider is returned (not undefined as on the pre-fix regression) ... + expect(provider).toBeInstanceOf(NodeTracerProvider) - // And a span created via the global API (as lib/tracing/trace.js does for every CDS span) - // does not crash — with no real provider registered it is simply a non-recording span. - expect(() => otel.trace.getTracer('@cap-js/telemetry').startSpan('cds-span').end()).not.toThrow() - } finally { - cds.env.requires.telemetry.kind = savedKind - if (savedEnv === undefined) delete process.env.DT_NODE_PRELOAD_OPTIONS - else process.env.DT_NODE_PRELOAD_OPTIONS = savedEnv - } + // ... and it records: a span created the way lib/tracing/trace.js creates them is a real, + // recording span — not a NonRecordingSpan — so CDS spans actually come into existence for + // OneAgent to capture. The empty processor list means creating and ending it never crashes. + const span = provider.getTracer('@cap-js/telemetry').startSpan('cds-span') + expect(span.constructor.name).not.toBe('NonRecordingSpan') + expect(span.isRecording()).toBe(true) + expect(() => span.end()).not.toThrow() + }) + + test('CALM + OneAgent: rejected as an unsupported combination', () => { + // A falsy resource is the CALM path (@sap/xotel-agent-ext-js owns the provider). Combined with + // OneAgent, two agents would own tracing at once — a contradictory, unverified setup — so the + // factory rejects it rather than silently doing nothing. + expect(() => setupTracing(undefined)).toThrow(/OneAgent with @sap\/xotel-agent-ext-js .* not supported/) + }) }) }) From f7c24e5854283707d6e807ce4a7262404c8a5fb6 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Fri, 18 Sep 2026 22:53:49 +0200 Subject: [PATCH 29/31] docs: add changelog entry for tenant id log attribute (#513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the missing CHANGELOG entry for #506, which shipped to \`develop\` without one. #506 adds the \`sap.tenancy.tenant_id\` attribute to exported log records (from \`cds.context\`), aligning log metadata with traces and metrics. Note: #506's title/description also mentions \`sap.cds.correlation_id\`, but the merged code only adds \`sap.tenancy.tenant_id\` — the entry reflects what actually shipped. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4112a94..e598b9ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). - Support for `telemetry-to-caas` kind for CaaS (Collector as a Service). By default, mTLS certificates are provisioned and rotated automatically via the Zero Trust Identity (ZTI) sidecar; alternatively, mTLS certificates can be provided manually via `x509` credentials. - Queue worker transactions are traced as coherent ` - tx` spans under the `cds.spawn - run task` root, instead of orphaned per-call spans - The span processor is now configurable via `cds.requires.telemetry.tracing.processor = { kind, config? }` (`BatchSpanProcessor` or `SimpleSpanProcessor`); defaults to `BatchSpanProcessor`, and to `SimpleSpanProcessor` in the `[development]` profile +- Exported log records now carry the tenant as attribute `sap.tenancy.tenant_id` (when available from `cds.context`), aligning log metadata with traces and metrics ### Changed From e50079cf5025020f86335f02709781a31f918113 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Tue, 22 Sep 2026 11:42:16 +0200 Subject: [PATCH 30/31] docs: add SAP Cloud ALM (beta) section, carve out telemetry-to-caas (#516) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What - **New section: Running with SAP Cloud ALM (beta).** Documents that when `@sap/xotel-agent-ext-js` is present, the plugin detects it and contributes its CDS spans to the agent's tracing pipeline (delegate on the shared span processor) instead of standing up its own tracer provider — the OTel-2.0-compatible coexistence tracked in #99. Marked beta; refers to SAP Cloud ALM's own docs for agent onboarding; notes the CALM + Dynatrace OneAgent combination is unsupported. - **Carve out `telemetry-to-caas`.** The heavy CaaS deployment recipe (collector instance creation, ZTI sidecar `mta.yaml`, certificate-identity workaround, manual mTLS) is removed from the public README and moved to the internal golden-path docs, where the audience and internal links belong. The kind's TOC entry is removed too. ## Why - CALM coexistence is now demand-driven (#99) and works on OTel 2.0 via the delegate path; documenting it (beta) gives users a supported story. - The CaaS recipe is deployment/onboarding content for an internal offering — it fits golden-path, not the plugin's public API reference. Net: README is +11 / −101 lines. ## Requirements CALM coexistence requires `@sap/xotel-agent-ext-js` 2.0.4 or later. Closes #99 --- README.md | 112 ++++++------------------------------------------------ 1 file changed, 11 insertions(+), 101 deletions(-) diff --git a/README.md b/README.md index 26a0737a..8c5ab59b 100644 --- a/README.md +++ b/README.md @@ -25,9 +25,9 @@ Documentation can be found at [cap.cloud.sap](https://cap.cloud.sap/docs) and [o - [`telemetry-to-console`](#telemetry-to-console) - [`telemetry-to-dynatrace`](#telemetry-to-dynatrace) - [`telemetry-to-cloud-logging`](#telemetry-to-cloud-logging) - - [`telemetry-to-caas`](#telemetry-to-caas) - [`telemetry-to-jaeger`](#telemetry-to-jaeger) - [`telemetry-to-otlp`](#telemetry-to-otlp) +- [Running with SAP Cloud ALM (beta)](#running-with-sap-cloud-alm-beta) - [Detailed Configuration Options](#detailed-configuration-options) - [Configuration Pass Through](#configuration-pass-through) - [Resource Attributes](#resource-attributes) @@ -286,106 +286,6 @@ If you are binding your app to SAP Cloud Logging via a [user-provided service in > For detailed information about binding resolution in CAP, consult [`cds.connect()` → Service Bindings](https://cap.cloud.sap/docs/node.js/cds-connect#service-bindings). -### `telemetry-to-caas` - -Exports traces and metrics to CaaS (Collector as a Service). -CaaS acts as a managed OpenTelemetry Collector that can route telemetry data to downstream backends like SAP Cloud Logging. - -Use via `cds.requires.telemetry.kind = 'to-caas'`. - -Required additional dependencies: -- `@opentelemetry/exporter-trace-otlp-proto` -- `@opentelemetry/exporter-metrics-otlp-proto` - -CaaS needs two things: a **CaaS collector instance** (created with its pipeline config) and an **app deployment** that authenticates to it over mTLS — automatically via ZTI (default), or with a manually provided certificate. - -#### Create the CaaS instance - -The collector's `otelConfig` (receivers, exporters, pipelines) is fixed at creation — a binding can't change it later — so create and configure the instance up front, via the BTP cockpit or the CF CLI: - -```bash -cf create-service caas my-app-caas -c '{ - "otelConfig": { - "receivers": { "otlp": { "protocols": { "grpc": {}, "http": {} } } }, - "exporters": { "otlp/sink": { "endpoint": "" } }, - "service": { "pipelines": { - "traces": { "receivers": ["otlp"], "exporters": ["otlp/sink"] }, - "metrics": { "receivers": ["otlp"], "exporters": ["otlp/sink"] } - } } - } -}' -``` - -The `receivers` block is required. See the CaaS onboarding docs for the full `otelConfig` / `secrets` / PII-redaction options. - -#### Automatic certificates via ZTI (default) - -`cds add mta` generates the base `mta.yaml`. Add the ZTI sidecar buildpack to your service module, bind the CaaS instance (as an `existing-service`, since it's pre-created) and a `zero-trust-identity` service: - -```yaml -modules: - - name: my-app-srv - type: nodejs - path: gen/srv - parameters: - buildpacks: - - zero_trust_sidecar_buildpack - - nodejs_buildpack - requires: - - name: my-app-caas - parameters: - config: - subject: - issuer: - - name: my-app-zti - parameters: - config: - app-identifier: my-app - svid-store: - file: - name: my-app-svid -resources: - - name: my-app-caas # the instance created above - type: org.cloudfoundry.existing-service - - name: my-app-zti - type: org.cloudfoundry.managed-service - parameters: - service: zero-trust-identity - service-plan: standard -``` - -##### Certificate identity (current workaround) - -`subject`/`issuer` identify the client certificate. Until MTA tooling supports service-key placeholders, extract them before deployment from a throwaway `config-policy` key: - -```bash -cf create-service zero-trust-identity config-policy tmp-cp -cf create-service-key tmp-cp k -c '{ "app-identifier": "my-app", "type": "subject_dn" }' -cf service-key tmp-cp k # copy subject/issuer into the CaaS binding above -cf delete-service-key tmp-cp k -f && cf delete-service tmp-cp -f -``` - -#### Manual mTLS certificates (alternative) - -For environments without ZTI, provide an SAP-signed certificate yourself instead of the `zero-trust-identity` binding: - -```yaml -modules: - - name: my-app-srv - requires: - - name: my-app-caas - parameters: - config: - subject: - issuer: - properties: - CDS_REQUIRES_TELEMETRY_X509_CERT: - CDS_REQUIRES_TELEMETRY_X509_KEY: -``` - -Extract `subject`/`issuer` with `openssl x509 -in cert.pem -noout -subject -issuer -nameopt RFC2253` (use the direct issuer, not the root CA). - - ### `telemetry-to-jaeger` Exports traces to Jaeger. @@ -437,6 +337,16 @@ Please note that `@cap-js/telemetry` does not validate the configuration via env +## Running with SAP Cloud ALM (beta) + +When your application already runs the SAP Cloud ALM agent extension (`@sap/xotel-agent-ext-js`), that agent owns the OpenTelemetry SDK. `@cap-js/telemetry` detects it and, instead of setting up its own tracer provider, contributes its CDS spans to the agent's tracing pipeline (as a delegate on the agent's span processor). Both run on a single, shared OpenTelemetry SDK instance, so the CDS spans appear alongside the data the agent already collects — no second SDK, no duplicate setup. + +This requires `@sap/xotel-agent-ext-js` 2.0.4 or later. For enabling and onboarding the SAP Cloud ALM agent itself, refer to the [SAP Cloud ALM documentation](https://support.sap.com/en/alm/sap-cloud-alm/operations/expert-portal/data-collection-infrastructure.html). + +> Note: this coexistence is currently in beta. Running the SAP Cloud ALM agent together with Dynatrace OneAgent is not supported. + + + ## Detailed Configuration Options From be2c6aee6ccf9536f37a70d2d384b44ad5f9ff52 Mon Sep 17 00:00:00 2001 From: sjvans <30337871+sjvans@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:31:50 +0200 Subject: [PATCH 31/31] final polishing (#517) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Polish CaaS Exporter Buffering and Tooling Cleanup ### Bug Fix 🐛 Refines CaaS exporter buffering so telemetry batches are buffered only while certificates are unavailable, flushed once certificates arrive, and then exported directly for all future calls. Also includes small documentation and tooling cleanup. ### Changes * `lib/utils/caas.js`: Simplified buffering logic by removing the separate readiness state and relying on certificate availability. Buffered batches are flushed once certificates are present, the original exporter is restored, and pre-certificate batches are acknowledged while buffered. * `README.md`: Corrected the documented number of predefined telemetry kinds from six to five. * `package.json`: Updated lint and format scripts to call local binaries directly instead of using `npx`. * `.oxfmtrc.jsonc`: Reduced formatter ignore patterns so `CHANGELOG.md` and `jest.config.js` are no longer excluded. * `eslint.config.mjs`: Removed the custom Vitest `vi` global override and now exports the recommended CDS ESLint config directly. * `lib/tracing/locate.js`: Clarified the comment explaining why locate failures stay quiet during tests. * `test/tracing.test.js`: Removed an outdated Jest-related revisit comment. - [ ] 🔄 Regenerate and Update Summary
PR Bot Information **Version:** `1.31.51` - Correlation ID: `c1d5d860-b8d8-11f1-994e-9f11a17ac661` - Output Template: [Default Template](https://github.tools.sap/Code-Change-Intelligence/pr-bot/blob/main/src/services/llm/prompts/summary_default_output_template.md) - File Content Strategy: Full file content - LLM: `gpt-5.5` - Summary Prompt: [Default Prompt](https://github.tools.sap/Code-Change-Intelligence/pr-bot/blob/main/src/services/llm/prompts/summary_instructions_prompt.md) - Event Trigger: `pull_request.edited`
--- .oxfmtrc.jsonc | 2 +- README.md | 2 +- eslint.config.mjs | 13 +------------ lib/tracing/locate.js | 2 +- lib/utils/caas.js | 42 ++++++++++++++++++------------------------ package.json | 6 +++--- test/tracing.test.js | 1 - 7 files changed, 25 insertions(+), 43 deletions(-) diff --git a/.oxfmtrc.jsonc b/.oxfmtrc.jsonc index 413968f7..b24ae03d 100644 --- a/.oxfmtrc.jsonc +++ b/.oxfmtrc.jsonc @@ -17,5 +17,5 @@ "useTabs": false, "vueIndentScriptAndStyle": false, "sortPackageJson": false, - "ignorePatterns": ["*.md", "node_modules/**", "package-lock.json", "CHANGELOG.md", "jest.config.js"] + "ignorePatterns": ["*.md", "node_modules/**", "package-lock.json"] } diff --git a/README.md b/README.md index 8c5ab59b..12dff245 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ Please note that in order for logs to be exported via OpenTelemetry, `cds.log()` ## Predefined Kinds -There are six predefined kinds as follows: +There are five predefined kinds as follows: ### `telemetry-to-console` diff --git a/eslint.config.mjs b/eslint.config.mjs index 79f04fff..0d250b28 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,13 +1,2 @@ import cds from '@sap/cds/eslint.config.mjs' -export default [ - ...cds.recommended, - { - // The cds eslint config declares jest/mocha test globals but not vitest's `vi`. - files: ['**/+(test|tests)/**/*.+(js|cjs|mjs)', '**/*.test.+(js|cjs|mjs)', '**/*-test.+(js|cjs|mjs)'], - languageOptions: { - globals: { - vi: 'readonly' - } - } - } -] +export default [...cds.recommended] diff --git a/lib/tracing/locate.js b/lib/tracing/locate.js index 63ab4e7c..e511f006 100644 --- a/lib/tracing/locate.js +++ b/lib/tracing/locate.js @@ -33,7 +33,7 @@ const _locate = async fn => { } return location } catch (err) { - // REVISIT: the above does not work in tests (jest?) + // Stay quiet during tests: a locate miss must not pollute log-matching assertions. if (process.env.NODE_ENV !== 'test' && LOG._warn) { LOG.warn(`Unable to locate function "${fn.name}" due to error:`, err) } diff --git a/lib/utils/caas.js b/lib/utils/caas.js index b35fac7b..82cad13c 100644 --- a/lib/utils/caas.js +++ b/lib/utils/caas.js @@ -8,36 +8,30 @@ const MAX_BUFFER_SIZE = Number(process.env.TELEMETRY_MAX_BUFFER_SIZE ?? 10) function wrapExporterWithBuffer(exporter) { const buffer = [] - let ready = false const originalExport = exporter.export.bind(exporter) exporter.export = function (items, callback) { - if (!ready) { - if (certsAvailable()) { - ready = true - if (buffer.length > 0) { - LOG._debug && LOG.debug(`Flushing ${buffer.length} buffered items`) - for (const buffered of buffer) { - originalExport(buffered, () => {}) - } - buffer.length = 0 + if (certsAvailable()) { + if (buffer.length > 0) { + LOG._debug && LOG.debug(`Flushing ${buffer.length} buffered items`) + for (const buffered of buffer) { + originalExport(buffered, () => {}) } - // Restore original export for all future calls - exporter.export = originalExport - return originalExport(items, callback) + buffer.length = 0 } - // Not ready yet — buffer this export batch. MAX_BUFFER_SIZE caps the - // number of pending batches (not records). The batch is ack'd as success - // to the SDK now; if certs never arrive, buffered batches are dropped. - if (buffer.length >= MAX_BUFFER_SIZE) { - buffer.shift() - LOG._warn && LOG.warn('Buffer full, dropping oldest batch') - } - buffer.push(items) - callback({ code: 0 }) - return + // Certs have arrived: drop the buffering wrapper so all future exports go straight through. + exporter.export = originalExport + return originalExport(items, callback) + } + // Not ready yet — buffer this export batch. MAX_BUFFER_SIZE caps the + // number of pending batches (not records). The batch is ack'd as success + // to the SDK now; if certs never arrive, buffered batches are dropped. + if (buffer.length >= MAX_BUFFER_SIZE) { + buffer.shift() + LOG._warn && LOG.warn('Buffer full, dropping oldest batch') } - return originalExport(items, callback) + buffer.push(items) + callback({ code: 0 }) } return exporter diff --git a/package.json b/package.json index 646d4566..29cc0b5b 100644 --- a/package.json +++ b/package.json @@ -14,10 +14,10 @@ "lib" ], "scripts": { - "lint": "npx eslint . --max-warnings=0", + "lint": "eslint . --max-warnings=0", "test": "vitest run --silent", - "format": "npx oxfmt", - "format:check": "npx oxfmt --check" + "format": "oxfmt", + "format:check": "oxfmt --check" }, "dependencies": { "@opentelemetry/api": "^1.9", diff --git a/test/tracing.test.js b/test/tracing.test.js index 47407588..3f1122a0 100644 --- a/test/tracing.test.js +++ b/test/tracing.test.js @@ -1,4 +1,3 @@ -// REVISIT: jest breaks otel's patching of incoming request handling -> we can't ignore via ignoreIncomingRequestHook // The sampler's ignoreIncomingPaths (/odata/v4/admin/Authors) is configured via the // `sampler-ignore-authors` profile in test/bookshop/.cdsrc.json, composed with `tracing-in-memory`. const cds = require('@sap/cds')