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
60 changes: 57 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,62 @@ 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'
)
})
})

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 +103,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
44 changes: 31 additions & 13 deletions packages/debugger/src/domain/deliveryApi.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
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) {
return `${proxy}${DELIVERY_API_PATH}`
}
return `https://api.${site}${DELIVERY_API_PATH}`
}

interface DeliveryApiResponse {
nextCursor: string
updates: Probe[]
Expand All @@ -31,9 +45,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 +59,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 +81,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 @@ -60,6 +60,15 @@ export interface DebuggerInitConfiguration {
* @defaultValue 60000
*/
pollInterval?: 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 @@ -106,6 +115,9 @@ function makeDebuggerPublicApi(): DebuggerPublicApi {

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
Loading