Skip to content
Merged
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
78 changes: 75 additions & 3 deletions packages/debugger/src/domain/deliveryApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,80 @@ import { registerCleanupTask, mockClock, replaceMockable } from '@datadog/browse
import type { Clock } from '@datadog/browser-core/test'
import { getProbes, clearProbes } from './probes'
import type { Probe } from './probes'
import { startDeliveryApiPolling, stopDeliveryApiPolling, clearDeliveryApiState } from './deliveryApi'
import {
buildDeliveryApiUrl,
startDeliveryApiPolling,
stopDeliveryApiPolling,
clearDeliveryApiState,
} from './deliveryApi'
import type { DeliveryApiConfiguration } from './deliveryApi'

describe('buildDeliveryApiUrl', () => {
it('should default to datadoghq.com', () => {
expect(buildDeliveryApiUrl()).toBe('https://api.datadoghq.com/api/unstable/debugger/frontend/probes')
})

it('should build URL for US1 site', () => {
expect(buildDeliveryApiUrl('datadoghq.com')).toBe('https://api.datadoghq.com/api/unstable/debugger/frontend/probes')
})

it('should build URL for EU1 site', () => {
expect(buildDeliveryApiUrl('datadoghq.eu')).toBe('https://api.datadoghq.eu/api/unstable/debugger/frontend/probes')
})

it('should build URL for US3 site', () => {
expect(buildDeliveryApiUrl('us3.datadoghq.com')).toBe(
'https://api.us3.datadoghq.com/api/unstable/debugger/frontend/probes'
)
})

it('should build URL for staging site', () => {
expect(buildDeliveryApiUrl('datad0g.com')).toBe('https://api.datad0g.com/api/unstable/debugger/frontend/probes')
})

it('should build URL for gov site', () => {
expect(buildDeliveryApiUrl('ddog-gov.com')).toBe('https://api.ddog-gov.com/api/unstable/debugger/frontend/probes')
})

it('should use proxy as origin when provided', () => {
expect(buildDeliveryApiUrl('datadoghq.com', 'http://localhost:9000')).toBe(
'http://localhost:9000/api/unstable/debugger/frontend/probes'
)
})

it('should ignore site when proxy is provided', () => {
expect(buildDeliveryApiUrl('datadoghq.eu', 'http://proxy.example.com')).toBe(
'http://proxy.example.com/api/unstable/debugger/frontend/probes'
)
})

it('should trim a trailing slash from a proxy origin to avoid a double-slash path', () => {
expect(buildDeliveryApiUrl('datadoghq.com', 'https://proxy.example.com/')).toBe(
'https://proxy.example.com/api/unstable/debugger/frontend/probes'
)
})

it('should trim a trailing slash from a proxy that has a sub-path', () => {
expect(buildDeliveryApiUrl('datadoghq.com', 'https://proxy.example.com/dd/')).toBe(
'https://proxy.example.com/dd/api/unstable/debugger/frontend/probes'
)
})

it('should preserve a proxy sub-path that has no trailing slash', () => {
expect(buildDeliveryApiUrl('datadoghq.com', 'https://proxy.example.com/dd')).toBe(
'https://proxy.example.com/dd/api/unstable/debugger/frontend/probes'
)
})
})

