diff --git a/packages/debugger/README.md b/packages/debugger/README.md index 463982ce71..cee8044656 100644 --- a/packages/debugger/README.md +++ b/packages/debugger/README.md @@ -16,10 +16,14 @@ datadogDebugger.init({ site: '', service: 'my-web-application', // env: 'production', - // version: '1.0.0', + // version: 'my-deployed-build-version', }) ``` +When you also use the Datadog Live Debugger build plugin, `init().version` defaults to the build-time `liveDebugger.version` metadata injected into the bundle. If you pass both values explicitly and they differ, the SDK keeps the `init()` value and logs a warning. + +If provided, `version` should be set to the immutable deployed browser build identifier used for source map upload and browser build resolution. If omitted, debugger delivery and snapshots still work, but browser build lookup and source-aware resolution may be unavailable. + ## Troubleshooting Need help? Contact [Datadog Support][3]. diff --git a/packages/debugger/src/entries/main.spec.ts b/packages/debugger/src/entries/main.spec.ts index c75239d842..52495be1ff 100644 --- a/packages/debugger/src/entries/main.spec.ts +++ b/packages/debugger/src/entries/main.spec.ts @@ -1,6 +1,30 @@ +import { display } from '@datadog/browser-core' +import { registerCleanupTask, replaceMockableWithSpy } from '@datadog/browser-core/test' +import { initDebuggerTransport } from '../domain/api' +import { startDeliveryApiPolling } from '../domain/deliveryApi' +import { startDebuggerBatch } from '../transport/startDebuggerBatch' +import type { BrowserWindow } from './main' import { datadogDebugger } from './main' describe('datadogDebugger', () => { + const browserWindow: BrowserWindow = window + + beforeEach(() => { + delete browserWindow.__DD_LIVE_DEBUGGER_BUILD__ + delete browserWindow.$dd_entry + delete browserWindow.$dd_return + delete browserWindow.$dd_throw + delete browserWindow.$dd_probes + + registerCleanupTask(() => { + delete browserWindow.__DD_LIVE_DEBUGGER_BUILD__ + delete browserWindow.$dd_entry + delete browserWindow.$dd_return + delete browserWindow.$dd_throw + delete browserWindow.$dd_probes + }) + }) + it('should only expose init, version, and onReady', () => { expect(datadogDebugger).toEqual({ init: jasmine.any(Function), @@ -8,4 +32,67 @@ describe('datadogDebugger', () => { onReady: jasmine.any(Function), }) }) + + it('should default the init version from build-plugin metadata', async () => { + browserWindow.__DD_LIVE_DEBUGGER_BUILD__ = { version: 'build-version' } + replaceMockableWithSpy(startDebuggerBatch).and.callFake(() => ({ + flushController: undefined as any, + add: () => undefined, + flush: () => undefined, + stop: () => undefined, + upsert: () => undefined, + })) + const initTransportSpy = replaceMockableWithSpy(initDebuggerTransport) + const startDeliveryApiPollingSpy = replaceMockableWithSpy(startDeliveryApiPolling) + + datadogDebugger.init({ + clientToken: 'client-token', + service: 'service-name', + env: 'staging', + }) + + await flushPromises() + + expect(initTransportSpy).toHaveBeenCalledWith( + jasmine.objectContaining({ version: 'build-version' }), + jasmine.anything() + ) + expect(startDeliveryApiPollingSpy).toHaveBeenCalledWith(jasmine.objectContaining({ version: 'build-version' })) + expect(browserWindow.$dd_entry).toBeDefined() + expect(browserWindow.$dd_return).toBeDefined() + expect(browserWindow.$dd_throw).toBeDefined() + expect(browserWindow.$dd_probes).toBeDefined() + }) + + it('should warn when the explicit init version mismatches build-plugin metadata', async () => { + browserWindow.__DD_LIVE_DEBUGGER_BUILD__ = { version: 'build-version' } + replaceMockableWithSpy(startDebuggerBatch).and.callFake(() => ({ + flushController: undefined as any, + add: () => undefined, + flush: () => undefined, + stop: () => undefined, + upsert: () => undefined, + })) + replaceMockableWithSpy(initDebuggerTransport) + const startDeliveryApiPollingSpy = replaceMockableWithSpy(startDeliveryApiPolling) + const warnSpy = spyOn(display, 'warn') + + datadogDebugger.init({ + clientToken: 'client-token', + service: 'service-name', + env: 'staging', + version: 'runtime-version', + }) + + await flushPromises() + + expect(warnSpy).toHaveBeenCalledWith(jasmine.stringMatching(/does not match the build-plugin version/)) + expect(startDeliveryApiPollingSpy).toHaveBeenCalledWith(jasmine.objectContaining({ version: 'runtime-version' })) + }) }) + +async function flushPromises() { + for (let i = 0; i < 10; i++) { + await Promise.resolve() + } +} diff --git a/packages/debugger/src/entries/main.ts b/packages/debugger/src/entries/main.ts index dac1e80de1..f5327d2835 100644 --- a/packages/debugger/src/entries/main.ts +++ b/packages/debugger/src/entries/main.ts @@ -6,18 +6,15 @@ * @see [Live Debugger Documentation](https://docs.datadoghq.com/tracing/live_debugger/) */ -import { defineGlobal, getGlobalObject, makePublicApi } from '@datadog/browser-core' +import { defineGlobal, display, getGlobalObject, makePublicApi, mockable } from '@datadog/browser-core' import type { PublicApi, Site } from '@datadog/browser-core' -import { onEntry, onReturn, onThrow, initDebuggerTransport } from '../domain/api' +import { initDebuggerTransport, onEntry, onReturn, onThrow } from '../domain/api' import { startDeliveryApiPolling } from '../domain/deliveryApi' import { getProbes } from '../domain/probes' import { startDebuggerBatch } from '../transport/startDebuggerBatch' -type DebuggerInstrumentationGlobal = typeof globalThis & { - $dd_entry?: typeof onEntry - $dd_return?: typeof onReturn - $dd_throw?: typeof onThrow - $dd_probes?: typeof getProbes +export interface DebuggerBuildMetadata { + version?: string } /** @@ -120,39 +117,68 @@ export interface DatadogDebugger extends PublicApi { * service: 'my-app', * site: 'datadoghq.com', * env: 'production' + * version: 'my-deployed-build-version', * }) * ``` */ init: (initConfiguration: DebuggerInitConfiguration) => void } +export interface BrowserWindow extends Window { + DD_DEBUGGER?: DatadogDebugger + __DD_LIVE_DEBUGGER_BUILD__?: DebuggerBuildMetadata + $dd_entry?: typeof onEntry + $dd_return?: typeof onReturn + $dd_throw?: typeof onThrow + $dd_probes?: typeof getProbes +} + +function resolveDebuggerVersion(initConfiguration: DebuggerInitConfiguration): string | undefined { + const buildVersion = getGlobalObject().__DD_LIVE_DEBUGGER_BUILD__?.version + + if ( + initConfiguration.version !== undefined && + buildVersion !== undefined && + initConfiguration.version !== buildVersion + ) { + display.warn( + `Debugger: init version "${initConfiguration.version}" does not match the build-plugin version "${buildVersion}". Using the init version.` + ) + } + + return initConfiguration.version ?? buildVersion +} + /** * Create the public API for the Live Debugger */ function makeDebuggerPublicApi(): DatadogDebugger { return makePublicApi({ init: (initConfiguration: DebuggerInitConfiguration) => { + const resolvedConfiguration = { + ...initConfiguration, + version: resolveDebuggerVersion(initConfiguration), + } + // Initialize debugger's own transport - const batch = startDebuggerBatch(initConfiguration) - initDebuggerTransport(initConfiguration, batch) + const batch = mockable(startDebuggerBatch)(resolvedConfiguration) + mockable(initDebuggerTransport)(resolvedConfiguration, batch) // Expose internal hooks on globalThis for instrumented code - if (typeof globalThis !== 'undefined') { - const debuggerGlobal = globalThis as DebuggerInstrumentationGlobal - debuggerGlobal.$dd_entry = onEntry - debuggerGlobal.$dd_return = onReturn - debuggerGlobal.$dd_throw = onThrow - debuggerGlobal.$dd_probes = getProbes - } - - startDeliveryApiPolling({ - service: initConfiguration.service, - clientToken: initConfiguration.clientToken, - site: initConfiguration.site, - proxy: initConfiguration.proxy, - env: initConfiguration.env, - version: initConfiguration.version, - pollInterval: initConfiguration.pollInterval, + const debuggerGlobal = getGlobalObject() + debuggerGlobal.$dd_entry = onEntry + debuggerGlobal.$dd_return = onReturn + debuggerGlobal.$dd_throw = onThrow + debuggerGlobal.$dd_probes = getProbes + + mockable(startDeliveryApiPolling)({ + service: resolvedConfiguration.service, + clientToken: resolvedConfiguration.clientToken, + site: resolvedConfiguration.site, + proxy: resolvedConfiguration.proxy, + env: resolvedConfiguration.env, + version: resolvedConfiguration.version, + pollInterval: resolvedConfiguration.pollInterval, }) }, }) @@ -167,8 +193,4 @@ function makeDebuggerPublicApi(): DatadogDebugger { */ export const datadogDebugger = makeDebuggerPublicApi() -export interface BrowserWindow extends Window { - DD_DEBUGGER?: DatadogDebugger -} - defineGlobal(getGlobalObject(), 'DD_DEBUGGER', datadogDebugger)