supaguardsupaguardDocs
Use cases

How to Monitor a React Application with Synthetic Monitoring

Set up production monitoring for your React app. Write Playwright tests for SPAs with client-side routing, lazy loading, state management, and loading states.

React single-page applications (SPAs) present unique monitoring challenges. Client-side routing, lazy-loaded components, and asynchronous state management mean traditional uptime checks miss most of the failure modes that affect real users. Synthetic monitoring with Playwright simulates actual user behavior to catch these issues.

Why React Apps Need Synthetic Monitoring

React SPAs differ fundamentally from server-rendered pages:

ChallengeWhy It Matters
Client-side routingURL changes don't trigger page loads — uptime monitors see nothing
Lazy loadingComponents load on demand — failures only appear when users navigate
JavaScript errorsA thrown error can break the entire app (white screen of death)
API dependenciesUI depends on API responses — backend failures show as frontend bugs
Loading statesSpinners and skeletons can persist forever if data fetch fails

Monitoring Core User Flows

React Router (or Tanstack Router, etc.) handles routing client-side. Verify navigation works:

import { test, expect } from "@playwright/test";

test("navigation between pages works", async ({ page }) => {
  await page.goto("https://your-react-app.com");

  // Navigate via client-side routing
  await page.getByRole("link", { name: "Products" }).click();
  await expect(page).toHaveURL(/.*products/);
  await expect(page.getByRole("heading", { name: "Products" })).toBeVisible();

  // Navigate to another page
  await page.getByRole("link", { name: "About" }).click();
  await expect(page).toHaveURL(/.*about/);
  await expect(page.getByRole("heading", { name: "About" })).toBeVisible();
});

Authentication Flow

test("user can log in and access dashboard", async ({ page }) => {
  await page.goto("https://your-react-app.com/login");

  await page.getByLabel("Email").fill(process.env.TEST_USER_EMAIL!);
  await page.getByLabel("Password").fill(process.env.TEST_USER_PASSWORD!);
  await page.getByRole("button", { name: "Log In" }).click();

  // Wait for client-side redirect and auth state to settle
  await expect(page).toHaveURL(/.*dashboard/);
  await expect(page.getByTestId("user-menu")).toBeVisible();
});

Form Submission

test("form submits and shows confirmation", async ({ page }) => {
  await page.goto("https://your-react-app.com/contact");

  await page.getByLabel("Name").fill("Test User");
  await page.getByLabel("Email").fill("test@example.com");
  await page.getByLabel("Message").fill("Monitoring test message");
  await page.getByRole("button", { name: "Send" }).click();

  // Wait for success state (not loading)
  await expect(page.getByText("Message sent successfully")).toBeVisible();
});

Handling React-Specific Patterns

Loading States and Suspense

React apps use loading states, skeletons, and Suspense boundaries. Wait for them to resolve:

test("dashboard data loads completely", async ({ page }) => {
  await page.goto("https://your-react-app.com/dashboard");

  // Wait for loading state to finish
  await expect(page.getByTestId("loading-skeleton")).not.toBeVisible({
    timeout: 15000,
  });

  // Verify actual data rendered
  await expect(page.getByTestId("revenue-card")).toBeVisible();
  await expect(page.getByTestId("revenue-card")).not.toHaveText("$0");
});

Error Boundaries

React error boundaries catch rendering errors. Monitor that they don't appear:

test("no error boundaries triggered", async ({ page }) => {
  await page.goto("https://your-react-app.com/dashboard");

  // Verify error boundary is NOT showing
  await expect(page.getByText("Something went wrong")).not.toBeVisible();
  await expect(page.getByText("Error")).not.toBeVisible();

  // Verify actual content is showing
  await expect(page.getByTestId("main-content")).toBeVisible();
});

React modals often use portals. Playwright handles them naturally:

test("settings modal opens and saves", async ({ page }) => {
  await page.goto("https://your-react-app.com/dashboard");

  await page.getByRole("button", { name: "Settings" }).click();

  // Modal content (rendered via portal)
  await expect(page.getByRole("dialog")).toBeVisible();
  await page.getByLabel("Display Name").fill("Updated Name");
  await page.getByRole("button", { name: "Save" }).click();

  // Modal should close and changes should persist
  await expect(page.getByRole("dialog")).not.toBeVisible();
  await expect(page.getByText("Updated Name")).toBeVisible();
});

Blocking Third-Party Scripts

React apps often include analytics, chat widgets, and other third-party scripts that can interfere with tests:

test("core flow without third-party interference", async ({ page }) => {
  // Block scripts that might interfere
  await page.route("**/analytics.js", (route) => route.abort());
  await page.route("**/chat-widget/**", (route) => route.abort());
  await page.route("**/hotjar.com/**", (route) => route.abort());

  await page.goto("https://your-react-app.com");
  // ... rest of test
});

Monitoring API Dependencies

React apps depend heavily on APIs. Monitor them alongside the UI:

test("API and UI are in sync", async ({ page }) => {
  await page.goto("https://your-react-app.com/products");

  // Wait for API response and verify UI renders it
  const responsePromise = page.waitForResponse("**/api/products");
  await page.getByRole("button", { name: "Load Products" }).click();

  const response = await responsePromise;
  expect(response.status()).toBe(200);

  // Verify UI updated with API data
  await expect(page.getByRole("listitem")).toHaveCount(10);
});
FlowFrequencyPriority
Login flowEvery 5 minCritical
Core feature (main page)Every 5 minCritical
Navigation between routesEvery 10 minHigh
Form submissionEvery 15 minMedium
Settings and profileEvery 30 minLow

Next Steps

On this page