Shared Modules: Reuse Code Across Monitoring Checks
Write reusable TypeScript modules and share them across all your monitoring checks. Build page objects, auth helpers, and utility functions once — use them everywhere.
Shared modules let you write reusable TypeScript code — page objects, auth helpers, utility functions — and import them into any monitoring check in your organization. Write once, use everywhere.
The Problem: Duplicate Code Across Checks
As your monitoring coverage grows, you'll notice patterns repeating across checks:
- Login flows shared by 10+ checks
- Common selectors for navigation, modals, or form components
- Helper functions for date formatting, data generation, or assertions
- API authentication logic used in every API check
Without shared modules, you end up copy-pasting the same code into every check script. When your UI changes, you update it in one check and forget the other nine.
How Shared Modules Work
Shared modules are TypeScript files stored per-organization in supaguard. When a check runs, all your organization's modules are bundled alongside the check script and made available via the @org/* import alias.
your-check.spec.ts → imports from @org/*
@org/pages/LoginPage.ts → shared login page object
@org/helpers/auth.ts → shared auth helpers
@org/helpers/dates.ts → shared date utilitiesThe flow:
- Write a module locally (e.g.,
LoginPage.ts) - Push it to your organization with the CLI
- Import it in any check using
@org/path/to/module - Run — supaguard bundles the module with your check automatically
Pushing Modules
Use the supaguard CLI to push a module to your organization:
supaguard modules push ./src/pages/LoginPage.ts --path "pages/LoginPage.ts"The --path flag sets the import path. If omitted, it defaults to the filename. After pushing, any check in your organization can import from @org/pages/LoginPage.
Upsert behavior: If a module with the same path already exists, it's updated automatically. No need to delete and re-create.
Managing modules
# List all modules in your organization
supaguard modules list
# Delete a module
supaguard modules delete <module-id>Importing Modules in Checks
Use the @org/* prefix to import shared modules in any check script:
import { test, expect } from "@playwright/test";
import { LoginPage } from "@org/pages/LoginPage";
import { generateTestEmail } from "@org/helpers/utils";
test("dashboard loads after login", async ({ page }) => {
const loginPage = new LoginPage(page);
const email = generateTestEmail();
await loginPage.goto();
await loginPage.loginWithEmail(email, process.env.TEST_PASSWORD);
await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
});Modules can also import from other modules:
// @org/pages/LoginPage.ts
import { fillAndSubmitForm } from "@org/helpers/forms";
export class LoginPage {
constructor(private page: any) {}
async goto() {
await this.page.goto("https://app.example.com/login");
}
async loginWithEmail(email: string, password: string) {
await fillAndSubmitForm(this.page, {
"input[name='email']": email,
"input[name='password']": password,
});
}
}Examples
Page object pattern
The most common use case — wrap page interactions in a reusable class:
// Push as: supaguard modules push LoginPage.ts --path "pages/LoginPage.ts"
import { expect, type Page } from "@playwright/test";
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto("https://app.example.com/login");
}
async loginWithEmail(email: string, password: string) {
await this.page.fill("[data-testid='email']", email);
await this.page.fill("[data-testid='password']", password);
await this.page.click("button[type='submit']");
await expect(this.page.getByText("Welcome")).toBeVisible();
}
async loginWithGoogle() {
await this.page.click("a:has-text('Sign in with Google')");
}
}Auth helper
Centralize authentication logic so every check starts from an authenticated state:
// Push as: supaguard modules push auth.ts --path "helpers/auth.ts"
import type { Page } from "@playwright/test";
export async function authenticateUser(
page: Page,
email: string,
password: string,
) {
await page.goto("https://app.example.com/login");
await page.fill("[data-testid='email']", email);
await page.fill("[data-testid='password']", password);
await page.click("button[type='submit']");
await page.waitForURL("**/dashboard");
}Test data generator
Generate unique data for each check run to avoid test pollution:
// Push as: supaguard modules push data.ts --path "helpers/data.ts"
import { faker } from "@faker-js/faker";
export function generateUser() {
return {
email: `monitor+${Date.now()}@example.com`,
name: faker.person.fullName(),
company: faker.company.name(),
};
}
export function generateOrderId() {
return `ORD-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
}Navigation helper
Reuse common navigation patterns across checks:
// Push as: supaguard modules push nav.ts --path "helpers/nav.ts"
import { expect, type Page } from "@playwright/test";
export async function navigateTo(page: Page, section: string) {
await page.click(`nav a:has-text("${section}")`);
await expect(page.getByRole("heading", { level: 1 })).toContainText(section);
}
export async function openSettingsTab(page: Page, tab: string) {
await page.goto("https://app.example.com/settings");
await page.click(`[role="tab"]:has-text("${tab}")`);
}Path Rules
Module paths must follow these rules:
- Must end in
.tsor.js - Can only contain alphanumeric characters, underscores, hyphens, and forward slashes
- Cannot contain
..(no path traversal) - Cannot start with
/(must be relative)
Valid paths:
LoginPage.tspages/LoginPage.tshelpers/auth/login.tspage-objects/HomePage.ts
Allowed Dependencies
Modules run in a sandboxed environment. You can import from:
| Package | Description |
|---|---|
@playwright/test | Playwright test utilities |
@org/* | Other shared modules in your organization |
@faker-js/faker | Test data generation |
dayjs, date-fns | Date manipulation |
lodash, lodash-es | Utility functions |
uuid | UUID generation |
zod | Schema validation |
Blocked: Node.js built-ins (fs, net, http, child_process, etc.), eval(), Function(), and dynamic import() statements are not allowed for security.
Best Practices
Organize by concern
@org/
pages/ # Page object models
LoginPage.ts
DashboardPage.ts
CheckoutPage.ts
helpers/ # Utility functions
auth.ts
data.ts
nav.ts
assertions.tsKeep modules focused
Each module should have a single responsibility. A LoginPage module handles login — it doesn't also manage the dashboard.
Use environment variables for secrets
Never hardcode credentials in modules. Use process.env and configure values in your organization's environment variables:
// Good
await loginPage.loginWithEmail(
process.env.TEST_EMAIL,
process.env.TEST_PASSWORD,
);
// Bad — credentials in source code
await loginPage.loginWithEmail("admin@example.com", "secret123");Version through path conventions
When making breaking changes to a module, consider a versioned path:
supaguard modules push LoginPage-v2.ts --path "pages/LoginPage-v2.ts"This lets you migrate checks one at a time instead of breaking all of them at once.
Related Resources
- CLI documentation — full module command reference
- Environment variables — manage secrets across checks
- Writing Playwright tests — script authoring patterns
- Claude Code guide — let AI generate and manage modules for you
Monitoring as Code vs. AI-Native Monitoring
Compare Checkly's Monitoring as Code (MaC) with supaguard's AI-Native approach. Learn which strategy is better for your team's reliability and developer experience.
Multi-Region Verification (Smart Retries)
Eliminate false alarms with supaguard's Multi-Region Verification. Learn how our 'instant teleportation' protocol confirms real outages before alerting your team.