From 8516a0e08c93a36f4db0a5fd7e6ca27721b9377c Mon Sep 17 00:00:00 2001 From: Marc Pichler Date: Tue, 24 Mar 2026 12:36:29 +0100 Subject: [PATCH] fix(api): re-introduce fallback chain for global utils --- .github/workflows/backcompat.yml | 63 ++++++++++++++++++++++ api/CHANGELOG.md | 1 + api/package.json | 1 + api/src/internal/global-utils.ts | 16 +++++- api/test/backcompat/node8-compat.js | 81 +++++++++++++++++++++++++++++ 5 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/backcompat.yml create mode 100644 api/test/backcompat/node8-compat.js diff --git a/.github/workflows/backcompat.yml b/.github/workflows/backcompat.yml new file mode 100644 index 00000000000..8276ef6f521 --- /dev/null +++ b/.github/workflows/backcompat.yml @@ -0,0 +1,63 @@ +name: Old Node.js Compatibility +on: + push: + branches: + - main + paths: + - 'api/**' + - '.github/workflows/backcompat.yml' + pull_request: + paths: + - 'api/**' + - '.github/workflows/backcompat.yml' + merge_group: + paths: + - 'api/**' + - '.github/workflows/backcompat.yml' + +permissions: + contents: read + +jobs: + backcompat: + name: 'backcompat: ${{ matrix.package }} on Node.js ${{ matrix.node-version }}' + strategy: + fail-fast: false + matrix: + include: + - package: api + node-version: '8' + script: test/backcompat/node8-compat.js + runs-on: ubuntu-latest + env: + PACKAGE: ${{ matrix.package }} + NODE_VERSION: ${{ matrix.node-version }} + SCRIPT: ${{ matrix.script }} + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + + # Use a modern Node.js to install dependencies and compile TypeScript. + - name: Setup Node.js (compile) + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: package-lock.json + + - name: Bootstrap + run: npm ci --ignore-scripts + + - name: Compile + run: npm run compile + + # Switch to the target old Node.js and run the standalone plain-JS smoke + # test. Package is already compiled and dependencies are already installed. + - name: Setup Node.js ${{ env.NODE_VERSION }} (test) + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Run compatibility smoke test + run: node "$SCRIPT" + working-directory: ${{ env.PACKAGE }} diff --git a/api/CHANGELOG.md b/api/CHANGELOG.md index d940a5224bc..26896042cb1 100644 --- a/api/CHANGELOG.md +++ b/api/CHANGELOG.md @@ -15,6 +15,7 @@ All notable changes to this project will be documented in this file. * fix(api): update diag `consoleLogger` to use original console methods to prevent infinite loop when a console instrumentation is present [#6395](https://github.com/open-telemetry/opentelemetry-js/pull/6395) * fix(api): use `Attributes` instead of deprecated `SpanAttributes` in `SpanOptions` [#6478](https://github.com/open-telemetry/opentelemetry-js/pull/6478) @overbalance * fix(diag): change types in `DiagComponentLogger` from `any` to `unknown`[#5478](https://github.com/open-telemetry/opentelemetry-js/pull/5478) @loganrosen +* fix(api): re-introduce fallback chain for global utils [#6523](https://github.com/open-telemetry/opentelemetry-js/pull/6523/) @pichlermarc ### :books: (Refine Doc) diff --git a/api/package.json b/api/package.json index 0b979a7104d..0a6a2fc75c3 100644 --- a/api/package.json +++ b/api/package.json @@ -28,6 +28,7 @@ "lint": "eslint . --ext .ts && npm run cycle-check", "test:browser": "karma start --single-run", "test": "nyc mocha 'test/**/*.test.ts'", + "test:node8-compat": "node test/backcompat/node8-compat.js", "test:webworker": "karma start karma.worker.js --single-run", "cycle-check": "dpdm --exit-code circular:1 src/index.ts", "version": "node ../scripts/version-update.js", diff --git a/api/src/internal/global-utils.ts b/api/src/internal/global-utils.ts index 9981abd5bb2..1c4b45ad8ef 100644 --- a/api/src/internal/global-utils.ts +++ b/api/src/internal/global-utils.ts @@ -16,7 +16,21 @@ const GLOBAL_OPENTELEMETRY_API_KEY = Symbol.for( `opentelemetry.js.api.${major}` ); -const _global = globalThis as OTelGlobal; +declare const self: unknown; +declare const window: unknown; +declare const global: unknown; + +const _global = ( + typeof globalThis === 'object' + ? globalThis + : typeof self === 'object' + ? self + : typeof window === 'object' + ? window + : typeof global === 'object' + ? global + : {} +) as OTelGlobal; export function registerGlobal( type: Type, diff --git a/api/test/backcompat/node8-compat.js b/api/test/backcompat/node8-compat.js new file mode 100644 index 00000000000..a84dbc32a45 --- /dev/null +++ b/api/test/backcompat/node8-compat.js @@ -0,0 +1,81 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Simple Node.js v8 compatibility smoke test. Intentionally avoids dependencies + * so that we can keep updating tooling used for unit and integration tests. + * + * Intended to be run directly with node node8-compat.js + */ + +const assert = require('assert'); + +const GLOBAL_API_SYMBOL = Symbol.for('opentelemetry.js.api.1'); + +function clearGlobal() { + delete global[GLOBAL_API_SYMBOL]; +} + +function freshApi() { + // Evict every cached module so each call returns a brand-new instance. + Object.keys(require.cache).forEach(function (key) { + delete require.cache[key]; + }); + return require('../../build/src/index.js'); +} + +// Test 1: test global registration (the global used must exist) +clearGlobal(); +const api = freshApi(); + +// this will throw or return false if registration fails. +const result = api.diag.setLogger( + new api.DiagConsoleLogger(), + api.DiagLogLevel.ALL +); +assert.strictEqual( + result, + true, + 'Test 1 failed: diag.setLogger should return true' +); + +// Test 2: test that multiple loads share the same global state (e.g. logger) +clearGlobal(); + +const api1 = freshApi(); +const api2 = freshApi(); // fresh load – different module object, same _global + +const infoMessages = []; +const sharedLogger = { + error: function () {}, + warn: function () {}, + info: function (msg) { + infoMessages.push(msg); + }, + debug: function () {}, + verbose: function () {}, +}; + +const ok = api1.diag.setLogger(sharedLogger, api1.DiagLogLevel.ALL); +assert.strictEqual( + ok, + true, + 'Test 2 setup failed: api1.diag.setLogger should return true' +); + +api2.diag.info('shared-state-check'); +assert.strictEqual( + infoMessages.length, + 1, + 'Test 2 failed: api2 should forward logs to the logger registered via api1' +); +assert.strictEqual( + infoMessages[0], + 'shared-state-check', + 'Test 2 failed: unexpected log message received' +); + +// clean-up +clearGlobal();