From 2a3cdcea8498420553d9e2693d08db76f8b5eb2b Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Thu, 12 Mar 2026 17:16:54 +0100 Subject: [PATCH 01/11] =?UTF-8?q?=E2=9C=A8=20add=20serializeDynamicValueTo?= =?UTF-8?q?Js=20and=20CodeExpression=20for=20Node=20code=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../remote-config/src/nodeResolution.spec.ts | 68 +++++++++++++ packages/remote-config/src/nodeResolution.ts | 96 +++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 packages/remote-config/src/nodeResolution.spec.ts create mode 100644 packages/remote-config/src/nodeResolution.ts diff --git a/packages/remote-config/src/nodeResolution.spec.ts b/packages/remote-config/src/nodeResolution.spec.ts new file mode 100644 index 0000000000..adecff6463 --- /dev/null +++ b/packages/remote-config/src/nodeResolution.spec.ts @@ -0,0 +1,68 @@ +import { serializeDynamicValueToJs } from './nodeResolution' + +describe('serializeDynamicValueToJs', () => { + it('should serialize cookie strategy', () => { + const result = serializeDynamicValueToJs({ + rcSerializedType: 'dynamic', + strategy: 'cookie', + name: 'user_id', + }) + expect(result).toBe("__dd_getCookie('user_id')") + }) + + it('should serialize js strategy', () => { + const result = serializeDynamicValueToJs({ + rcSerializedType: 'dynamic', + strategy: 'js', + path: 'window.user', + }) + expect(result).toBe("__dd_getJs('window.user')") + }) + + it('should serialize dom strategy (text content)', () => { + const result = serializeDynamicValueToJs({ + rcSerializedType: 'dynamic', + strategy: 'dom', + selector: '[data-env]', + }) + expect(result).toBe("__dd_getDomText('[data-env]')") + }) + + it('should serialize dom strategy (attribute)', () => { + const result = serializeDynamicValueToJs({ + rcSerializedType: 'dynamic', + strategy: 'dom', + selector: '[data-env]', + attribute: 'data-env', + }) + expect(result).toBe("__dd_getDomAttr('[data-env]','data-env')") + }) + + it('should serialize localStorage strategy', () => { + const result = serializeDynamicValueToJs({ + rcSerializedType: 'dynamic', + strategy: 'localStorage', + key: 'app_version', + }) + expect(result).toBe("__dd_getLocalStorage('app_version')") + }) + + it('should wrap expression with extractor when present', () => { + const result = serializeDynamicValueToJs({ + rcSerializedType: 'dynamic', + strategy: 'cookie', + name: 'version_string', + extractor: { rcSerializedType: 'regex', value: 'v(\\d+)' }, + }) + expect(result).toBe("__dd_extract(__dd_getCookie('version_string'),'v(\\\\d+)')") + }) + + it('should return "undefined" literal for unknown strategy', () => { + const result = serializeDynamicValueToJs({ + rcSerializedType: 'dynamic', + strategy: 'unknown' as any, + name: 'foo', + } as any) + expect(result).toBe('undefined') + }) +}) diff --git a/packages/remote-config/src/nodeResolution.ts b/packages/remote-config/src/nodeResolution.ts new file mode 100644 index 0000000000..d54c182b9e --- /dev/null +++ b/packages/remote-config/src/nodeResolution.ts @@ -0,0 +1,96 @@ +import { display, isIndexableObject } from '@datadog/browser-core' +import type { DynamicOption, ContextItem } from './remoteConfiguration.types' + +// --------------------------------------------------------------------------- +// CodeExpression — internal marker for raw JS code strings +// --------------------------------------------------------------------------- + +export interface CodeExpression { + __isCodeExpression: true + code: string +} + +export function codeExpression(code: string): CodeExpression { + return { __isCodeExpression: true, code } +} + +export function isCodeExpression(value: unknown): value is CodeExpression { + return isIndexableObject(value) && (value as unknown as CodeExpression).__isCodeExpression === true +} + +// --------------------------------------------------------------------------- +// serializeDynamicValueToJs +// --------------------------------------------------------------------------- + +/** + * Serialize a string as a single-quoted JS string literal. + * Escapes backslashes and single quotes so the result is valid JS. + */ +function jsStringLiteral(value: string): string { + return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'` +} + +export function serializeDynamicValueToJs(option: DynamicOption): string { + let expr: string + switch (option.strategy) { + case 'cookie': + expr = `__dd_getCookie(${jsStringLiteral(option.name)})` + break + case 'js': + expr = `__dd_getJs(${jsStringLiteral(option.path)})` + break + case 'dom': + expr = + option.attribute !== undefined + ? `__dd_getDomAttr(${jsStringLiteral(option.selector)},${jsStringLiteral(option.attribute)})` + : `__dd_getDomText(${jsStringLiteral(option.selector)})` + break + case 'localStorage': + expr = `__dd_getLocalStorage(${jsStringLiteral(option.key)})` + break + default: + display.warn(`Unsupported remote configuration strategy: "${(option as DynamicOption).strategy}"`) + return 'undefined' + } + + if (option.extractor !== undefined) { + expr = `__dd_extract(${expr},${jsStringLiteral(option.extractor.value)})` + } + + return expr +} + +// --------------------------------------------------------------------------- +// nodeContextItemHandler and serializeConfigToJs — implemented in Task 2 +// --------------------------------------------------------------------------- + +/** @internal */ +export function nodeContextItemHandler(items: ContextItem[], resolve: (value: unknown) => unknown): unknown { + const entries = items + .map(({ key, value }) => { + if (value === undefined) return null + const resolved = resolve(value) + if (resolved === undefined) return null + const code = isCodeExpression(resolved) ? resolved.code : JSON.stringify(resolved) + return `${JSON.stringify(key)}: ${code}` + }) + .filter((entry): entry is string => entry !== null) + + return codeExpression(`{ ${entries.join(', ')} }`) +} + +export function serializeConfigToJs(config: unknown): string { + if (isCodeExpression(config)) { + return config.code + } + if (Array.isArray(config)) { + return `[${config.map(serializeConfigToJs).join(', ')}]` + } + if (isIndexableObject(config)) { + const entries = Object.entries(config as Record).map( + ([k, v]) => `${JSON.stringify(k)}: ${serializeConfigToJs(v)}` + ) + return `{ ${entries.join(', ')} }` + } + return JSON.stringify(config) +} From de274fe920f3c03a82be242f142d6b023ea89795 Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Thu, 12 Mar 2026 17:21:10 +0100 Subject: [PATCH 02/11] =?UTF-8?q?=F0=9F=90=9B=20fix=20jsStringLiteral=20es?= =?UTF-8?q?caping=20and=20tighten=20nodeContextItemHandler=20return=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../remote-config/src/nodeResolution.spec.ts | 9 +++++++++ packages/remote-config/src/nodeResolution.ts | 17 ++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/packages/remote-config/src/nodeResolution.spec.ts b/packages/remote-config/src/nodeResolution.spec.ts index adecff6463..77678fa8cf 100644 --- a/packages/remote-config/src/nodeResolution.spec.ts +++ b/packages/remote-config/src/nodeResolution.spec.ts @@ -57,6 +57,15 @@ describe('serializeDynamicValueToJs', () => { expect(result).toBe("__dd_extract(__dd_getCookie('version_string'),'v(\\\\d+)')") }) + it('should escape special characters in string values', () => { + const result = serializeDynamicValueToJs({ + rcSerializedType: 'dynamic', + strategy: 'cookie', + name: 'foo\nbar\u2028baz', + }) + expect(result).toBe("__dd_getCookie('foo\\nbar\\u2028baz')") + }) + it('should return "undefined" literal for unknown strategy', () => { const result = serializeDynamicValueToJs({ rcSerializedType: 'dynamic', diff --git a/packages/remote-config/src/nodeResolution.ts b/packages/remote-config/src/nodeResolution.ts index d54c182b9e..c64bb5c3da 100644 --- a/packages/remote-config/src/nodeResolution.ts +++ b/packages/remote-config/src/nodeResolution.ts @@ -24,10 +24,17 @@ export function isCodeExpression(value: unknown): value is CodeExpression { /** * Serialize a string as a single-quoted JS string literal. - * Escapes backslashes and single quotes so the result is valid JS. + * Escapes backslashes, single quotes, and control characters so the result is valid JS. */ function jsStringLiteral(value: string): string { - return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'` + return `'${value + .replace(/\\/g, '\\\\') + .replace(/'/g, "\\'") + .replace(/\r/g, '\\r') + .replace(/\n/g, '\\n') + .replace(/\t/g, '\\t') + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029')}'` } export function serializeDynamicValueToJs(option: DynamicOption): string { @@ -49,7 +56,7 @@ export function serializeDynamicValueToJs(option: DynamicOption): string { expr = `__dd_getLocalStorage(${jsStringLiteral(option.key)})` break default: - display.warn(`Unsupported remote configuration strategy: "${(option as DynamicOption).strategy}"`) + display.error(`Unsupported remote configuration strategy: "${(option as DynamicOption).strategy}"`) return 'undefined' } @@ -65,7 +72,7 @@ export function serializeDynamicValueToJs(option: DynamicOption): string { // --------------------------------------------------------------------------- /** @internal */ -export function nodeContextItemHandler(items: ContextItem[], resolve: (value: unknown) => unknown): unknown { +export function nodeContextItemHandler(items: ContextItem[], resolve: (value: unknown) => unknown): CodeExpression { const entries = items .map(({ key, value }) => { if (value === undefined) return null @@ -90,7 +97,7 @@ export function serializeConfigToJs(config: unknown): string { const entries = Object.entries(config as Record).map( ([k, v]) => `${JSON.stringify(k)}: ${serializeConfigToJs(v)}` ) - return `{ ${entries.join(', ')} }` + return entries.length ? `{ ${entries.join(', ')} }` : '{}' } return JSON.stringify(config) } From 0f8e17817542b713c71e8f8cd668c8ad3364517b Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Thu, 12 Mar 2026 17:23:17 +0100 Subject: [PATCH 03/11] =?UTF-8?q?=E2=9C=85=20add=20tests=20for=20nodeConte?= =?UTF-8?q?xtItemHandler=20and=20serializeConfigToJs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../remote-config/src/nodeResolution.spec.ts | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/packages/remote-config/src/nodeResolution.spec.ts b/packages/remote-config/src/nodeResolution.spec.ts index 77678fa8cf..8e3a44a492 100644 --- a/packages/remote-config/src/nodeResolution.spec.ts +++ b/packages/remote-config/src/nodeResolution.spec.ts @@ -1,4 +1,10 @@ -import { serializeDynamicValueToJs } from './nodeResolution' +import { + serializeDynamicValueToJs, + nodeContextItemHandler, + serializeConfigToJs, + CodeExpression, +} from './nodeResolution' +import type { ContextItem, DynamicOption } from './remoteConfiguration.types' describe('serializeDynamicValueToJs', () => { it('should serialize cookie strategy', () => { @@ -75,3 +81,92 @@ describe('serializeDynamicValueToJs', () => { expect(result).toBe('undefined') }) }) + +describe('nodeContextItemHandler', () => { + const resolve = (v: unknown): unknown => { + if (typeof v === 'object' && v !== null && 'strategy' in v) { + return { __isCodeExpression: true as const, code: serializeDynamicValueToJs(v as DynamicOption) } + } + return v + } + + it('should convert a single ContextItem to a JS object literal CodeExpression', () => { + const items: ContextItem[] = [ + { key: 'id', value: { rcSerializedType: 'dynamic', strategy: 'js', path: 'window.user' } }, + ] + const result = nodeContextItemHandler(items, resolve) as CodeExpression + expect(result.__isCodeExpression).toBe(true) + expect(result.code).toContain('"id"') + expect(result.code).toContain("__dd_getJs('window.user')") + }) + + it('should handle multiple keys', () => { + const items: ContextItem[] = [ + { key: 'id', value: { rcSerializedType: 'dynamic', strategy: 'cookie', name: 'uid' } }, + { key: 'env', value: { rcSerializedType: 'dynamic', strategy: 'js', path: 'window.env' } }, + ] + const result = nodeContextItemHandler(items, resolve) as CodeExpression + expect(result.code).toContain("__dd_getCookie('uid')") + expect(result.code).toContain("__dd_getJs('window.env')") + }) + + it('should skip items where resolve returns undefined', () => { + const items: ContextItem[] = [ + { key: 'id', value: { rcSerializedType: 'dynamic', strategy: 'js', path: 'window.user' } }, + ] + const result = nodeContextItemHandler(items, () => undefined) as CodeExpression + expect(result.code).toBe('{ }') + }) +}) + +describe('serializeConfigToJs', () => { + it('should serialize primitive values', () => { + expect(serializeConfigToJs(24)).toBe('24') + expect(serializeConfigToJs('hello')).toBe('"hello"') + expect(serializeConfigToJs(true)).toBe('true') + expect(serializeConfigToJs(null)).toBe('null') + }) + + it('should inline a CodeExpression as raw code', () => { + const expr: CodeExpression = { __isCodeExpression: true, code: "__dd_getJs('window.user')" } + expect(serializeConfigToJs(expr)).toBe("__dd_getJs('window.user')") + }) + + it('should serialize a plain object recursively', () => { + const result = serializeConfigToJs({ sessionSampleRate: 24, env: 'prod' }) + expect(result).toContain('"sessionSampleRate": 24') + expect(result).toContain('"env": "prod"') + }) + + it('should inline CodeExpression values within an object', () => { + const config = { + sessionSampleRate: 24, + user: { __isCodeExpression: true as const, code: "{ id: __dd_getJs('window.user') }" }, + } + const result = serializeConfigToJs(config) + expect(result).toContain('"sessionSampleRate": 24') + expect(result).toContain('"user": { id: __dd_getJs(\'window.user\') }') + }) + + it('should serialize arrays', () => { + expect(serializeConfigToJs([1, 'two', true])).toBe('[1, "two", true]') + }) + + it('should return {} for an empty object', () => { + expect(serializeConfigToJs({})).toBe('{}') + }) + + it('should handle a realistic full config with mixed static and dynamic values', () => { + const config = { + applicationId: 'd717cc88-ced7-4830-a377-14433a5c7bb0', + sessionSampleRate: 24, + env: 'remote_config_demo', + user: { __isCodeExpression: true as const, code: "{ 'id': __dd_getJs('window.user') }" }, + } + const result = serializeConfigToJs(config) + expect(result).toContain('"applicationId": "d717cc88-ced7-4830-a377-14433a5c7bb0"') + expect(result).toContain('"sessionSampleRate": 24') + expect(result).toContain('"env": "remote_config_demo"') + expect(result).toContain("\"user\": { 'id': __dd_getJs('window.user') }") + }) +}) From 5395aaef559a4d71d4342746ccf704a80eb75a58 Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Thu, 12 Mar 2026 17:25:30 +0100 Subject: [PATCH 04/11] =?UTF-8?q?=E2=9C=A8=20add=20Node.js=20entry=20point?= =?UTF-8?q?=20to=20@datadog/browser-remote-config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/remote-config/package.json | 4 ++++ packages/remote-config/src/entries/node.ts | 28 ++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 packages/remote-config/src/entries/node.ts diff --git a/packages/remote-config/package.json b/packages/remote-config/package.json index d6818bbc07..d3dddb777c 100644 --- a/packages/remote-config/package.json +++ b/packages/remote-config/package.json @@ -5,6 +5,10 @@ "main": "cjs/index.js", "module": "esm/index.js", "types": "cjs/index.d.ts", + "exports": { + ".": "./src/index.ts", + "./node": "./src/entries/node.ts" + }, "scripts": { "build": "node ../../scripts/build/build-package.ts --modules" }, diff --git a/packages/remote-config/src/entries/node.ts b/packages/remote-config/src/entries/node.ts new file mode 100644 index 0000000000..9bd9e9c2d1 --- /dev/null +++ b/packages/remote-config/src/entries/node.ts @@ -0,0 +1,28 @@ +import { + resolveDynamicValues as resolveDynamicValuesFn, + fetchRemoteConfiguration, + buildEndpoint, +} from '../remoteConfiguration' +import { nodeContextItemHandler } from '../nodeResolution' + +/** + * Resolve dynamic RC values for Node.js code generation. + * DynamicOption fields are converted to inline JS expression strings (CodeExpression) + * rather than resolved against live browser APIs. + */ +export function resolveDynamicValues( + configValue: unknown, + options: { + onCookie?: (value: string | undefined) => void + onDom?: (value: string | null | undefined) => void + onJs?: (value: unknown) => void + } = {} +): unknown { + return resolveDynamicValuesFn(configValue, { + ...options, + contextItemHandler: nodeContextItemHandler, + }) +} + +export { serializeConfigToJs } from '../nodeResolution' +export { fetchRemoteConfiguration, buildEndpoint } from '../remoteConfiguration' From d6195424ca2d7c17051ea59c869122d38f334859 Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Fri, 13 Mar 2026 14:16:31 +0100 Subject: [PATCH 05/11] =?UTF-8?q?=E2=9C=A8=20add=20@datadog/browser-sdk-en?= =?UTF-8?q?dpoint=20package=20with=20Node=20code=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/endpoint/package.json | 16 ++++ packages/endpoint/src/bundleGenerator.ts | 103 +++++++++++++++++++++++ packages/endpoint/src/helpers.ts | 11 +++ packages/endpoint/src/index.ts | 2 + packages/endpoint/src/sdkDownloader.ts | 92 ++++++++++++++++++++ packages/endpoint/tsconfig.json | 20 +++++ tsconfig.default.json | 3 + tsconfig.json | 1 + yarn.lock | 10 ++- 9 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 packages/endpoint/package.json create mode 100644 packages/endpoint/src/bundleGenerator.ts create mode 100644 packages/endpoint/src/helpers.ts create mode 100644 packages/endpoint/src/index.ts create mode 100644 packages/endpoint/src/sdkDownloader.ts create mode 100644 packages/endpoint/tsconfig.json diff --git a/packages/endpoint/package.json b/packages/endpoint/package.json new file mode 100644 index 0000000000..1bc0a19a68 --- /dev/null +++ b/packages/endpoint/package.json @@ -0,0 +1,16 @@ +{ + "name": "@datadog/browser-sdk-endpoint", + "version": "6.28.0", + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "engines": { + "node": ">=18" + }, + "dependencies": { + "@datadog/browser-remote-config": "6.28.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/endpoint/src/bundleGenerator.ts b/packages/endpoint/src/bundleGenerator.ts new file mode 100644 index 0000000000..6fcb5182a5 --- /dev/null +++ b/packages/endpoint/src/bundleGenerator.ts @@ -0,0 +1,103 @@ +import type { SdkVariant } from './sdkDownloader.ts' +import { INLINE_HELPERS } from './helpers.ts' + +export type { SdkVariant } from './sdkDownloader.ts' + +export interface FetchConfigOptions { + applicationId: string + remoteConfigurationId: string + site?: string +} + +export interface CombineBundleOptions { + sdkCode: string + configJs: string + variant: SdkVariant + sdkVersion?: string +} + +export interface GenerateBundleOptions { + applicationId: string + remoteConfigurationId: string + variant: SdkVariant + site?: string + datacenter?: string +} + +const CONFIG_FETCH_TIMEOUT_MS = 30_000 +const VALID_VARIANTS: SdkVariant[] = ['rum', 'rum-slim'] + +export async function fetchConfig(options: FetchConfigOptions) { + const { fetchRemoteConfiguration } = await import('@datadog/browser-remote-config') + const result = await fetchRemoteConfiguration({ + applicationId: options.applicationId, + remoteConfigurationId: options.remoteConfigurationId, + site: options.site, + signal: AbortSignal.timeout(CONFIG_FETCH_TIMEOUT_MS), + }) + + if (!result.ok) { + throw new Error( + `Failed to fetch remote configuration: ${result.error.message}\n` + + `Verify applicationId "${options.applicationId}" and ` + + `configId "${options.remoteConfigurationId}" are correct.` + ) + } + + return { ok: true as const, value: result.value } +} + +export function generateCombinedBundle(options: CombineBundleOptions): string { + const { sdkCode, configJs, variant, sdkVersion } = options + const versionDisplay = sdkVersion ?? 'unknown' + + return `/** + * Datadog Browser SDK with Embedded Remote Configuration + * SDK Variant: ${variant} + * SDK Version: ${versionDisplay} + */ +(function() { + 'use strict'; + + // Inline helpers for dynamic value resolution + ${INLINE_HELPERS} + + // Embedded remote configuration + var __DATADOG_REMOTE_CONFIG__ = ${configJs}; + + // SDK bundle (${variant}) from CDN + ${sdkCode} + + // Auto-initialize + if (typeof window !== 'undefined' && window.DD_RUM) { + window.DD_RUM.init(__DATADOG_REMOTE_CONFIG__); + } +})();` +} + +export async function generateBundle(options: GenerateBundleOptions): Promise { + if (!options.applicationId || typeof options.applicationId !== 'string') { + throw new Error("Option 'applicationId' is required and must be a non-empty string.") + } + if (!options.remoteConfigurationId || typeof options.remoteConfigurationId !== 'string') { + throw new Error("Option 'remoteConfigurationId' is required and must be a non-empty string.") + } + if (!VALID_VARIANTS.includes(options.variant)) { + throw new Error(`Option 'variant' must be 'rum' or 'rum-slim', got '${String(options.variant)}'.`) + } + + const configResult = await fetchConfig({ + applicationId: options.applicationId, + remoteConfigurationId: options.remoteConfigurationId, + site: options.site, + }) + + const { resolveDynamicValues, serializeConfigToJs } = await import('@datadog/browser-remote-config/node') + const resolved = resolveDynamicValues(configResult.value) + const configJs = serializeConfigToJs(resolved) + + const { downloadSDK } = await import('./sdkDownloader.ts') + const sdkCode = await downloadSDK({ variant: options.variant, datacenter: options.datacenter }) + + return generateCombinedBundle({ sdkCode, configJs, variant: options.variant }) +} diff --git a/packages/endpoint/src/helpers.ts b/packages/endpoint/src/helpers.ts new file mode 100644 index 0000000000..fd393f1352 --- /dev/null +++ b/packages/endpoint/src/helpers.ts @@ -0,0 +1,11 @@ +/** + * Inline browser helper functions embedded in generated bundles. + * Minimal vanilla JS — no dependencies, no transpilation needed. + */ +export const INLINE_HELPERS = `\ +function __dd_getCookie(n){var m=document.cookie.match(new RegExp('(?:^|; )'+encodeURIComponent(n)+'=([^;]*)'));return m?decodeURIComponent(m[1]):undefined} +function __dd_getJs(p){try{return p.split('.').reduce(function(o,k){return o[k]},window)}catch(e){return undefined}} +function __dd_getDomText(s){try{var e=document.querySelector(s);return e?e.textContent:undefined}catch(e){return undefined}} +function __dd_getDomAttr(s,a){try{var e=document.querySelector(s);return e?e.getAttribute(a):undefined}catch(e){return undefined}} +function __dd_getLocalStorage(k){try{return localStorage.getItem(k)}catch(e){return undefined}} +function __dd_extract(v,p){if(v==null)return undefined;var m=new RegExp(p).exec(String(v));return m?m[1]!==undefined?m[1]:m[0]:undefined}` diff --git a/packages/endpoint/src/index.ts b/packages/endpoint/src/index.ts new file mode 100644 index 0000000000..736f200ee8 --- /dev/null +++ b/packages/endpoint/src/index.ts @@ -0,0 +1,2 @@ +export { generateBundle, generateCombinedBundle, fetchConfig } from './bundleGenerator.ts' +export type { GenerateBundleOptions, CombineBundleOptions, FetchConfigOptions } from './bundleGenerator.ts' diff --git a/packages/endpoint/src/sdkDownloader.ts b/packages/endpoint/src/sdkDownloader.ts new file mode 100644 index 0000000000..47ed8a33e9 --- /dev/null +++ b/packages/endpoint/src/sdkDownloader.ts @@ -0,0 +1,92 @@ +// eslint-disable-next-line local-rules/disallow-side-effects, local-rules/enforce-prod-deps-imports -- Node.js build tool +import https from 'node:https' +// eslint-disable-next-line local-rules/disallow-side-effects, local-rules/enforce-prod-deps-imports -- Node.js build tool +import { createRequire } from 'node:module' + +export type SdkVariant = 'rum' | 'rum-slim' + +export interface DownloadSDKOptions { + variant: SdkVariant + datacenter?: string + version?: string +} + +const CDN_HOST = 'https://www.datadoghq-browser-agent.com' +const DEFAULT_DATACENTER = 'us1' + +const sdkCache = new Map() + +function getDefaultVersion(): string { + const require = createRequire(import.meta.url) + const pkg = require('@datadog/browser-remote-config/package.json') as { version: string } + return pkg.version +} + +function getMajorVersion(version: string): number { + const major = parseInt(version.split('.')[0], 10) + if (isNaN(major)) { + throw new Error(`Invalid SDK version format: ${version}`) + } + return major +} + +export async function downloadSDK(options: SdkVariant | DownloadSDKOptions): Promise { + const variant = typeof options === 'string' ? options : options.variant + const datacenter = typeof options === 'string' ? DEFAULT_DATACENTER : (options.datacenter ?? DEFAULT_DATACENTER) + const version = typeof options === 'string' ? getDefaultVersion() : (options.version ?? getDefaultVersion()) + const majorVersion = getMajorVersion(version) + + const cacheKey = `${variant}-${majorVersion}-${datacenter}` + const cached = sdkCache.get(cacheKey) + if (cached) { + return cached + } + + const cdnUrl = `${CDN_HOST}/${datacenter}/v${majorVersion}/datadog-${variant}.js` + + const sdkCode = await new Promise((resolve, reject) => { + const request = https.get(cdnUrl, { timeout: 30000 }, (res) => { + let data = '' + + if (res.statusCode === 404) { + reject( + new Error( + `SDK bundle not found at ${cdnUrl}\n` + + `Check that variant "${variant}" (major version: v${majorVersion}) is correct.\n` + + `Full SDK version: ${version}` + ) + ) + return + } + + if (!res.statusCode || res.statusCode >= 400) { + reject(new Error(`Failed to download SDK from CDN: HTTP ${res.statusCode}\nURL: ${cdnUrl}`)) + return + } + + res.on('data', (chunk: Buffer | string) => { + data += String(chunk) + }) + + res.on('end', () => { + resolve(data) + }) + }) + + request.on('error', (error: Error) => { + reject(new Error(`Network error downloading SDK from CDN:\nURL: ${cdnUrl}\nError: ${error.message}`)) + }) + + request.on('timeout', () => { + request.destroy() + reject(new Error(`Timeout downloading SDK from CDN (30s):\nURL: ${cdnUrl}`)) + }) + }) + + sdkCache.set(cacheKey, sdkCode) + return sdkCode +} + +export function clearSdkCache(): void { + sdkCache.clear() +} diff --git a/packages/endpoint/tsconfig.json b/packages/endpoint/tsconfig.json new file mode 100644 index 0000000000..53a3cab434 --- /dev/null +++ b/packages/endpoint/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "declaration": true, + "rootDir": "./src/", + "module": "preserve", + "moduleResolution": "bundler", + "target": "ES2024", + "lib": ["ES2024", "DOM"], + "types": ["node"], + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "noEmit": true, + "allowImportingTsExtensions": true + }, + "include": ["./src/**/*.ts"], + "exclude": [] +} diff --git a/tsconfig.default.json b/tsconfig.default.json index 1170dbfce4..c510d13fef 100644 --- a/tsconfig.default.json +++ b/tsconfig.default.json @@ -18,6 +18,9 @@ "scripts", "test/envUtils.ts", + // Files included in ./packages/endpoint/tsconfig.json + "packages/endpoint", + // Files included in ./test/e2e/tsconfig.json "test/e2e", "test/lib", diff --git a/tsconfig.json b/tsconfig.json index 013707afb2..80066487c6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ "references": [ { "path": "./tsconfig.default.json" }, { "path": "./tsconfig.scripts.json" }, + { "path": "./packages/endpoint/tsconfig.json" }, { "path": "./developer-extension/tsconfig.json" }, { "path": "./test/e2e/tsconfig.json" }, { "path": "./test/performance/tsconfig.json" } diff --git a/yarn.lock b/yarn.lock index a2efdfa7ee..e26bf0e997 100644 --- a/yarn.lock +++ b/yarn.lock @@ -299,7 +299,7 @@ __metadata: languageName: unknown linkType: soft -"@datadog/browser-remote-config@workspace:packages/remote-config": +"@datadog/browser-remote-config@npm:6.28.0, @datadog/browser-remote-config@workspace:packages/remote-config": version: 0.0.0-use.local resolution: "@datadog/browser-remote-config@workspace:packages/remote-config" dependencies: @@ -444,6 +444,14 @@ __metadata: languageName: unknown linkType: soft +"@datadog/browser-sdk-endpoint@workspace:packages/endpoint": + version: 0.0.0-use.local + resolution: "@datadog/browser-sdk-endpoint@workspace:packages/endpoint" + dependencies: + "@datadog/browser-remote-config": "npm:6.28.0" + languageName: unknown + linkType: soft + "@datadog/browser-worker@workspace:packages/worker": version: 0.0.0-use.local resolution: "@datadog/browser-worker@workspace:packages/worker" From 32c2c71cafdeb8e65c9e80d44c9527f039910ff8 Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Fri, 13 Mar 2026 14:21:13 +0100 Subject: [PATCH 06/11] =?UTF-8?q?=F0=9F=90=9B=20export=20SdkVariant=20and?= =?UTF-8?q?=20fix=20SDK=20version=20in=20bundle=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/endpoint/src/bundleGenerator.ts | 4 +++- packages/endpoint/src/index.ts | 1 + packages/endpoint/src/sdkDownloader.ts | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/endpoint/src/bundleGenerator.ts b/packages/endpoint/src/bundleGenerator.ts index 6fcb5182a5..6ef47d2de7 100644 --- a/packages/endpoint/src/bundleGenerator.ts +++ b/packages/endpoint/src/bundleGenerator.ts @@ -1,4 +1,5 @@ import type { SdkVariant } from './sdkDownloader.ts' +import { getDefaultVersion } from './sdkDownloader.ts' import { INLINE_HELPERS } from './helpers.ts' export type { SdkVariant } from './sdkDownloader.ts' @@ -98,6 +99,7 @@ export async function generateBundle(options: GenerateBundleOptions): Promise() -function getDefaultVersion(): string { +export function getDefaultVersion(): string { const require = createRequire(import.meta.url) const pkg = require('@datadog/browser-remote-config/package.json') as { version: string } return pkg.version From 5a28264e04e5ecb67073a8122e728cad18cca7e3 Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Fri, 13 Mar 2026 14:22:34 +0100 Subject: [PATCH 07/11] =?UTF-8?q?=E2=9C=85=20add=20bundleGenerator=20tests?= =?UTF-8?q?=20for=20code=20generation=20and=20inline=20helpers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/endpoint/src/bundleGenerator.spec.ts | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 packages/endpoint/src/bundleGenerator.spec.ts diff --git a/packages/endpoint/src/bundleGenerator.spec.ts b/packages/endpoint/src/bundleGenerator.spec.ts new file mode 100644 index 0000000000..b3d73306fa --- /dev/null +++ b/packages/endpoint/src/bundleGenerator.spec.ts @@ -0,0 +1,99 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert' +import { generateCombinedBundle } from './bundleGenerator.ts' +import { INLINE_HELPERS } from './helpers.ts' + +const mockSdkCode = 'window.DD_RUM = { init: function(c) { this.config = c; } };' + +describe('generateCombinedBundle', () => { + it('wraps output in an IIFE', () => { + const bundle = generateCombinedBundle({ + sdkCode: mockSdkCode, + configJs: '{ "sessionSampleRate": 24 }', + variant: 'rum', + }) + assert.ok(bundle.includes('(function() {'), 'Should contain IIFE start') + assert.ok(bundle.includes('})();'), 'Should contain IIFE end') + assert.ok(bundle.includes("'use strict';"), 'Should include use strict') + }) + + it('embeds configJs verbatim without re-serializing', () => { + const configJs = '{ "sessionSampleRate": 24, "user": { id: __dd_getJs(\'window.user\') } }' + const bundle = generateCombinedBundle({ sdkCode: mockSdkCode, configJs, variant: 'rum' }) + assert.ok(bundle.includes(configJs), 'Should embed configJs verbatim') + }) + + it('always includes all six inline helpers', () => { + const bundle = generateCombinedBundle({ + sdkCode: mockSdkCode, + configJs: '{ "sessionSampleRate": 100 }', + variant: 'rum', + }) + assert.ok(bundle.includes('__dd_getCookie'), 'Should include cookie helper') + assert.ok(bundle.includes('__dd_getJs'), 'Should include JS helper') + assert.ok(bundle.includes('__dd_getDomText'), 'Should include DOM text helper') + assert.ok(bundle.includes('__dd_getDomAttr'), 'Should include DOM attr helper') + assert.ok(bundle.includes('__dd_getLocalStorage'), 'Should include localStorage helper') + assert.ok(bundle.includes('__dd_extract'), 'Should include extract helper') + }) + + it('includes variant in header comment', () => { + const bundle = generateCombinedBundle({ + sdkCode: mockSdkCode, + configJs: '{}', + variant: 'rum-slim', + }) + assert.ok(bundle.includes('SDK Variant: rum-slim'), 'Should include variant in header') + }) + + it('includes SDK version in header when provided', () => { + const bundle = generateCombinedBundle({ + sdkCode: mockSdkCode, + configJs: '{}', + variant: 'rum', + sdkVersion: '6.28.0', + }) + assert.ok(bundle.includes('SDK Version: 6.28.0'), 'Should include version in header') + }) + + it('calls DD_RUM.init with the embedded config variable', () => { + const bundle = generateCombinedBundle({ + sdkCode: mockSdkCode, + configJs: '{ "sessionSampleRate": 50 }', + variant: 'rum', + }) + assert.ok(bundle.includes('window.DD_RUM.init(__DATADOG_REMOTE_CONFIG__)'), 'Should call DD_RUM.init') + }) + + it('a config with only static values contains no helper calls in the config section', () => { + const configJs = '{ "sessionSampleRate": 24, "env": "prod" }' + const bundle = generateCombinedBundle({ sdkCode: mockSdkCode, configJs, variant: 'rum' }) + // helpers are present as function definitions but not called from the config + const configSection = bundle.split('var __DATADOG_REMOTE_CONFIG__')[1].split(';')[0] + assert.ok(!configSection.includes('__dd_getCookie('), 'Static config should not call cookie helper') + assert.ok(!configSection.includes('__dd_getJs('), 'Static config should not call JS helper') + }) + + it('a config with dynamic values embeds helper calls in the config section', () => { + const configJs = '{ "user": { id: __dd_getJs(\'window.user\') } }' + const bundle = generateCombinedBundle({ sdkCode: mockSdkCode, configJs, variant: 'rum' }) + const configSection = bundle.split('var __DATADOG_REMOTE_CONFIG__')[1].split(';')[0] + assert.ok(configSection.includes("__dd_getJs('window.user')"), 'Dynamic config should call JS helper') + }) +}) + +describe('INLINE_HELPERS', () => { + it('defines all six helper functions', () => { + assert.ok(INLINE_HELPERS.includes('function __dd_getCookie'), 'Should define getCookie') + assert.ok(INLINE_HELPERS.includes('function __dd_getJs'), 'Should define getJs') + assert.ok(INLINE_HELPERS.includes('function __dd_getDomText'), 'Should define getDomText') + assert.ok(INLINE_HELPERS.includes('function __dd_getDomAttr'), 'Should define getDomAttr') + assert.ok(INLINE_HELPERS.includes('function __dd_getLocalStorage'), 'Should define getLocalStorage') + assert.ok(INLINE_HELPERS.includes('function __dd_extract'), 'Should define extract') + }) + + it('is valid JavaScript', () => { + // Wrapping in a function and parsing via Function constructor validates syntax + assert.doesNotThrow(() => new Function(INLINE_HELPERS), 'INLINE_HELPERS should be valid JS') + }) +}) From 2c9401f23e0c5ebd5ac6b94649d171a4d4e539c7 Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Fri, 13 Mar 2026 14:36:03 +0100 Subject: [PATCH 08/11] =?UTF-8?q?=F0=9F=90=9B=20fix=20nodeContextItemHandl?= =?UTF-8?q?er=20to=20use=20serializeDynamicValueToJs=20directly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/remote-config/src/nodeResolution.spec.ts | 8 +++----- packages/remote-config/src/nodeResolution.ts | 14 ++++++++------ 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/remote-config/src/nodeResolution.spec.ts b/packages/remote-config/src/nodeResolution.spec.ts index 8e3a44a492..132d89ef91 100644 --- a/packages/remote-config/src/nodeResolution.spec.ts +++ b/packages/remote-config/src/nodeResolution.spec.ts @@ -110,12 +110,10 @@ describe('nodeContextItemHandler', () => { expect(result.code).toContain("__dd_getJs('window.env')") }) - it('should skip items where resolve returns undefined', () => { - const items: ContextItem[] = [ - { key: 'id', value: { rcSerializedType: 'dynamic', strategy: 'js', path: 'window.user' } }, - ] + it('should skip items where value is undefined (malformed ContextItem)', () => { + const items = [{ key: 'id', value: undefined as any }] const result = nodeContextItemHandler(items, () => undefined) as CodeExpression - expect(result.code).toBe('{ }') + expect(result.code).toBe('{}') }) }) diff --git a/packages/remote-config/src/nodeResolution.ts b/packages/remote-config/src/nodeResolution.ts index c64bb5c3da..a7c5e03f3a 100644 --- a/packages/remote-config/src/nodeResolution.ts +++ b/packages/remote-config/src/nodeResolution.ts @@ -72,18 +72,20 @@ export function serializeDynamicValueToJs(option: DynamicOption): string { // --------------------------------------------------------------------------- /** @internal */ -export function nodeContextItemHandler(items: ContextItem[], resolve: (value: unknown) => unknown): CodeExpression { +export function nodeContextItemHandler( + items: ContextItem[], + // resolve is intentionally unused — ContextItem values are always DynamicOption, + // so we serialize them directly to JS expressions rather than resolving live values. + _resolve: (value: unknown) => unknown +): CodeExpression { const entries = items .map(({ key, value }) => { if (value === undefined) return null - const resolved = resolve(value) - if (resolved === undefined) return null - const code = isCodeExpression(resolved) ? resolved.code : JSON.stringify(resolved) - return `${JSON.stringify(key)}: ${code}` + return `${JSON.stringify(key)}: ${serializeDynamicValueToJs(value)}` }) .filter((entry): entry is string => entry !== null) - return codeExpression(`{ ${entries.join(', ')} }`) + return codeExpression(entries.length ? `{ ${entries.join(', ')} }` : '{}') } export function serializeConfigToJs(config: unknown): string { From 4c68c1c4e79686106baa1610e2163174e3fb4a8e Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Fri, 13 Mar 2026 16:11:40 +0100 Subject: [PATCH 09/11] =?UTF-8?q?=E2=9C=A8=20add=20logs=20and=20rum-and-lo?= =?UTF-8?q?gs=20variants,=20=5F=5FDD=5FBASE=5FCONFIG=5F=5F=20merge,=20stat?= =?UTF-8?q?ic=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/endpoint/src/bundleGenerator.ts | 17 +++++++++-------- packages/endpoint/src/sdkDownloader.ts | 22 ++++++++++++++++++++-- packages/remote-config/package.json | 3 ++- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/packages/endpoint/src/bundleGenerator.ts b/packages/endpoint/src/bundleGenerator.ts index 6ef47d2de7..eaef70a879 100644 --- a/packages/endpoint/src/bundleGenerator.ts +++ b/packages/endpoint/src/bundleGenerator.ts @@ -1,6 +1,8 @@ import type { SdkVariant } from './sdkDownloader.ts' -import { getDefaultVersion } from './sdkDownloader.ts' +import { getDefaultVersion, downloadSDK } from './sdkDownloader.ts' import { INLINE_HELPERS } from './helpers.ts' +import { fetchRemoteConfiguration } from '@datadog/browser-remote-config' +import { resolveDynamicValues, serializeConfigToJs } from '@datadog/browser-remote-config/node' export type { SdkVariant } from './sdkDownloader.ts' @@ -26,10 +28,9 @@ export interface GenerateBundleOptions { } const CONFIG_FETCH_TIMEOUT_MS = 30_000 -const VALID_VARIANTS: SdkVariant[] = ['rum', 'rum-slim'] +const VALID_VARIANTS: SdkVariant[] = ['rum', 'rum-slim', 'logs', 'rum-and-logs'] export async function fetchConfig(options: FetchConfigOptions) { - const { fetchRemoteConfiguration } = await import('@datadog/browser-remote-config') const result = await fetchRemoteConfiguration({ applicationId: options.applicationId, remoteConfigurationId: options.remoteConfigurationId, @@ -69,9 +70,11 @@ export function generateCombinedBundle(options: CombineBundleOptions): string { // SDK bundle (${variant}) from CDN ${sdkCode} - // Auto-initialize - if (typeof window !== 'undefined' && window.DD_RUM) { - window.DD_RUM.init(__DATADOG_REMOTE_CONFIG__); + // Auto-initialize — merge base config (clientToken, site, etc.) set by the page before this script + var __DD_CONFIG__ = Object.assign({}, window.__DD_BASE_CONFIG__ || {}, __DATADOG_REMOTE_CONFIG__); + if (typeof window !== 'undefined') { + if (window.DD_RUM) { window.DD_RUM.init(__DD_CONFIG__); } + if (window.DD_LOGS) { window.DD_LOGS.init(__DD_CONFIG__); } } })();` } @@ -93,11 +96,9 @@ export async function generateBundle(options: GenerateBundleOptions): Promise, string> = { + rum: 'rum', + 'rum-slim': 'rum-slim', + logs: 'logs', +} export interface DownloadSDKOptions { variant: SdkVariant @@ -34,7 +41,18 @@ export async function downloadSDK(options: SdkVariant | DownloadSDKOptions): Pro const variant = typeof options === 'string' ? options : options.variant const datacenter = typeof options === 'string' ? DEFAULT_DATACENTER : (options.datacenter ?? DEFAULT_DATACENTER) const version = typeof options === 'string' ? getDefaultVersion() : (options.version ?? getDefaultVersion()) + + // rum-and-logs: download both SDKs and concatenate + if (variant === 'rum-and-logs') { + const [rum, logs] = await Promise.all([ + downloadSDK({ variant: 'rum', datacenter, version }), + downloadSDK({ variant: 'logs', datacenter, version }), + ]) + return `${rum}\n${logs}` + } + const majorVersion = getMajorVersion(version) + const cdnVariant = CDN_VARIANTS[variant] const cacheKey = `${variant}-${majorVersion}-${datacenter}` const cached = sdkCache.get(cacheKey) @@ -42,7 +60,7 @@ export async function downloadSDK(options: SdkVariant | DownloadSDKOptions): Pro return cached } - const cdnUrl = `${CDN_HOST}/${datacenter}/v${majorVersion}/datadog-${variant}.js` + const cdnUrl = `${CDN_HOST}/${datacenter}/v${majorVersion}/datadog-${cdnVariant}.js` const sdkCode = await new Promise((resolve, reject) => { const request = https.get(cdnUrl, { timeout: 30000 }, (res) => { diff --git a/packages/remote-config/package.json b/packages/remote-config/package.json index d3dddb777c..7896becb52 100644 --- a/packages/remote-config/package.json +++ b/packages/remote-config/package.json @@ -7,7 +7,8 @@ "types": "cjs/index.d.ts", "exports": { ".": "./src/index.ts", - "./node": "./src/entries/node.ts" + "./node": "./src/entries/node.ts", + "./package.json": "./package.json" }, "scripts": { "build": "node ../../scripts/build/build-package.ts --modules" From 44a3cc5faf3c1e6852b45b02da49c56fda4a6f87 Mon Sep 17 00:00:00 2001 From: Adrian de la Rosa Date: Mon, 16 Mar 2026 12:55:26 +0100 Subject: [PATCH 10/11] =?UTF-8?q?=F0=9F=93=9D=20Add=20README=20for=20@data?= =?UTF-8?q?dog/browser-sdk-endpoint=20with=20SSR=20usage=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/endpoint/README.md | 135 ++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 packages/endpoint/README.md diff --git a/packages/endpoint/README.md b/packages/endpoint/README.md new file mode 100644 index 0000000000..9c7182d34a --- /dev/null +++ b/packages/endpoint/README.md @@ -0,0 +1,135 @@ +# @datadog/browser-sdk-endpoint + +Node.js package for generating self-contained Datadog Browser SDK bundles with embedded remote configuration. Designed for CI/CD pipelines, SSR frameworks, and custom build tooling. + +## Installation + +```bash +npm install @datadog/browser-sdk-endpoint +``` + +## Usage + +### Generate a bundle (CI / build script) + +Fetch remote configuration, download the SDK from CDN, and produce a single self-executing IIFE in one call: + +```typescript +import { generateBundle } from '@datadog/browser-sdk-endpoint' +import { writeFileSync } from 'node:fs' + +const bundle = await generateBundle({ + applicationId: 'your-app-id', + remoteConfigurationId: 'your-config-id', + variant: 'rum', // 'rum' | 'rum-slim' | 'logs' | 'rum-and-logs' + site: 'datadoghq.com', // optional, defaults to datadoghq.com +}) + +writeFileSync('public/datadog-sdk.js', bundle) +``` + +Then in your HTML, set `window.__DD_BASE_CONFIG__` with the fields that belong to the page (not the remote config) before loading the bundle: + +```html + + +``` + +The bundle merges `__DD_BASE_CONFIG__` with the embedded remote configuration and calls `DD_RUM.init()` automatically — no additional `init()` call needed in your app code. + +### SSR: inject configuration at render time + +If you use server-side rendering and want to avoid a separate bundle generation step, you can fetch the remote configuration per request, serialize it to an inline JS expression, and inject it into the HTML response. The SDK is loaded separately; only the configuration is embedded. + +```typescript +import { fetchRemoteConfiguration } from '@datadog/browser-remote-config' +import { resolveDynamicValues, serializeConfigToJs } from '@datadog/browser-remote-config/node' + +// In your SSR handler (Express, Next.js getServerSideProps, etc.) +const result = await fetchRemoteConfiguration({ + applicationId: 'your-app-id', + remoteConfigurationId: 'your-config-id', +}) + +if (result.ok) { + const configJs = serializeConfigToJs(resolveDynamicValues(result.value)) + + // Inject into before the SDK script tag + // configJs is a JS object literal — dynamic values (cookies, DOM, window.*) are + // serialized as inline expressions that evaluate in the browser at page load time + html += `` +} +``` + +Then in your client-side code: + +```typescript +import { datadogRum } from '@datadog/browser-rum' + +datadogRum.init({ + applicationId: 'your-app-id', + clientToken: 'your-client-token', + site: 'datadoghq.com', + ...window.__DD_RC_CONFIG__ +}) +``` + +`resolveDynamicValues` from the Node entry point serializes `DynamicOption` fields (cookie, JS path, DOM, localStorage) as inline JS expressions rather than evaluating them on the server. Those expressions run against live browser APIs when the `