describe('deliveryApi', () => {
let fetchSpy: jasmine.Spy
let clock: Clock

function makeConfig(overrides: Partial<DeliveryApiConfiguration> = {}): DeliveryApiConfiguration {
return {
service: 'test-service',
clientToken: 'test-client-token',
env: 'staging',
version: '1.0.0',
pollInterval: 5000,
Expand Down Expand Up @@ -57,11 +121,19 @@ describe('deliveryApi', () => {

expect(fetchSpy).toHaveBeenCalledTimes(1)
const [url, options] = fetchSpy.calls.mostRecent().args
expect(url).toBe('/api/ui/debugger/probe-delivery')
expect(url).toBe('https://api.datadoghq.com/api/unstable/debugger/frontend/probes')
expect(options.method).toBe('POST')
expect(options.credentials).toBe('same-origin')
expect(options.credentials).toBeUndefined()
expect(options.headers['Content-Type']).toBe('application/json; charset=utf-8')
expect(options.headers['Accept']).toBe('application/vnd.datadog.debugger-probes+json; version=1')
expect(options.headers['dd-client-token']).toBe('test-client-token')
})

it('should use the configured site for the request URL', () => {
startDeliveryApiPolling(makeConfig({ site: 'datadoghq.eu' }))

const [url] = fetchSpy.calls.mostRecent().args
expect(url).toBe('https://api.datadoghq.eu/api/unstable/debugger/frontend/probes')
})

it('should send the correct request body', () => {
Expand Down
45 changes: 32 additions & 13 deletions packages/debugger/src/domain/deliveryApi.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,38 @@
import type { TimeoutId } from '@datadog/browser-core'
import { display, fetch, getGlobalObject, mockable, setInterval, clearInterval } from '@datadog/browser-core'
import type { TimeoutId, Site } from '@datadog/browser-core'
import {
display,
fetch,
getGlobalObject,
mockable,
setInterval,
clearInterval,
INTAKE_SITE_US1,
} from '@datadog/browser-core'
import { addProbe, removeProbe } from './probes'
import type { Probe } from './probes'

declare const __BUILD_ENV__SDK_VERSION__: string

const DELIVERY_API_PATH = '/api/ui/debugger/probe-delivery'
const DEFAULT_HEADERS: Record<string, string> = {
'Content-Type': 'application/json; charset=utf-8',
Accept: 'application/vnd.datadog.debugger-probes+json; version=1',
}
const DELIVERY_API_PATH = '/api/unstable/debugger/frontend/probes'

export interface DeliveryApiConfiguration {
service: string
clientToken: string
site?: Site
proxy?: string
env?: string
version?: string
pollInterval?: number
}

export function buildDeliveryApiUrl(site: Site = INTAKE_SITE_US1, proxy?: string): string {
if (proxy) {
proxy = proxy.endsWith('/') ? proxy.slice(0, -1) : proxy
return `${proxy}${DELIVERY_API_PATH}`
Comment thread
watson marked this conversation as resolved.
}
return `https://api.${site}${DELIVERY_API_PATH}`
}

interface DeliveryApiResponse {
nextCursor: string
updates: Probe[]
Expand All @@ -31,9 +46,8 @@ let knownProbeIds = new Set<string>()
/**
* Start polling the Datadog Delivery API for probe updates.
*
* This is designed for dogfooding the Live Debugger inside the Datadog web UI,
* where the user is already authenticated via session cookies (ValidUser auth).
* Requests are same-origin, so no explicit domain is needed.
* Requests are authenticated via `dd-client-token` header (ClientTokenAuth)
* against the public Smart Edge route.
*/
export function startDeliveryApiPolling(config: DeliveryApiConfiguration): void {
if (!('location' in mockable(getGlobalObject)())) {
Expand All @@ -46,6 +60,12 @@ export function startDeliveryApiPolling(config: DeliveryApiConfiguration): void
}

const pollInterval = config.pollInterval || 60_000
const url = buildDeliveryApiUrl(config.site, config.proxy)
const headers: Record<string, string> = {
'Content-Type': 'application/json; charset=utf-8',
Accept: 'application/vnd.datadog.debugger-probes+json; version=1',
'dd-client-token': config.clientToken,
}

const baseRequestBody = {
service: config.service,
Expand All @@ -62,11 +82,10 @@ export function startDeliveryApiPolling(config: DeliveryApiConfiguration): void
body.nextCursor = currentCursor
}

const response = await fetch(DELIVERY_API_PATH, {
const response = await fetch(url, {
method: 'POST',
headers: { ...DEFAULT_HEADERS },
headers,
body: JSON.stringify(body),
credentials: 'same-origin',
})

if (!response.ok) {
Expand Down
12 changes: 12 additions & 0 deletions packages/debugger/src/entries/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,15 @@ export interface DebuggerInitConfiguration {
* @defaultValue 5000
*/
maxNonSnapshotsPerSecondPerProbe?: number

/**
* A proxy URL for routing SDK requests. When set, delivery API requests are
* sent to `{proxy}/api/unstable/debugger/frontend/probes` instead of the
* default Datadog API host derived from `site`.
*
* @category Transport
*/
proxy?: string
}

/**
Expand Down Expand Up @@ -138,6 +147,9 @@ function makeDebuggerPublicApi(): DatadogDebugger {

startDeliveryApiPolling({
service: initConfiguration.service,
clientToken: initConfiguration.clientToken,
site: initConfiguration.site,
proxy: initConfiguration.proxy,
env: initConfiguration.env,
version: initConfiguration.version,
pollInterval: initConfiguration.pollInterval,
Expand Down
5 changes: 4 additions & 1 deletion test/e2e/lib/framework/httpServers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export type ServerApp = (req: http.IncomingMessage, res: http.ServerResponse) =>

export type MockServerApp = ServerApp & {
getLargeResponseWroteSize(): number
}

export type IntakeServerApp = ServerApp & {
setDebuggerProbes(probes: object[]): void
}

Expand All @@ -26,7 +29,7 @@ export interface Server<App extends ServerApp> {

export interface Servers {
base: Server<MockServerApp>
intake: Server<ServerApp>
intake: Server<IntakeServerApp>
crossOrigin: Server<MockServerApp>
}

Expand Down
11 changes: 10 additions & 1 deletion test/e2e/lib/framework/serverApps/intake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,19 @@ import type { IntakeRegistry } from '../intakeRegistry'

export function createIntakeServerApp(intakeRegistry: IntakeRegistry) {
const app = express()
let debuggerProbes: object[] = []

app.use(cors())

app.post('/', createIntakeProxyMiddleware({ onRequest: (request) => intakeRegistry.push(request) }))

return app
app.post('/api/unstable/debugger/frontend/probes', (_req, res) => {
res.json({ nextCursor: '', updates: debuggerProbes, deletions: [] })
})

return Object.assign(app, {
setDebuggerProbes(probes: object[]) {
debuggerProbes = probes
},
})
}
8 changes: 0 additions & 8 deletions test/e2e/lib/framework/serverApps/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ export function createMockServerApp(servers: Servers, setup: string, setupOption
const { remoteConfiguration, worker } = setupOptions ?? {}
const app = express()
let largeResponseBytesWritten = 0
let debuggerProbes: object[] = []

app.use(cors())
app.disable('etag') // disable automatic resource caching
Expand Down Expand Up @@ -228,17 +227,10 @@ export function createMockServerApp(servers: Servers, setup: string, setupOption
res.send(JSON.stringify(remoteConfiguration))
})

app.post('/api/ui/debugger/probe-delivery', (_req, res) => {
res.json({ nextCursor: '', updates: debuggerProbes, deletions: [] })
})

return Object.assign(app, {
getLargeResponseWroteSize() {
return largeResponseBytesWritten
},
setDebuggerProbes(probes: object[]) {
debuggerProbes = probes
},
})
}

Expand Down
2 changes: 1 addition & 1 deletion test/e2e/scenario/debugger.scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createTest } from '../lib/framework'
import type { Servers } from '../lib/framework'

function setDebuggerProbes(servers: Servers, probes: object[]) {
servers.base.app.setDebuggerProbes(probes)
servers.intake.app.setDebuggerProbes(probes)
}

function makeProbe({
Expand Down
7 changes: 4 additions & 3 deletions test/performance/createBenchmarkTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export function createBenchmarkTest(scenarioName: string) {
}

if (shouldInjectDebugger(scenarioConfiguration)) {
await injectDebugger(page, scenarioConfiguration)
await injectDebugger(page, scenarioConfiguration, server.origin)
}

await runner(page, takeMeasurements, buildAppUrl(server.origin, scenarioConfiguration))
Expand Down Expand Up @@ -151,11 +151,11 @@ async function injectRumSDK(page: Page, scenarioConfiguration: ScenarioConfigura
* statistical soundness: it ensures V8 can JIT-optimize against the final code path during
* the warmup phase, instead of deoptimizing mid-measurement when probes appear.
*
* Step 3 starts an async probe-delivery poll (mocked by the test server) and flips
* Step 3 starts an async probe-delivery poll (proxied to the test server) and flips
* `__benchmarkReady` once the response is observed. The benchmark scenario is responsible
* for awaiting that flag before running its warmup.
*/
async function injectDebugger(page: Page, scenarioConfiguration: ScenarioConfiguration) {
async function injectDebugger(page: Page, scenarioConfiguration: ScenarioConfiguration, proxy: string) {
await page.addInitScript(() => {
;(window as any).USE_INSTRUMENTED = true
;(window as BrowserWindow).__benchmarkReady = false
Expand All @@ -173,6 +173,7 @@ async function injectDebugger(page: Page, scenarioConfiguration: ScenarioConfigu
const configuration: DebuggerInitConfiguration = {
clientToken: CLIENT_TOKEN,
site: DATADOG_SITE,
proxy,
// The mock probe-delivery handler keys off `service` to decide which probes to return,
// so we use the configuration name to keep parallel benchmark workers isolated.
service: scenarioConfiguration,
Expand Down
2 changes: 1 addition & 1 deletion test/performance/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface Server {

// Probe-delivery endpoint hardcoded in `packages/debugger/src/domain/deliveryApi.ts`.
// Must be served from the same origin as the page since the debugger uses same-origin fetch.
const DEBUGGER_PROBE_DELIVERY_PATH = '/api/ui/debugger/probe-delivery'
const DEBUGGER_PROBE_DELIVERY_PATH = '/api/unstable/debugger/frontend/probes'

export function startPerformanceServer(scenarioName: string): Promise<Server> {
return new Promise((resolve, reject) => {
Expand Down
Loading