Introduction
Synthetic monitoring is a proactive approach to monitoring web applications by simulating user interactions and measuring performance from the end-user perspective. This article demonstrates how to build a robust synthetic monitoring solution using Azure Functions and Playwright that can automatically test your web applications, report results to Azure Application Insights, and store test artifacts in Azure Blob Storage.
Why Synthetic Monitoring Matters
Traditional monitoring only tells you when something is already broken. Synthetic monitoring helps you:
- Detect issues before users do by continuously running automated tests
- Monitor critical user journeys like login, checkout, or key workflows
- Validate deployments by ensuring core functionality works after releases
Architecture Overview
Our solution combines several Azure services to create a comprehensive monitoring system:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Azure Timer │ │ Azure Function │ │ Playwright │
│ Trigger │───▶│ (Runtime) │───▶│ Test Runner │
│ (Scheduled) │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐
│ Test Results │
│ Processing │
└─────────────────┘
│
┌─────────┴─────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Application │ │ Azure Blob │
│ Insights │ │ Storage │
│ (Telemetry) │ │ (Artifacts) │
└─────────────────┘ └─────────────────┘
Prerequisites
Before starting, ensure you have:
- Node.js 18+ installed
- Azure CLI installed and configured
- Azure Functions Core Tools v4
- An Azure subscription with:
- Application Insights instance
- Storage Account
- Function App (Premium or Dedicated plan recommended)
Project Setup
1. Initialize the Project
# Create project directory
mkdir synthetic-monitoring
cd synthetic-monitoring
# Initialize Node.js project
npm init -y
# Install core dependencies
npm install @azure/functions @playwright/test playwright
npm install @azure/storage-blob applicationinsights archiver dotenv
# Install development dependencies
npm install -D typescript @types/node @types/archiver
2. Project Structure
Create the following directory structure:
synthetic-monitoring/
├── src/
│ ├── functions/
│ │ └── synthetic-monitor.ts # Timer-triggered function
│ ├── support/
│ │ ├── azure/
│ │ │ ├── app-insights-client.ts
│ │ │ ├── app-insights.ts
│ │ │ ├── blob-storage-client.ts
│ │ │ └── blob-storage.ts
│ │ ├── logger/
│ │ │ └── logger.ts
│ │ ├── pages/ # Page Object Models
│ │ ├── reporter/
│ │ │ ├── appinsights-reporter.ts
│ │ │ └── utils.ts
│ │ └── services/ # Authentication services
│ ├── tests/
│ │ └── marketplace-integration.spec.ts
│ └── index.ts # Main entry point
├── host.json # Azure Functions configuration
├── local.settings.json # Local environment variables
├── package.json
├── playwright.config.ts # Playwright configuration
└── tsconfig.json # TypeScript configuration
Core Implementation
1. Azure Function Timer Trigger
Create the main timer-triggered function (src/functions/synthetic-monitor.ts
):
import { app, InvocationContext, Timer } from '@azure/functions';
import { runPlaywrightTests } from '../index';
app.timer('syntheticMonitor', {
schedule: process.env.SYNTHETIC_MONITOR_SCHEDULE || '|| '0 0 * * * *',
handler: async (myTimer: Timer, context: InvocationContext) => {
try {
context.log("🚀 Executing synthetic monitoring tests...");
await runPlaywrightTests(context);
context.log("✅ Synthetic monitoring tests completed successfully!");
} catch (error) {
context.log("❌ Error in synthetic monitoring tests:", error);
throw error; // Ensure the function fails for monitoring purposes
} finally {
context.log("🔄 Synthetic monitoring cycle completed.");
}
}
});
2. Main Test Runner
Implement the core test execution logic (src/index.ts
):
import { spawn } from 'child_process';
import { setLoggerContext } from './support/logger/logger';
import { InvocationContext } from '@azure/functions';
import path from 'path';
// Import the function to register it
import './functions/synthetic-monitor';
export async function runPlaywrightTests(context: InvocationContext): Promise<void> {
setLoggerContext(context);
return new Promise((resolve, reject) => {
context.log('▶️ Running Playwright tests via CLI...');
// Use full path to playwright binary
const playwrightPath = path.join(process.cwd(), 'node_modules', '.bin', 'playwright.cmd');
const command = \`"\${playwrightPath}" test\`;
context.log(\`📍 Using command: \${command}\`);
const child = spawn(command, {
shell: true,
env: {
...process.env,
// Add any environment variables your tests need
NODE_ENV: 'production'
}
});
child.stdout.on('data', (data) => {
context.log(data.toString());
});
child.stderr.on('data', (data) => {
context.log(data.toString());
});
child.on('error', (error) => {
context.log(\`❌ Playwright tests failed: \${error.message}\`);
reject(error);
});
child.on('close', (code) => {
if (code === 0) {
context.log('✅ Playwright tests completed.');
resolve();
} else {
context.log(\`❌ Playwright tests exited with code \${code}\`);
reject(new Error(\`Playwright tests failed with code \${code}\`));
}
});
});
}
3. Playwright Configuration
Configure Playwright for Azure Functions (playwright.config.ts
):
import { defineConfig, devices } from '@playwright/test';
import fs from 'fs';
import os from 'os';
import path from 'path';
const tmpDir = os.tmpdir();
const outputPath = process.env.PLAYWRIGHT_OUTPUT_DIR || path.join(tmpDir, 'playwright-artifacts');
const htmlReportPath = process.env.PLAYWRIGHT_HTML_REPORT || path.join(tmpDir, 'playwright-html-report');
// Ensure directories exist
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true });
}
if (!fs.existsSync(htmlReportPath)) {
fs.mkdirSync(htmlReportPath, { recursive: true });
}
export default defineConfig({
testDir: './src/tests',
timeout: 180 * 1000, // 3 minutes per test
retries: 1, // Retry failed tests once
fullyParallel: true,
workers: 3, // Adjust based on your Azure Function plan
outputDir: outputPath,
use: {
headless: true, // Always run headless in Azure Functions
viewport: { width: 1280, height: 720 },
baseURL: process.env.baseUrl,
screenshot: 'only-on-failure',
trace: 'retain-on-failure',
actionTimeout: 30000,
},
reporter: [
['html', { outputFolder: htmlReportPath, open: 'never' }],
['junit', { outputFile: path.join(outputPath, 'junit-report.xml') }],
['list'],
['./src/support/reporter/appinsights-reporter.ts'] // Custom reporter
],
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome']
},
}
],
});
4. Azure Functions Configuration
Configure the function app (host.json
):
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"excludedTypes": "Request"
}
},
"logLevel": {
"default": "Information"
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
}
}
Sample Test Implementation
Create a comprehensive test (src/tests/do-somethinng.spec.ts
):
import { test, expect, BrowserContext, Page } from '@playwright/test';
const aadUsername: string = process.env['username'] || '';
const aadPassword: string = process.env['password'] || '';
const baseURL: string = process.env['baseUrl'] || '';
let context: BrowserContext;
let page: Page;
test.beforeEach(async ({ browser: bro }) => {
context = await bro.newContext();
page = await context.newPage();
// Init all pages here
await page.goto(baseURL);
});
test('User should be able to do somethinng', { tag: ['@some-tag', ] }, async () => {
// All the test implemention here
//Arrange
//Act
//Assert
});
test.afterEach(async () => {
await page.close();
await context.close();
});
Deployment and Configuration
1. Environment Variables
Set up your environment variables in Azure Function App settings:
{
"AzureWebJobsStorage": "DefaultEndpointsProtocol=https;AccountName=<storage>;AccountKey=<key>",
"FUNCTIONS_WORKER_RUNTIME": "node",
"FUNCTIONS_EXTENSION_VERSION": "~4",
//ScheduleTime
"SYNTHETIC_MONITOR_SCHEDULE": "0 */10 * * * *",
// Test Configuration
"username": "<test-user-email>",
"password": "<test-user-password>",
"baseUrl": "<base-url>",
// Azure Services
"APPLICATIONINSIGHTS_CONNECTION_STRING": "<app-insights-connection>",
"APPINSIGHTS_INSTRUMENTATIONKEY": "<app-insights-key>",
"AZURE_BLOB_CONNECTION_STRING": "<blob-storage-connection>",
"AZURE_BLOB_CONTAINER_NAME": "<container-name>",
}
2. Package.json Scripts
Configure build and deployment scripts:
{
"scripts": {
"build": "tsc",
"start": "func start --typescript",
"test:local": "npx playwright test",
"install:browsers": "npx playwright install"
}
}
Conclusion
Implementing synthetic monitoring with Azure Functions and Playwright provides a powerful, scalable solution for proactive application monitoring. This approach offers:
- Automated testing on a schedule
- Comprehensive reporting via Application Insights
- Artifact storage for debugging failures
- Cost-effective scaling with Azure Functions
- Real user simulation with Playwright
The key to success is choosing the right Azure Function plan, optimizing Playwright configuration for cloud execution, and implementing robust error handling and monitoring.
With this foundation, you can expand your monitoring to cover critical user journeys, API endpoints, performance metrics, and multi-region testing to ensure your applications deliver optimal user experiences around the clock.
Additional Resources
- Azure Functions Documentation
- Playwright Documentation
- Application Insights Documentation
- Azure Blob Storage Documentation
- Github Project - synthetic-monitoring-tests
This implementation is based on a real-world production synthetic monitoring system that monitors applications and critical user workflows.
Top comments (0)