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
7 changes: 7 additions & 0 deletions test/apps/vanilla/app.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { datadogLogs } from '@datadog/browser-logs'
import { datadogRum } from '@datadog/browser-rum'
import { datadogDebugger } from '@datadog/browser-debugger'

declare global {
interface Window {
LOGS_INIT?: () => void
RUM_INIT?: () => void
DEBUGGER_INIT?: () => void
}
}

Expand All @@ -16,9 +18,14 @@ if (typeof window !== 'undefined') {
if (window.RUM_INIT) {
window.RUM_INIT()
}

if (window.DEBUGGER_INIT) {
window.DEBUGGER_INIT()
}
} else {
// compat test
datadogLogs.init({ clientToken: 'xxx', beforeSend: undefined })
datadogRum.init({ clientToken: 'xxx', applicationId: 'xxx', beforeSend: undefined })
datadogRum.setUser({ id: undefined })
datadogDebugger.init({ clientToken: 'xxx', service: 'xxx' })
}
9 changes: 7 additions & 2 deletions test/apps/vanilla/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@
},
"peerDependencies": {
"@datadog/browser-logs": "*",
"@datadog/browser-rum": "*"
"@datadog/browser-rum": "*",
"@datadog/browser-debugger": "*"
},
"peerDependenciesMeta": {
"@datadog/browser-logs": {
"optional": true
},
"@datadog/browser-rum": {
"optional": true
},
"@datadog/browser-debugger": {
"optional": true
}
},
"resolutions": {
Expand All @@ -25,7 +29,8 @@
"@datadog/browser-rum-core": "file:../../../packages/rum-core/package.tgz",
"@datadog/browser-rum-react": "file:../../../packages/rum-react/package.tgz",
"@datadog/browser-rum-slim": "file:../../../packages/rum-slim/package.tgz",
"@datadog/browser-worker": "file:../../../packages/worker/package.tgz"
"@datadog/browser-worker": "file:../../../packages/worker/package.tgz",
"@datadog/browser-debugger": "file:../../../packages/debugger/package.tgz"
},
"devDependencies": {
"ts-loader": "9.5.7",
Expand Down
304 changes: 304 additions & 0 deletions test/e2e/scenario/debugger.scenario.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
/* eslint-disable @typescript-eslint/no-unsafe-call, camelcase */
import { test, expect } from '@playwright/test'
import type { Page } from '@playwright/test'
import { createTest } from '../lib/framework'
import type { Servers } from '../lib/framework'

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

function makeProbe({
id = 'test-probe-1',
version = 1,
typeName = 'TestModule',
methodName = 'testFunction',
template = 'Probe hit',
captureSnapshot = true,
evaluateAt = 'EXIT' as const,
condition,
segments,
}: {
id?: string
version?: number
typeName?: string
methodName?: string
template?: string
captureSnapshot?: boolean
evaluateAt?: 'ENTRY' | 'EXIT'
condition?: { dsl: string; json: unknown }
segments?: Array<{ str?: string; dsl?: string; json?: unknown }>
} = {}) {
return {
id,
version,
type: 'LOG_PROBE',
where: { typeName, methodName },
template,
segments: segments ?? [{ str: template }],
captureSnapshot,
capture: {},
sampling: { snapshotsPerSecond: 5000 },
evaluateAt,
when: condition,
}
}

/**
* Injects an instrumented function into the page that calls the debugger hooks.
* The function is named `testFunction` and registered under the `TestModule;testFunction` function ID.
*/
async function injectInstrumentedFunction(page: Page) {
await page.evaluate(() => {
const $dd_probes = (globalThis as any).$dd_probes as (id: string) => unknown[] | undefined
const $dd_entry = (globalThis as any).$dd_entry as (probes: unknown[], self: unknown, args: object) => void
const $dd_return = (globalThis as any).$dd_return as (
probes: unknown[],
value: unknown,
self: unknown,
args: object,
locals: object
) => unknown

;(window as any).testFunction = function testFunction(a: unknown, b: unknown) {
const probes = $dd_probes('TestModule;testFunction')
if (probes) {
$dd_entry(probes, this, { a, b })
}
const result = String(a) + String(b)
const returnValue = result
if (probes) {
return $dd_return(probes, returnValue, this, { a, b }, { result })
}
return returnValue
}
})
}

/**
* Injects an instrumented function that throws, triggering `$dd_throw`.
*/
async function injectThrowingFunction(page: Page) {
await page.evaluate(() => {
const $dd_probes = (globalThis as any).$dd_probes as (id: string) => unknown[] | undefined
const $dd_entry = (globalThis as any).$dd_entry as (probes: unknown[], self: unknown, args: object) => void
const $dd_throw = (globalThis as any).$dd_throw as (
probes: unknown[],
error: Error,
self: unknown,
args: object
) => void

;(window as any).throwingFunction = function throwingFunction(msg: string) {
const probes = $dd_probes('TestModule;throwingFunction')
if (probes) {
$dd_entry(probes, this, { msg })
}
try {
throw new Error(msg)
} catch (e) {
if (probes) {
$dd_throw(probes, e as Error, this, { msg })
}
throw e
}
}
})
}

test.describe('debugger', () => {
createTest('send debugger snapshot when instrumented function is called')
.withDebugger()
.run(async ({ intakeRegistry, flushEvents, page, browserName, servers }) => {
test.skip(browserName !== 'chromium', 'Debugger tests require Chromium')

const probe = makeProbe()
setDebuggerProbes(servers, [probe])

await page.reload()
await injectInstrumentedFunction(page)

await page.evaluate(() => {
;(window as any).testFunction('hello', ' world')
})

await flushEvents()

expect(intakeRegistry.debuggerEvents.length).toBeGreaterThanOrEqual(1)

const event = intakeRegistry.debuggerEvents[0]
expect(event.message).toBe('Probe hit')
expect(event.service).toBe('browser-sdk-e2e-test')
expect(event.hostname).toBeDefined()

const snapshot = (event.debugger as any).snapshot
expect(snapshot.probe.id).toBe('test-probe-1')
expect(snapshot.language).toBe('javascript')
expect(snapshot.duration).toBeGreaterThan(0)
})

createTest('capture function arguments and return value')
.withDebugger()
.run(async ({ intakeRegistry, flushEvents, page, browserName, servers }) => {
test.skip(browserName !== 'chromium', 'Debugger tests require Chromium')

const probe = makeProbe({ captureSnapshot: true })
setDebuggerProbes(servers, [probe])

await page.reload()
await injectInstrumentedFunction(page)

await page.evaluate(() => {
;(window as any).testFunction('foo', 'bar')
})

await flushEvents()

expect(intakeRegistry.debuggerEvents.length).toBeGreaterThanOrEqual(1)

const snapshot = (intakeRegistry.debuggerEvents[0].debugger as any).snapshot
expect(snapshot.captures).toBeDefined()
expect(snapshot.captures.return).toBeDefined()

const returnCapture = snapshot.captures.return
expect(returnCapture.locals['@return']).toBeDefined()
expect(returnCapture.locals['@return'].value).toBe('foobar')
})

createTest('capture exception in snapshot on throw')
.withDebugger()
.run(async ({ intakeRegistry, flushEvents, page, browserName, servers }) => {
test.skip(browserName !== 'chromium', 'Debugger tests require Chromium')

const probe = makeProbe({
typeName: 'TestModule',
methodName: 'throwingFunction',
})
setDebuggerProbes(servers, [probe])

await page.reload()
await injectThrowingFunction(page)

await page.evaluate(() => {
try {
;(window as any).throwingFunction('test error')
} catch {
// expected
}
})

await flushEvents()

expect(intakeRegistry.debuggerEvents.length).toBeGreaterThanOrEqual(1)

const snapshot = (intakeRegistry.debuggerEvents[0].debugger as any).snapshot
expect(snapshot.captures.return.throwable).toBeDefined()
expect(snapshot.captures.return.throwable.message).toBe('test error')
})

createTest('evaluate probe message template with expression segments')
.withDebugger()
.run(async ({ intakeRegistry, flushEvents, page, browserName, servers }) => {
test.skip(browserName !== 'chromium', 'Debugger tests require Chromium')

const probe = makeProbe({
template: '',
segments: [
{ str: 'Result is: ' },
{ dsl: 'a', json: { ref: 'a' } },
{ str: ' and ' },
{ dsl: 'b', json: { ref: 'b' } },
],
})
setDebuggerProbes(servers, [probe])

await page.reload()
await injectInstrumentedFunction(page)

await page.evaluate(() => {
;(window as any).testFunction('X', 'Y')
})

await flushEvents()

expect(intakeRegistry.debuggerEvents.length).toBeGreaterThanOrEqual(1)
expect(intakeRegistry.debuggerEvents[0].message).toBe('Result is: X and Y')
})

createTest('do not send snapshot when probe condition is not met')
.withDebugger()
.run(async ({ intakeRegistry, flushEvents, page, browserName, servers }) => {
test.skip(browserName !== 'chromium', 'Debugger tests require Chromium')

const probe = makeProbe({
evaluateAt: 'EXIT',
condition: {
dsl: '$dd_return == "match"',
json: { eq: [{ ref: '$dd_return' }, 'match'] },
},
})
setDebuggerProbes(servers, [probe])

await page.reload()
await injectInstrumentedFunction(page)

await page.evaluate(() => {
;(window as any).testFunction('no', 'match')
})

await flushEvents()

expect(intakeRegistry.debuggerEvents).toHaveLength(0)
})

createTest('send snapshot when probe condition is met')
.withDebugger()
.run(async ({ intakeRegistry, flushEvents, page, browserName, servers }) => {
test.skip(browserName !== 'chromium', 'Debugger tests require Chromium')

const probe = makeProbe({
evaluateAt: 'EXIT',
condition: {
dsl: '$dd_return == "foobar"',
json: { eq: [{ ref: '$dd_return' }, 'foobar'] },
},
})
setDebuggerProbes(servers, [probe])

await page.reload()
await injectInstrumentedFunction(page)

await page.evaluate(() => {
;(window as any).testFunction('foo', 'bar')
})

await flushEvents()

expect(intakeRegistry.debuggerEvents.length).toBeGreaterThanOrEqual(1)
expect(intakeRegistry.debuggerEvents[0].message).toBe('Probe hit')
})

createTest('omit trace correlation data when no active span is available')
.withRum()
.withDebugger()
.run(async ({ intakeRegistry, flushEvents, page, browserName, servers }) => {
test.skip(browserName !== 'chromium', 'Debugger tests require Chromium')

const probe = makeProbe()
setDebuggerProbes(servers, [probe])

await page.reload()
await injectInstrumentedFunction(page)

await page.evaluate(() => {
;(window as any).testFunction('hello', ' world')
})

await flushEvents()

expect(intakeRegistry.debuggerEvents.length).toBeGreaterThanOrEqual(1)

const event = intakeRegistry.debuggerEvents[0]
expect(event.dd).toBeUndefined()
})
})
Loading