E2E tests use Playwright to test the SDK in real browser environments.
# First-time setup: builds packages and apps; installs or updates Playwright browsers
# for the workspace Playwright version.
yarn test:e2e:setup
# Optional: install Chromium, Firefox, and WebKit for Playwright 1.40.1. Required
# locally if you run the firefox-pinned / webkit-pinned projects (see docs/TESTING.md).
yarn test:e2e:setup:pinned
# Run E2E tests locally
yarn test:e2e
# Run tests matching specific pattern
yarn test:e2e -g "unhandled rejections"Important: yarn test:e2e does not build the SDK automatically. Run yarn build:apps to rebuild the SDK and test apps if you have made changes to the source code.
To rebuild only specific apps (faster during development):
# List available apps
yarn build:apps --help
# Build a single app
yarn build:apps --app vanilla
# Build multiple apps
yarn build:apps --app vanilla --app react-heavy-spaLocated in test/apps/:
test/apps/
├── vanilla/ # Plain JavaScript app for basic E2E tests
├── react-router-app/ # React Router v7 integration tests
└── base-extension/ # Browser extension testing
Running yarn build:apps generates additional test apps from the source apps:
test/apps/ (generated)
├── react-router-v6-app/ # Generated from react-router-v7-app with RR v6
├── cdn-extension/ # Generated from base-extension (CDN variant)
└── appendChild-extension/ # Generated from base-extension (appendChild variant)
Note: Generated apps are not checked into Git.
E2E test apps are served by the dev server and loaded in Playwright tests.
All E2E test files follow this pattern:
- Located in
test/e2e/scenario/directory (or subdirectories) - Named with
.scenario.tssuffix - Examples:
test/e2e/scenario/logs.scenario.tstest/e2e/scenario/rum/errors.scenario.tstest/e2e/scenario/recorder/shadowDom.scenario.ts
playwright.config.ts- Project list and webServer wiring (current + pinned browsers)
E2E tests use a custom createTest() builder pattern instead of raw Playwright:
import { test, expect } from '@playwright/test'
import { createTest } from '../lib/framework'
test.describe('feature name', () => {
createTest('should track user interactions')
.withRum() // Initialize RUM SDK
.run(async ({ intakeRegistry, flushEvents, page }) => {
// Interact with page
await page.click('button')
// Wait for SDK to flush events
await flushEvents()
// Assert on captured events
expect(intakeRegistry.rumEvents).toHaveLength(1)
expect(intakeRegistry.rumEvents[0].type).toBe('action')
})
})SDK Configuration:
.withRum(config?)- Initialize RUM SDK with optional config.withLogs(config?)- Initialize Logs SDK with optional config.withRumSlim()- Also test with rum-slim variant.withWorker(worker)- Test with Service Worker (pass acreateWorker()builder)
Custom Initialization:
.withRumInit(fn)- Custom RUM initialization logic.withLogsInit(fn)- Custom Logs initialization logic
Page Customization:
.withHead(html)- Add content to<head>.withBody(html)- Add content to<body>.withReactApp(name)- Use a React test app.withExtension(ext)- Test with browser extension
Configuration:
.withRemoteConfiguration(config)- Simulate remote config.withEventBridge()- Enable event bridge (for mobile/React Native).withBasePath(path)- Change base URL path
By default, each test runs with multiple SDK integration setups:
- CDN - SDK loaded via
<script>tag (CDN bundle) - npm - SDK imported as ES module (npm package)
This ensures the SDK works correctly in both integration methods. Tests appear as multiple test cases in the Playwright UI.
To use only one setup (not recommended):
createTest('should work with custom setup')
.withRum()
.withSetup(cdnSetup) // Only test CDN setup
.run(async ({ intakeRegistry, flushEvents }) => {
// Test code
})The .run() callback receives a TestContext object:
{
// Event capture
intakeRegistry: IntakeRegistry // Captured SDK events
flushEvents: () => Promise<void> // Wait for SDK to flush
// Playwright objects
page: Page
browserContext: BrowserContext
browserName: 'chromium' | 'firefox' | 'webkit' | 'msedge'
// Test servers
servers: Servers
baseUrl: string
// Browser logs
withBrowserLogs: (cb) => void // Access browser console logs
flushBrowserLogs: () => void // Clear browser logs
// Utilities
deleteAllCookies: () => Promise<void>
sendXhr: (url, headers?) => Promise<string>
evaluateInWorker: (fn) => Promise<void> // Execute code inside the service worker
getExtensionId: () => Promise<string>
}createTest('should send logs')
.withLogs()
.run(async ({ intakeRegistry, flushEvents, page }) => {
await page.evaluate(() => {
window.DD_LOGS!.logger.log('hello')
})
await flushEvents()
// Check captured events
expect(intakeRegistry.logsEvents).toHaveLength(1)
expect(intakeRegistry.logsEvents[0].message).toBe('hello')
})createTest('should display logs in console when using console handler')
.withLogs()
.run(async ({ intakeRegistry, flushEvents, page, withBrowserLogs }) => {
await page.evaluate(() => {
window.DD_LOGS!.logger.setHandler('console')
window.DD_LOGS!.logger.warn('hello')
})
await flushEvents()
// Check console logs
withBrowserLogs((logs) => {
expect(logs).toHaveLength(1)
expect(logs[0].level).toBe('warning')
expect(logs[0].message).toEqual(expect.stringContaining('hello'))
})
})Note: The test teardown automatically validates that there are no console errors. Use withBrowserLogs() to check for specific warning messages or console output.
createTest('should work in chromium only')
.withRum()
.run(async ({ browserName }) => {
test.skip(browserName !== 'chromium', 'Chromium-only feature')
// Test chromium-specific behavior
})Use createWorker() to configure a service worker with SDK products:
import { createTest, createWorker } from '../lib/framework'
createTest('worker with logs')
.withWorker(createWorker().withLogs({ forwardConsoleLogs: 'all' }))
.run(async ({ evaluateInWorker, flushEvents, intakeRegistry }) => {
await evaluateInWorker(() => {
DD_LOGS!.logger.log('hello from worker')
})
await flushEvents()
expect(intakeRegistry.logsEvents[0].message).toBe('hello from worker')
})createWorker({ importScripts: true })- UseimportScriptsinstead of ES modules.withLogs(config?)- Initialize Logs SDK in the worker.withRum(config?)- Initialize RUM SDK in the workerevaluateInWorker(fn)- Execute a function inside the service worker scope- ESM workers only work in Chromium; use
test.skipfor other browsers
The intakeRegistry object captures all events sent by the SDK:
intakeRegistry.rumEvents // All RUM events
intakeRegistry.rumViewEvents // View events
intakeRegistry.rumActionEvents // Action events
intakeRegistry.rumErrorEvents // Error events
intakeRegistry.rumResourceEvents // Resource events
intakeRegistry.rumLongTaskEvents // Long task events
intakeRegistry.logsEvents // All log events
intakeRegistry.telemetryEvents // Telemetry events
intakeRegistry.telemetryErrorEvents // Telemetry errors
intakeRegistry.rumRequests // Raw HTTP requests (RUM)
intakeRegistry.logsRequests // Raw HTTP requests (Logs)Each test automatically:
- Starts with a fresh page
- Clears cookies between tests
- Flushes events after test completion
- Validates no telemetry errors occurred
- Validates no console errors occurred
- Validates RUM event format
// ✅ Good - shows actual array content on failure
expect([1, 2, 3]).toHaveLength(3)
// ❌ Less helpful - only shows length mismatch
expect([1, 2, 3].length).toBe(3)❌ Don't use page.waitForRequest() - use intakeRegistry instead
❌ Don't forget await flushEvents() before checking intakeRegistry
❌ Don't test SDK internals - test observable behavior (captured events)
❌ Don't use .withRumInit() AND .withRum() config together (init overrides config)
❌ Don't create tests without the .scenario.ts suffix
✅ Do use createTest() builder pattern
✅ Do use intakeRegistry to check captured events
✅ Do test real user workflows
✅ Do validate event structure and content
✅ Do check browser logs with withBrowserLogs()
✅ Do use test.skip() for browser-specific tests