DEV Community

Nathan Araújo
Nathan Araújo

Posted on

Exploring Azure Functions for Synthetic Monitoring with Playwright: A Complete Guide - Part 1

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)     │
                                     └─────────────────┘ └─────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.");
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

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}\`));
      }
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

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']
      },
    }
  ],
});
Enter fullscreen mode Exit fullscreen mode

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)"
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
});

Enter fullscreen mode Exit fullscreen mode

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>",
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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


This implementation is based on a real-world production synthetic monitoring system that monitors applications and critical user workflows.

Top comments (0)