This framework provides a Playwright fixture for testing observability by interacting with the Dynatrace Real User Monitoring (RUM) API.
Follow these steps to configure and use the framework:
Read RUM manual insertion tagsAPI V2To use the framework, you need to provide the following details:
Populate your playwright config file with these fields or call test.use to provide them to a test suite, like so:
import { test } from "@dynatrace/rum-javascript-sdk/test";
test.describe("my suite", () => {
test.use({
dynatraceConfig: {
appId: process.env.DT_APP_ID!,
token: process.env.DT_TOKEN!,
endpointUrl: process.env.DT_ENDPOINT_URL!
}
});
});
Use the provided expect functions to assert on events the RUM JavaScript should have sent:
test("expect specific event", async ({ page, dynatraceTesting }) => {
await page.goto("http://127.0.0.1:3000/ui");
await page.getByText("Explore data").first().click();
await dynatraceTesting.expectToHaveSentEvent(expect.objectContaining({
"event_properties.component_rendered": "Data"
}));
});
⚠️ The order of fixture destructuring matters!
The dynatraceTesting fixture must be destructured before any custom fixtures that navigate the page. This ensures the RUM JavaScript is injected before navigation occurs.
test("my test", async ({ dynatraceTesting, myCustomFixture, page }) => {
// dynatraceTesting is initialized first
// RUM script is injected via page.addInitScript
// Then myCustomFixture can safely navigate
await page.goto("https://example.com");
});
test("my test", async ({ myCustomFixture, dynatraceTesting, page }) => {
// If myCustomFixture navigates the page, it happens BEFORE
// dynatraceTesting is initialized, so RUM script is not injected
// This will cause the test to fail with "no events received"
});
Playwright initializes fixtures in the order they appear in the destructuring pattern. The dynatraceTesting fixture uses page.addInitScript() to inject the RUM JavaScript, which only affects subsequent page navigations.
If you have custom fixtures that call page.goto(), always destructure dynatraceTesting first:
// Define custom fixture
const test = base.extend<{ myApp: MyApp }>({
myApp: async ({ page }, use) => {
await page.goto("https://myapp.com"); // Navigation happens here
await use(new MyApp(page));
}
});
// Use fixtures in correct order
test("test", async ({ dynatraceTesting, myApp }) => {
// ✅ Correct: dynatraceTesting initialized before myApp
});
The test fixture works by intercepting network requests to the RUM ingestion endpoint and capturing the events sent by the RUM JavaScript. Note that this has an impact on your capturing environment, since these beacons report real data.
If you see an error like:
[Dynatrace Testing] Missing required configuration fields: endpointUrl, appId, token
This means the required configuration was not provided. Make sure to configure the fixture using test.use():
test.use({
dynatraceConfig: {
endpointUrl: process.env.DT_ENDPOINT_URL!,
appId: process.env.DT_APP_ID!,
token: process.env.DT_TOKEN!
}
});
If your tests fail with "Dynatrace didn't send any events":
dynatraceTesting is destructured before any fixtures that navigate the pageWhen dumpEventsOnFail is enabled, the framework will output detailed information about received events when assertions fail:
[Dynatrace Testing] Event Dump (expectToHaveSentEvent - no match)
Total events received: 3
Total beacons received: 2
Received events:
Event 1: { "event_properties.custom_val": "load", ... }
Event 2: { "event_properties.custom_val": "user-action", ... }
Event 3: { "event_properties.custom_val": "custom", ... }
This helps identify why expected events weren't matched.
The DynatraceTesting interface provides utility methods for validating observability-related events and beacon
requests during tests.
waitForBeacons(options?: { minCount?: number; timeout?: number }): Promise<BeaconRequest[]>Waits for a specified number of beacon requests or until a timeout occurs.
options (Optional):
minCount: The minimum number of beacon requests to wait for.timeout: The maximum time to wait for beacon requests, in milliseconds. (Default: 30,000)A promise that resolves with an array of beacon requests.
expectToHaveSentEvent(event: Record<string, unknown>, options?: { timeout?: number }): Promise<void>Verifies that a specific event has been sent, optionally within a timeout period.
event: The event to check, represented as a key-value pair object. This uses Playwright's expect(event).toMatchObject.options (Optional):
timeout: The maximum time to wait for the event to be sent, in milliseconds. (Default: 30,000)A promise that resolves when the event is confirmed to have been sent.
expectToHaveSentEventTimes(event: Record<string, unknown>, times: number, options?: { timeout?: number }): Promise<void>Verifies that a specific event has been sent a specified number of times, optionally within a timeout period.
event: The event to check, represented as a key-value pair object. This uses Playwright's expect(event).toMatchObject.times: The exact number of times the event is expected to have been sent.options (Optional):
timeout: The maximum time to wait for the event to be sent, in milliseconds. (Default: 30,000)A promise that resolves when the event is confirmed to have been sent the specified number of times.
clearEvents(): voidClears all events and beacons, allowing subsequent expectations to ignore events that have been sent in the past.
sendFooEvent();
await dynatraceTesting.expectToHaveSentEventTimes({ foo: "bar" }, 1);
dynatraceTesting.clearEvents();
await dynatraceTesting.expectToHaveSentEventTimes({ foo: "bar" }, 1);
toMatchEventSnapshot(options?: SnapshotOptions): Promise<void>Compares captured events against a stored snapshot file. On first run, creates the snapshot. On subsequent runs, compares and fails if different. Volatile fields (timestamps, IDs, etc.) are processed by default to prevent flaky tests.
options (Optional):
ignoredFields: Array of field names or patterns whose values are replaced with [IGNORED] placeholder before comparison:
"start_time"): Value replaced with [IGNORED]"dt.rum.*"): All matching fields have values replaced with [IGNORED]Default: DEFAULT_IGNORED_FIELDS - includes timestamps, session IDs, trace IDs, etc.
removedFields: Array of field names or patterns to remove entirely before comparison:
"performance.time_origin"): Field is removed"inp.*"): All matching fields are removedDefault: DEFAULT_REMOVED_FIELDS - includes web vitals, performance metrics, viewport dimensions.
ignoreEvents: Array of event patterns to filter out completely. Events matching any pattern are excluded from the snapshot. Default: DEFAULT_IGNORED_EVENTS (filters long tasks and self-monitoring events).
name: Custom snapshot name. If not provided, defaults to "events".
A promise that resolves when the snapshot comparison succeeds.
test("captures user action events", async ({ page, dynatraceTesting }) => {
await page.goto("http://127.0.0.1:3000/ui");
await page.getByText("Submit").click();
// Wait for events to be captured
await dynatraceTesting.waitForBeacons({ minCount: 1 });
// Compare against snapshot
await dynatraceTesting.toMatchEventSnapshot();
});
import { DEFAULT_IGNORED_FIELDS, DEFAULT_REMOVED_FIELDS } from "@dynatrace/rum-javascript-sdk/test";
test("captures events with custom property handling", async ({ page, dynatraceTesting }) => {
await page.goto("http://127.0.0.1:3000/ui");
await page.getByText("Submit").click();
await dynatraceTesting.waitForBeacons({ minCount: 1 });
// Extend defaults with additional patterns
await dynatraceTesting.toMatchEventSnapshot({
ignoredFields: [
...DEFAULT_IGNORED_FIELDS,
"event_properties.request_id" // Value replaced with [IGNORED]
],
removedFields: [
...DEFAULT_REMOVED_FIELDS,
"user_action.resources.*" // All matching fields removed entirely
]
});
});
import { DEFAULT_IGNORED_EVENTS } from "@dynatrace/rum-javascript-sdk/test";
test("captures events excluding specific patterns", async ({ page, dynatraceTesting }) => {
await page.goto("http://127.0.0.1:3000/ui");
await page.getByText("Submit").click();
await dynatraceTesting.waitForBeacons({ minCount: 1 });
// Filter out specific event types
await dynatraceTesting.toMatchEventSnapshot({
ignoreEvents: [
...DEFAULT_IGNORED_EVENTS,
{ "characteristics.has_navigation": true }, // Exclude navigation events
{ "url.full": "http://example.com/favicon.ico" } // Exclude specific URL
]
});
});
test("captures multiple event sequences", async ({ page, dynatraceTesting }) => {
await page.goto("http://127.0.0.1:3000/ui");
// First interaction
await page.getByText("Login").click();
await dynatraceTesting.waitForBeacons({ minCount: 1 });
await dynatraceTesting.toMatchEventSnapshot({ name: "login-events" });
dynatraceTesting.clearEvents();
// Second interaction
await page.getByText("Submit").click();
await dynatraceTesting.waitForBeacons({ minCount: 1 });
await dynatraceTesting.toMatchEventSnapshot({ name: "submit-events" });
});
Event snapshots may differ between browsers due to variations in timing, event ordering, or browser-specific behavior. To handle this, include the browserName fixture in your snapshot name to create separate snapshots per browser:
test("captures events per browser", async ({ page, dynatraceTesting, browserName }) => {
await page.goto("http://127.0.0.1:3000/ui");
await page.getByText("Submit").click();
await dynatraceTesting.waitForBeacons({ minCount: 1 });
// Creates separate snapshots: basic-events-chromium, basic-events-firefox, basic-events-webkit
await dynatraceTesting.toMatchEventSnapshot({
name: `basic-events-${browserName}`
});
});
This creates separate snapshot files for each browser:
e2e/
├── my-test.spec.ts
└── my-test.spec.ts-snapshots/
├── basic-events-chromium.events.snap
├── basic-events-firefox.events.snap
└── basic-events-webkit.events.snap
Snapshots are stored alongside your test files in a directory named <test-file>-snapshots/. For example:
e2e/
├── my-test.spec.ts
└── my-test.spec.ts-snapshots/
└── events.events.snap
The snapshot files contain JSON-formatted event data with ignored fields replaced by [IGNORED] and removed fields omitted:
[
{
"duration": "[IGNORED]",
"name": "click on Submit",
"start_time": "[IGNORED]",
"type": "user_action"
}
]
To update snapshots when event structures change intentionally, run Playwright with the --update-snapshots flag:
npx playwright test --update-snapshots
The following fields have their values replaced with [IGNORED] by default:
| Category | Fields |
|---|---|
| Timing | start_time, duration, timestamp |
| Session/Instance IDs | dt.rum.sid, dt.rum.browser.sid, dt.rum.instance.id, user_session.id, user.anonymous_id, view.instance_id, page.instance_id, user_action.instance_id, etc. |
| Distributed Tracing | trace.id, span.id |
| Self-monitoring | dt.rum.sfm_events, dt.support.last_user_input |
The following fields are removed entirely by default (they vary in count or presence between runs):
| Category | Fields |
|---|---|
| Performance | performance.* |
| Web Vitals | fcp.*, fp.*, ttfb.*, lcp.*, fid.*, inp.*, cls.* |
| Long Tasks | long_task.* |
| Viewport/Screen | browser.window.*, device.screen.* |
The following event patterns are filtered out by default:
| Pattern | Description |
|---|---|
{ "characteristics.has_long_task": true } |
Long task events (timing varies) |
{ "characteristics.is_self_monitoring": true } |
Self-monitoring events |
When a snapshot comparison fails, the error message includes:
Common causes of snapshot failures:
--update-snapshotsignoredFields to replace values with [IGNORED]removedFields (use fieldname.* wildcard to remove all fields with that prefix)ignoreEvents to filter them out