Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -26,6 +27,7 @@ updates:

- package-ecosystem: 'github-actions'
directory: '/'
target-branch: 'develop'
schedule:
interval: 'weekly'
cooldown:
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
21 changes: 21 additions & 0 deletions .oxfmtrc.jsonc
Original file line number Diff line number Diff line change
@@ -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"]
}
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ 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

- Queue worker transactions are traced as coherent `<service> - tx` spans under the `cds.spawn - run task` root, instead of orphaned per-call spans

### Changed

### Fixed
Comment on lines +9 to +15

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

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

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

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


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

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


- 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
Expand Down
13 changes: 12 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -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'
}
}
}
]
14 changes: 0 additions & 14 deletions jest.config.js

This file was deleted.

4 changes: 3 additions & 1 deletion lib/exporter/ConsoleMetricExporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`
)
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion lib/logging/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions lib/metrics/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ??= {}

Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions lib/tracing/cds.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
41 changes: 24 additions & 17 deletions lib/tracing/cloud_sdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -25,26 +28,30 @@ 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
})
}
8 changes: 6 additions & 2 deletions lib/tracing/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading