diff --git a/developer-extension/src/background/domain/syncRules.ts b/developer-extension/src/background/domain/syncRules.ts index 11087ae5b5..e7d762675f 100644 --- a/developer-extension/src/background/domain/syncRules.ts +++ b/developer-extension/src/background/domain/syncRules.ts @@ -1,5 +1,4 @@ import type { NetRequestRulesOptions } from '../../common/extension.types' -import { DEV_RUM_SLIM_URL, DEV_RUM_URL, DEV_SERVER_ORIGIN } from '../../common/packagesUrlConstants' import { INTAKE_DOMAINS } from '../../common/intakeDomainConstants' import { createLogger } from '../../common/logger' import { onDevtoolsDisconnection, onDevtoolsMessage } from '../devtoolsPanelConnection' @@ -51,26 +50,11 @@ async function getExistingRulesInfos(tabId: number) { return { tabRuleIds, nextRuleId } } -function buildRules( - { tabId, useDevBundles, useRumSlim, blockIntakeRequests }: NetRequestRulesOptions, - nextRuleId: number -) { +function buildRules({ tabId, useRumSlim, blockIntakeRequests }: NetRequestRulesOptions, nextRuleId: number) { const rules: chrome.declarativeNetRequest.Rule[] = [] let id = nextRuleId - if (useDevBundles === 'cdn') { - const devRumUrl = useRumSlim ? DEV_RUM_SLIM_URL : DEV_RUM_URL - logger.log('add redirect to dev bundles rules') - rules.push( - createRedirectRule(/^https:\/\/.*\/datadog-(rum|rum-slim|logs)(-[\w-]+)?\.js$/, { - regexSubstitution: `${DEV_SERVER_ORIGIN}/datadog-\\1.js`, - }), - createRedirectRule(/^https:\/\/.*\/chunks\/(\w+)(-\w+)?-datadog-rum.js$/, { - regexSubstitution: `${DEV_SERVER_ORIGIN}/chunks/\\1-datadog-rum.js`, - }), - createRedirectRule('https://localhost:8443/static/datadog-rum-hotdog.js', { url: devRumUrl }) - ) - } else if (useRumSlim) { + if (useRumSlim) { logger.log('add redirect to rum slim rule') rules.push(createRedirectRule(/^(https:\/\/.*\/datadog-rum)(-slim)?/, { regexSubstitution: '\\1-slim' })) } diff --git a/developer-extension/src/common/extension.types.ts b/developer-extension/src/common/extension.types.ts index 6f99b3c5ca..134bd8cd7a 100644 --- a/developer-extension/src/common/extension.types.ts +++ b/developer-extension/src/common/extension.types.ts @@ -13,8 +13,9 @@ export interface DevtoolsToBackgroundMessage { options: NetRequestRulesOptions } -export type DevBundlesOverride = false | 'cdn' | 'npm' +export type DevBundlesOverride = false | 'npm' +export type InjectCdnProd = 'off' | 'on' export interface NetRequestRulesOptions { tabId: number useDevBundles: DevBundlesOverride @@ -57,4 +58,5 @@ export interface Settings { logsConfigurationOverride: object | null debugMode: boolean datadogMode: boolean + injectCdnProd: InjectCdnProd } diff --git a/developer-extension/src/common/packagesUrlConstants.ts b/developer-extension/src/common/packagesUrlConstants.ts index 9ec3f66a3f..38e49cc1f4 100644 --- a/developer-extension/src/common/packagesUrlConstants.ts +++ b/developer-extension/src/common/packagesUrlConstants.ts @@ -11,3 +11,8 @@ export const PROD_REPLAY_SANDBOX_URL = `${PROD_REPLAY_SANDBOX_ORIGIN}/${PROD_REP export const DEV_REPLAY_SANDBOX_ORIGIN = 'https://localhost:8443' export const DEV_REPLAY_SANDBOX_URL = `${DEV_REPLAY_SANDBOX_ORIGIN}/static-apps/replay-sandbox/public/index.html` + +export const CDN_BASE_URL = 'https://www.datadoghq-browser-agent.com' +export const CDN_RUM_URL = `${CDN_BASE_URL}/us1/v6/datadog-rum.js` +export const CDN_RUM_SLIM_URL = `${CDN_BASE_URL}/us1/v6/datadog-rum-slim.js` +export const CDN_LOGS_URL = `${CDN_BASE_URL}/us1/v6/datadog-logs.js` diff --git a/developer-extension/src/content-scripts/main.ts b/developer-extension/src/content-scripts/main.ts index 149261212d..8f42a8f8eb 100644 --- a/developer-extension/src/content-scripts/main.ts +++ b/developer-extension/src/content-scripts/main.ts @@ -1,7 +1,17 @@ import type { Settings } from '../common/extension.types' import { EventListeners } from '../common/eventListeners' -import { DEV_LOGS_URL, DEV_RUM_SLIM_URL, DEV_RUM_URL } from '../common/packagesUrlConstants' +import { + CDN_LOGS_URL, + CDN_RUM_SLIM_URL, + CDN_RUM_URL, + DEV_LOGS_URL, + DEV_RUM_SLIM_URL, + DEV_RUM_URL, +} from '../common/packagesUrlConstants' import { SESSION_STORAGE_SETTINGS_KEY } from '../common/sessionKeyConstant' +import { createLogger } from '../common/logger' + +const logger = createLogger('main') declare global { interface Window extends EventTarget { @@ -33,6 +43,8 @@ export function main() { ) { const ddRumGlobal = instrumentGlobal('DD_RUM') const ddLogsGlobal = instrumentGlobal('DD_LOGS') + const shouldInjectCdnBundles = settings.injectCdnProd === 'on' + const shouldUseRedirect = settings.useDevBundles === 'npm' if (settings.debugMode) { setDebug(ddRumGlobal) @@ -47,7 +59,14 @@ export function main() { overrideInitConfiguration(ddLogsGlobal, settings.logsConfigurationOverride) } - if (settings.useDevBundles === 'npm') { + if (shouldInjectCdnBundles && shouldUseRedirect) { + void injectCdnBundles({ useRumSlim: settings.useRumSlim }).then(() => { + injectDevBundle(settings.useRumSlim ? DEV_RUM_SLIM_URL : DEV_RUM_URL, ddRumGlobal) + injectDevBundle(DEV_LOGS_URL, ddLogsGlobal) + }) + } else if (shouldInjectCdnBundles) { + void injectCdnBundles({ useRumSlim: settings.useRumSlim }) + } else if (shouldUseRedirect) { injectDevBundle(settings.useRumSlim ? DEV_RUM_SLIM_URL : DEV_RUM_URL, ddRumGlobal) injectDevBundle(DEV_LOGS_URL, ddLogsGlobal) } @@ -83,11 +102,34 @@ function noBrowserSdkLoaded() { return !window.DD_RUM && !window.DD_LOGS } -function injectDevBundle(url: string, global: GlobalInstrumentation) { +function injectDevBundle(url: string, global: GlobalInstrumentation, config?: object | null) { + const existingInstance = global.get() + + let initConfig = config + if ( + existingInstance && + 'getInitConfiguration' in existingInstance && + typeof existingInstance.getInitConfiguration === 'function' + ) { + try { + initConfig = existingInstance.getInitConfiguration() || config + } catch { + initConfig = config + } + } + loadSdkScriptFromURL(url) const devInstance = global.get() as SdkPublicApi if (devInstance) { + if (initConfig && 'init' in devInstance && typeof devInstance.init === 'function') { + try { + ;(devInstance as { init(config: object): void }).init(initConfig) + } catch (error) { + logger.error('[DD Browser SDK extension] Error initializing dev bundle:', error) + } + } + global.onSet((sdkInstance) => proxySdk(sdkInstance, devInstance)) global.returnValue(devInstance) } @@ -140,11 +182,18 @@ function loadSdkScriptFromURL(url: string) { // // We'll probably have to revisit when using actual `import()` expressions instead of relying on // Webpack runtime to load the chunks. + // Extract the base directory URL from the full file URL. + const baseUrl = url.substring(0, url.lastIndexOf('/') + 1) + + // Replace the webpack error throw to set scriptUrl. sdkCode = sdkCode.replace( 'if (!scriptUrl) throw new Error("Automatic publicPath is not supported in this browser");', - `if (!scriptUrl) scriptUrl = ${JSON.stringify(url)};` + `if (!scriptUrl) scriptUrl = ${JSON.stringify(baseUrl)};` ) + // Override webpack publicPath assignments to ensure chunks load from dev server + sdkCode = sdkCode.replace(/(__webpack_require__\.p\s*=\s*)([^;]+);/g, `$1${JSON.stringify(baseUrl)};`) + const script = document.createElement('script') script.type = 'text/javascript' script.text = sdkCode @@ -182,3 +231,89 @@ function instrumentGlobal(global: 'DD_RUM' | 'DD_LOGS') { function proxySdk(target: SdkPublicApi, root: SdkPublicApi) { Object.assign(target, root) } + +function injectCdnBundles({ useRumSlim }: { useRumSlim: boolean }) { + const rumUrl = useRumSlim ? CDN_RUM_SLIM_URL : CDN_RUM_URL + const logsUrl = CDN_LOGS_URL + + return injectWhenDocumentReady(() => + Promise.all([ + injectAndInitializeSDK(rumUrl, 'DD_RUM', getDefaultRumConfig()), + injectAndInitializeSDK(logsUrl, 'DD_LOGS', getDefaultLogsConfig()), + ]).then(() => undefined) + ) +} + +function injectWhenDocumentReady(callback: () => Promise | T) { + if (document.readyState === 'loading') { + return new Promise((resolve) => { + document.addEventListener( + 'DOMContentLoaded', + () => { + resolve(callback()) + }, + { once: true } + ) + }) + } + + return Promise.resolve(callback()) +} + +function injectAndInitializeSDK(url: string, globalName: 'DD_RUM' | 'DD_LOGS', config: object | null) { + if (window[globalName]) { + return Promise.resolve() + } + + return new Promise((resolve) => { + const script = document.createElement('script') + script.src = url + script.async = true + script.onload = () => { + const sdkGlobal = window[globalName] + if (config && sdkGlobal && 'init' in sdkGlobal) { + try { + ;(sdkGlobal as { init(config: object): void }).init(config) + } catch (error) { + // Ignore "already initialized" errors - this can happen when dev bundles override CDN bundles + const errorMessage = error instanceof Error ? error.message : String(error) + if (!errorMessage.includes('already initialized')) { + // Only log non-initialization errors + // eslint-disable-next-line no-console + console.error(`[DD Browser SDK extension] Error initializing ${globalName}:`, error) + } + } + } + resolve() + } + script.onerror = () => { + resolve() + } + + try { + document.head.appendChild(script) + } catch { + document.documentElement.appendChild(script) + } + }) +} + +function getDefaultRumConfig() { + return { + applicationId: 'xxx', + clientToken: 'xxx', + site: 'datadoghq.com', + service: 'browser-sdk-extension', + allowedTrackingOrigins: [location.origin], + sessionReplaySampleRate: 100, + } +} + +function getDefaultLogsConfig() { + return { + clientToken: 'xxx', + site: 'datadoghq.com', + service: 'browser-sdk-extension', + allowedTrackingOrigins: [location.origin], + } +} diff --git a/developer-extension/src/panel/components/panel.tsx b/developer-extension/src/panel/components/panel.tsx index cca7d405d2..9b527c651b 100644 --- a/developer-extension/src/panel/components/panel.tsx +++ b/developer-extension/src/panel/components/panel.tsx @@ -100,7 +100,7 @@ export function Panel() { } function isInterceptingNetworkRequests(settings: Settings) { - return settings.blockIntakeRequests || settings.useDevBundles || settings.useRumSlim + return settings.blockIntakeRequests || settings.useRumSlim } function isOverridingInitConfiguration(settings: Settings) { diff --git a/developer-extension/src/panel/components/tabs/settingsTab.tsx b/developer-extension/src/panel/components/tabs/settingsTab.tsx index 77ab8260cb..a510f9c727 100644 --- a/developer-extension/src/panel/components/tabs/settingsTab.tsx +++ b/developer-extension/src/panel/components/tabs/settingsTab.tsx @@ -5,7 +5,7 @@ import { DevServerStatus, useDevServerStatus } from '../../hooks/useDevServerSta import { useSettings } from '../../hooks/useSettings' import { Columns } from '../columns' import { TabBase } from '../tabBase' -import type { DevBundlesOverride, EventCollectionStrategy } from '../../../common/extension.types' +import type { DevBundlesOverride, EventCollectionStrategy, InjectCdnProd } from '../../../common/extension.types' export function SettingsTab() { const sdkDevServerStatus = useDevServerStatus(DEV_LOGS_URL) @@ -21,6 +21,7 @@ export function SettingsTab() { autoFlush, debugMode: debug, datadogMode, + injectCdnProd, }, setSetting, ] = useSettings() @@ -36,8 +37,11 @@ export function SettingsTab() { Browser SDK - {sdkDevServerStatus === DevServerStatus.AVAILABLE && useDevBundles ? ( - Overridden + {(sdkDevServerStatus === DevServerStatus.AVAILABLE && useDevBundles) || + (sdkDevServerStatus === DevServerStatus.AVAILABLE && useDevBundles && injectCdnProd === 'on') ? ( + Injected Local Dev + ) : injectCdnProd === 'on' ? ( + Injected CDN ) : sdkDevServerStatus === DevServerStatus.AVAILABLE ? ( Available ) : sdkDevServerStatus === DevServerStatus.CHECKING ? ( @@ -50,12 +54,31 @@ export function SettingsTab() { - Use the local development version of the browser SDK. The development server must be running; to - start it, run yarn dev. + Use the local or production version of the browser SDK. For local development, the development + server must be running; to start it, run yarn dev. + + CDN Prod Injection: + setSetting('injectCdnProd', value as InjectCdnProd)} + /> + + } + description={<>Inject the CDN Prod RUM and Logs bundles into the page.} + /> + @@ -66,7 +89,6 @@ export function SettingsTab() { size="xs" data={[ { value: 'off', label: 'Off' }, - { value: 'cdn', label: 'Redirect' }, { value: 'npm', label: 'Inject' }, ]} onChange={(value) => @@ -77,9 +99,12 @@ export function SettingsTab() { } description={ <> - Choose an override strategy. Network request redirection is reliable, but only works for CDN - setups. Injecting the bundle into the page can work for both CDN and NPM setups, but it's not - always reliable. + Behavior: +
    +
  • Off / Redirect: Injects dev bundle and initializes
  • +
  • On / Off: Injects and initializes Prod CDN SDK
  • +
  • On / Redirect: Injects Prod CDN SDK, then redirects to dev server
  • +
} /> diff --git a/developer-extension/src/panel/hooks/useSettings.ts b/developer-extension/src/panel/hooks/useSettings.ts index c874513b8c..5302003c54 100644 --- a/developer-extension/src/panel/hooks/useSettings.ts +++ b/developer-extension/src/panel/hooks/useSettings.ts @@ -19,6 +19,7 @@ const DEFAULT_SETTINGS: Readonly = { logsConfigurationOverride: null, debugMode: false, datadogMode: false, + injectCdnProd: 'off', } let settings: Settings | undefined