From 7a554407b186250a7b234f1c8f0d11bb27fc2757 Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Fri, 3 Jul 2026 19:46:23 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20add=20@datadog/js-core/runtime?= =?UTF-8?q?=20sub-path=20with=20defineGlobal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a new runtime sub-path per the js-core RFC, containing defineGlobal, ported from browser-core's boot/init.ts. Used by openfeature-js-client to expose the DD_FLAGGING global. getGlobalObject was intentionally left out of this sub-path: js-core's util entry already exposes an equivalent globalObject constant that covers the same need, so no new API was added for it. defineGlobal creates its own generic 'Datadog SDK:' display (via @datadog/js-core/util's createDisplay) rather than reusing browser-core's 'Datadog Browser SDK:'-prefixed singleton, since this function is meant for any Datadog SDK, not just the browser one. Wires up the new sub-path: package.json exports + physical runtime/package.json fallback, tsconfig.base.json path mapping, and typedoc.json entry point, per packages/js-core/AGENTS.md. --- packages/js-core/api/runtime.api.md | 12 +++ packages/js-core/package.json | 6 ++ packages/js-core/runtime/package.json | 6 ++ packages/js-core/src/entries/runtime.spec.ts | 78 ++++++++++++++++++++ packages/js-core/src/entries/runtime.ts | 41 ++++++++++ packages/js-core/typedoc.json | 8 +- tsconfig.base.json | 3 +- 7 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 packages/js-core/api/runtime.api.md create mode 100644 packages/js-core/runtime/package.json create mode 100644 packages/js-core/src/entries/runtime.spec.ts create mode 100644 packages/js-core/src/entries/runtime.ts diff --git a/packages/js-core/api/runtime.api.md b/packages/js-core/api/runtime.api.md new file mode 100644 index 0000000000..2ca0b1de0b --- /dev/null +++ b/packages/js-core/api/runtime.api.md @@ -0,0 +1,12 @@ +## API Report File for "@datadog/js-core" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public +export function defineGlobal(global: Global, name: Name, api: Global[Name]): void; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages/js-core/package.json b/packages/js-core/package.json index 53bd600e86..516f6af367 100644 --- a/packages/js-core/package.json +++ b/packages/js-core/package.json @@ -28,6 +28,11 @@ "import": "./esm/entries/transport.mjs", "require": "./cjs/entries/transport.js", "types": "./cjs/entries/transport.d.ts" + }, + "./runtime": { + "import": "./esm/entries/runtime.mjs", + "require": "./cjs/entries/runtime.js", + "types": "./cjs/entries/runtime.d.ts" } }, "files": [ @@ -39,6 +44,7 @@ "monitor", "util", "transport", + "runtime", "!src/**/*.spec.ts", "!src/**/*.specHelper.ts" ], diff --git a/packages/js-core/runtime/package.json b/packages/js-core/runtime/package.json new file mode 100644 index 0000000000..d0dec10635 --- /dev/null +++ b/packages/js-core/runtime/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "main": "../cjs/entries/runtime.js", + "module": "../esm/entries/runtime.mjs", + "types": "../cjs/entries/runtime.d.ts" +} diff --git a/packages/js-core/src/entries/runtime.spec.ts b/packages/js-core/src/entries/runtime.spec.ts new file mode 100644 index 0000000000..f6d7f300a7 --- /dev/null +++ b/packages/js-core/src/entries/runtime.spec.ts @@ -0,0 +1,78 @@ +import { originalConsoleMethods } from '../util/display' +import { defineGlobal } from './runtime' + +describe('defineGlobal', () => { + it('adds new property to the global object', () => { + const myGlobal = {} as any + const value = 'my value' + defineGlobal(myGlobal, 'foo', value) + expect(myGlobal.foo).toBe(value) + }) + + it('overrides property if exists on the global object', () => { + const myGlobal = { foo: 'old value' } + const value = 'my value' + defineGlobal(myGlobal, 'foo', value) + expect(myGlobal.foo).toBe(value) + }) + + it('runs the queued callbacks on the old value', () => { + const fn1 = jasmine.createSpy() + const fn2 = jasmine.createSpy() + const myGlobal: any = { + foo: { + q: [fn1, fn2], + }, + } + const value = 'my value' + defineGlobal(myGlobal, 'foo', value) + expect(myGlobal.foo).toBe(value) + expect(fn1).toHaveBeenCalled() + expect(fn2).toHaveBeenCalled() + }) + + it('catches the errors thrown by the queued callbacks', () => { + const myError = 'Ooops!' + const onReady = () => { + // eslint-disable-next-line @typescript-eslint/only-throw-error + throw myError + } + const myGlobal: any = { + foo: { + q: [onReady], + }, + } + const displaySpy = spyOn(originalConsoleMethods, 'error') + + defineGlobal(myGlobal, 'foo', {}) + expect(displaySpy).toHaveBeenCalledWith('Datadog SDK:', 'onReady callback threw an error:', myError) + }) + + it('warns when a previous SDK instance is already installed', () => { + const myGlobal: any = { + foo: { + version: '1.2.3', + }, + } + const displaySpy = spyOn(originalConsoleMethods, 'warn') + + defineGlobal(myGlobal, 'foo', {}) + expect(displaySpy).toHaveBeenCalledWith( + 'Datadog SDK:', + 'SDK is loaded more than once. This is unsupported and might have unexpected behavior.' + ) + }) + + it('does not warn when the existing global has an onReady queue', () => { + const myGlobal: any = { + foo: { + q: [], + version: '1.2.3', + }, + } + const displaySpy = spyOn(originalConsoleMethods, 'warn') + + defineGlobal(myGlobal, 'foo', {}) + expect(displaySpy).not.toHaveBeenCalled() + }) +}) diff --git a/packages/js-core/src/entries/runtime.ts b/packages/js-core/src/entries/runtime.ts new file mode 100644 index 0000000000..de322d9a29 --- /dev/null +++ b/packages/js-core/src/entries/runtime.ts @@ -0,0 +1,41 @@ +import { createDisplay } from '../util/display' + +interface GlobalWithOnReadyQueue { + /** Queue of callbacks registered via a stub loader snippet before the real SDK was loaded. */ + q?: Array<() => void> + /** Marker set by the real SDK once loaded, used to detect duplicate inclusion. */ + version?: string +} + +/** + * Exposes `api` as `global[name]`, the standard way Datadog SDKs publish themselves as a global + * (e.g. `window.DD_RUM`). + * + * Warns if a previous SDK instance is already installed on `name` (guarding against duplicate + * script inclusion), and flushes any callbacks queued by a stub loader snippet — the common + * `window.DD_RUM = window.DD_RUM || { q: [], onReady: (cb) => window.DD_RUM.q.push(cb) }` pattern + * used to queue `onReady` calls made before the real SDK script has loaded. + * + * @param global - The object to attach the API to, typically the global/window object. + * @param name - The property name to define, e.g. `'DD_RUM'`. + * @param api - The public API object to expose. + */ +export function defineGlobal(global: Global, name: Name, api: Global[Name]) { + const display = createDisplay('Datadog SDK:') + const existingGlobalVariable = global[name] as unknown as GlobalWithOnReadyQueue | undefined + if (existingGlobalVariable && !existingGlobalVariable.q && existingGlobalVariable.version) { + display.warn('SDK is loaded more than once. This is unsupported and might have unexpected behavior.') + } + global[name] = api + if (existingGlobalVariable?.q) { + existingGlobalVariable.q.forEach((fn) => callOnReadyCallback(display, fn)) + } +} + +function callOnReadyCallback(display: ReturnType, fn: () => void) { + try { + fn() + } catch (err) { + display.error('onReady callback threw an error:', err) + } +} diff --git a/packages/js-core/typedoc.json b/packages/js-core/typedoc.json index 013fd5a7e9..7f68a6e9b3 100644 --- a/packages/js-core/typedoc.json +++ b/packages/js-core/typedoc.json @@ -1,4 +1,10 @@ { "$schema": "https://typedoc.org/schema.json", - "entryPoints": ["src/entries/time.ts", "src/entries/monitor.ts", "src/entries/util.ts", "src/entries/assembly.ts"] + "entryPoints": [ + "src/entries/time.ts", + "src/entries/monitor.ts", + "src/entries/util.ts", + "src/entries/assembly.ts", + "src/entries/runtime.ts" + ] } diff --git a/tsconfig.base.json b/tsconfig.base.json index b201f80876..b54e4348f6 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -50,7 +50,8 @@ "@datadog/js-core/time": ["./packages/js-core/src/entries/time"], "@datadog/js-core/monitor": ["./packages/js-core/src/entries/monitor"], "@datadog/js-core/util": ["./packages/js-core/src/entries/util"], - "@datadog/js-core/transport": ["./packages/js-core/src/entries/transport"] + "@datadog/js-core/transport": ["./packages/js-core/src/entries/transport"], + "@datadog/js-core/runtime": ["./packages/js-core/src/entries/runtime"] } } } From bb3c92dba2e432b05df9b050b5171b415654213a Mon Sep 17 00:00:00 2001 From: Thomas Lebeau Date: Wed, 8 Jul 2026 08:45:56 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20migrate=20browser-core?= =?UTF-8?q?=20to=20@datadog/js-core/runtime's=20defineGlobal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds an optional 'display' parameter to js-core's defineGlobal (defaulting to a generic 'Datadog SDK:'-prefixed one), so browser-core can pass its own 'Datadog Browser SDK:'-prefixed display and preserve the exact console warning text end users already see. This is an additive signature change (new optional trailing param), not a breaking one. browser-core/boot/init.ts's defineGlobal is now a one-line delegation to @datadog/js-core/runtime's defineGlobal, passing browser-core's own display. catchUserErrors is no longer used here (still used elsewhere, e.g. configuration.ts, so tools/catchUserErrors.ts stays). Trims init.spec.ts's defineGlobal tests down to the two behaviors that are actually specific to browser-core's wrapper (delegation works, and the browser-core display is the one used for warnings) since the underlying logic now has full coverage in js-core's own runtime.spec.ts; keeping the exhaustive matrix here would have duplicated test names across files. Adds '@datadog/js-core/runtime' to the disallow-side-effects lint rule's allowlist, alongside the other side-effect-free js-core sub-paths. Downstream packages (browser-logs, browser-rum, browser-rum-slim, browser-debugger) are unaffected: they still import defineGlobal from '@datadog/browser-core', unchanged. --- eslint-local-rules/disallowSideEffects.ts | 1 + packages/browser-core/src/boot/init.spec.ts | 26 ++------------------- packages/browser-core/src/boot/init.ts | 11 ++------- packages/js-core/api/runtime.api.md | 2 +- packages/js-core/src/entries/runtime.ts | 14 ++++++++--- 5 files changed, 17 insertions(+), 37 deletions(-) diff --git a/eslint-local-rules/disallowSideEffects.ts b/eslint-local-rules/disallowSideEffects.ts index 720442324d..2495350560 100644 --- a/eslint-local-rules/disallowSideEffects.ts +++ b/eslint-local-rules/disallowSideEffects.ts @@ -50,6 +50,7 @@ const packagesWithoutSideEffect = new Set([ '@datadog/js-core/util', '@datadog/js-core/monitor', '@datadog/js-core/transport', + '@datadog/js-core/runtime', '@datadog/browser-core', '@datadog/browser-rum-core', '@datadog/browser-rum-react/internal', diff --git a/packages/browser-core/src/boot/init.spec.ts b/packages/browser-core/src/boot/init.spec.ts index e3661f772b..a014d9d6b5 100644 --- a/packages/browser-core/src/boot/init.spec.ts +++ b/packages/browser-core/src/boot/init.spec.ts @@ -2,36 +2,14 @@ import { display } from '../tools/display' import { defineGlobal } from './init' describe('defineGlobal', () => { - it('adds new property to the global object', () => { + it('delegates to @datadog/js-core/runtime and adds new property to the global object', () => { const myGlobal = {} as any const value = 'my value' defineGlobal(myGlobal, 'foo', value) expect(myGlobal.foo).toBe(value) }) - it('overrides property if exists on the global object', () => { - const myGlobal = { foo: 'old value' } - const value = 'my value' - defineGlobal(myGlobal, 'foo', value) - expect(myGlobal.foo).toBe(value) - }) - - it('run the queued callbacks on the old value', () => { - const fn1 = jasmine.createSpy() - const fn2 = jasmine.createSpy() - const myGlobal: any = { - foo: { - q: [fn1, fn2], - }, - } - const value = 'my value' - defineGlobal(myGlobal, 'foo', value) - expect(myGlobal.foo).toBe(value) - expect(fn1).toHaveBeenCalled() - expect(fn2).toHaveBeenCalled() - }) - - it('catches the errors thrown by the queued callbacks', () => { + it('delegates to @datadog/js-core/runtime using browser-core own display', () => { const myError = 'Ooops!' const onReady = () => { // eslint-disable-next-line @typescript-eslint/only-throw-error diff --git a/packages/browser-core/src/boot/init.ts b/packages/browser-core/src/boot/init.ts index efc928e0fc..23cec5910c 100644 --- a/packages/browser-core/src/boot/init.ts +++ b/packages/browser-core/src/boot/init.ts @@ -1,5 +1,5 @@ import { setDebugMode } from '@datadog/js-core/util' -import { catchUserErrors } from '../tools/catchUserErrors' +import { defineGlobal as coreDefineGlobal } from '@datadog/js-core/runtime' import { display } from '../tools/display' // replaced at build time @@ -44,12 +44,5 @@ export function makePublicApi(stub: Omit(global: Global, name: Name, api: Global[Name]) { - const existingGlobalVariable = global[name] as { q?: Array<() => void>; version?: string } | undefined - if (existingGlobalVariable && !existingGlobalVariable.q && existingGlobalVariable.version) { - display.warn('SDK is loaded more than once. This is unsupported and might have unexpected behavior.') - } - global[name] = api - if (existingGlobalVariable?.q) { - existingGlobalVariable.q.forEach((fn) => catchUserErrors(fn, 'onReady callback threw an error:')()) - } + coreDefineGlobal(global, name, api, display) } diff --git a/packages/js-core/api/runtime.api.md b/packages/js-core/api/runtime.api.md index 2ca0b1de0b..bf77ef7ea1 100644 --- a/packages/js-core/api/runtime.api.md +++ b/packages/js-core/api/runtime.api.md @@ -5,7 +5,7 @@ ```ts // @public -export function defineGlobal(global: Global, name: Name, api: Global[Name]): void; +export function defineGlobal(global: Global, name: Name, api: Global[Name], display?: Display): void; // (No @packageDocumentation comment for this package) diff --git a/packages/js-core/src/entries/runtime.ts b/packages/js-core/src/entries/runtime.ts index de322d9a29..62b479d424 100644 --- a/packages/js-core/src/entries/runtime.ts +++ b/packages/js-core/src/entries/runtime.ts @@ -1,3 +1,4 @@ +import type { Display } from '../util/display' import { createDisplay } from '../util/display' interface GlobalWithOnReadyQueue { @@ -19,9 +20,16 @@ interface GlobalWithOnReadyQueue { * @param global - The object to attach the API to, typically the global/window object. * @param name - The property name to define, e.g. `'DD_RUM'`. * @param api - The public API object to expose. + * @param display - {@link Display} used to print the warnings above. Defaults to a generic + * `'Datadog SDK:'`-prefixed one; pass your own (see `createDisplay` in `@datadog/js-core/util`) to + * customize the prefix shown to end users. */ -export function defineGlobal(global: Global, name: Name, api: Global[Name]) { - const display = createDisplay('Datadog SDK:') +export function defineGlobal( + global: Global, + name: Name, + api: Global[Name], + display: Display = createDisplay('Datadog SDK:') +) { const existingGlobalVariable = global[name] as unknown as GlobalWithOnReadyQueue | undefined if (existingGlobalVariable && !existingGlobalVariable.q && existingGlobalVariable.version) { display.warn('SDK is loaded more than once. This is unsupported and might have unexpected behavior.') @@ -32,7 +40,7 @@ export function defineGlobal(global: Global, } } -function callOnReadyCallback(display: ReturnType, fn: () => void) { +function callOnReadyCallback(display: Display, fn: () => void) { try { fn() } catch (err) {