Skip to content
Draft
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
1 change: 1 addition & 0 deletions eslint-local-rules/disallowSideEffects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
26 changes: 2 additions & 24 deletions packages/browser-core/src/boot/init.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 2 additions & 9 deletions packages/browser-core/src/boot/init.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -44,12 +44,5 @@ export function makePublicApi<T extends PublicApi>(stub: Omit<T, keyof PublicApi
}

export function defineGlobal<Global, Name extends keyof Global>(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)
}
12 changes: 12 additions & 0 deletions packages/js-core/api/runtime.api.md
Original file line number Diff line number Diff line change
@@ -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, Name extends keyof Global>(global: Global, name: Name, api: Global[Name], display?: Display): void;

// (No @packageDocumentation comment for this package)

```
6 changes: 6 additions & 0 deletions packages/js-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -39,6 +44,7 @@
"monitor",
"util",
"transport",
"runtime",
"!src/**/*.spec.ts",
"!src/**/*.specHelper.ts"
],
Expand Down
6 changes: 6 additions & 0 deletions packages/js-core/runtime/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"private": true,
"main": "../cjs/entries/runtime.js",
"module": "../esm/entries/runtime.mjs",
"types": "../cjs/entries/runtime.d.ts"
}
78 changes: 78 additions & 0 deletions packages/js-core/src/entries/runtime.spec.ts
Original file line number Diff line number Diff line change
@@ -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()
})
})
49 changes: 49 additions & 0 deletions packages/js-core/src/entries/runtime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Display } from '../util/display'
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.
* @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, Name extends keyof Global>(
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.')
}
global[name] = api
if (existingGlobalVariable?.q) {
existingGlobalVariable.q.forEach((fn) => callOnReadyCallback(display, fn))
}
}

function callOnReadyCallback(display: Display, fn: () => void) {
try {
fn()
} catch (err) {
display.error('onReady callback threw an error:', err)
}
}
8 changes: 7 additions & 1 deletion packages/js-core/typedoc.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
3 changes: 2 additions & 1 deletion tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
}
}
Loading