/purr

PURR (PUppeteer RunneR) is a devops-friendly tool for browser testing and monitoring.

Primary LanguageJavaScriptOtherNOASSERTION

PURR

Intro

PURR (PUppeteer RunneR) is a devops-friendly tool for browser testing and monitoring.

The goal of this project is to have single set of browser checks, that could be used as tests, canaries in CI/CD pipelines and scenarios for production monitoring.

The tool uses puppeteer (https://pptr.dev/) to run standalone browsers (Chrome and Firefox are supported currently).

Checks results are stored as JSON reports, screenshots, traces and HAR files.

PURR has three modes:

  • CLI (mainly used in CI/CD pipelines)
  • Queue worker (scheduled monitoring checks)
  • REST service (show results and expose internal metrics for prometheus)

Configuration

data/checks dir

Stores descriptions of every single check

data/suites dir

Organizes checks into suites

data/parameters.yml

Specifies check parameters, i.e. target host or cookie values

data/schedules.yml

Define your schedules here

priority of parameters

  • Defaults from parameters.yml
  • Defaults from check/suite
  • Params from env
  • Explicitly specified params

PURR configuration

You can configure PURR behaviour using environmental variables. Please see the ENV.md for details.

CLI

Requirements

Before first run of whole application or custom checks, you need to provide .env file. Sample you can find in .env.sample file and full list of supported ENV variables in ENV.md.

Build

Native docker build

docker build -f $(pwd)/docker/Dockerfile -t ghcr.io/semrush/purr:latest .

Or use predefined make directive

make docker-build

Run single check

Native docker run

docker build -f $(pwd)/docker/Dockerfile -t ghcr.io/semrush/purr:latest .
docker run --rm -v $(pwd):/app --env-file $(pwd)/.env ghcr.io/semrush/purr:latest check example-com

Or use predefined make directive

make run-check CHECK_NAME=example-com

Run suite

Native docker run

docker build -f $(pwd)/docker/Dockerfile -t ghcr.io/semrush/purr:latest .
docker run --rm -v $(pwd):/app --env-file $(pwd)/.env ghcr.io/semrush/purr:latest suite example-com-suite

Or use predefined make directive

make run-suite SUITE_NAME=example-com-suite

Results

$ tree storage
storage
├── console_log
│   ├── console_semrush-com_0cedaca3-1153-47df-a616-55e21bf54635.log
│   └── console_semrush-com_ded5990f-7638-48e6-9d0e-77f8dba376fd.log
├── screenshots
│   ├── screenshot_semrush-com_0cedaca3-1153-47df-a616-55e21bf54635.png
│   └── screenshot_semrush-com_ded5990f-7638-48e6-9d0e-77f8dba376fd.png
└── traces
    ├── trace_semrush-com_0cedaca3-1153-47df-a616-55e21bf54635.json
    └── trace_semrush-com_ded5990f-7638-48e6-9d0e-77f8dba376fd.json

Traces and HARs

PURR have a feature to save Chromium traces and HARs.

You can open traces in Chromium Devtools Network Inspector or Chrome DevTools Timeline Viewer. For HAR you can use GSuite Toolbox HAR Analyze.

Scheduled jobs

Run application

docker compose up -d

Apply schedules

docker compose exec worker /app/src/cli.js schedule clean
docker compose exec worker /app/src/cli.js schedule apply

Stop schedules

docker compose exec worker /app/src/cli.js schedule clean

REST API

To enable access to API server, just create file docker-compose.override.yaml and place replacement of server service like in example:

version: '3.9'

services:
  server:
    ports:
      - '8080:8080'

After that, all commands called via docker compose will apply configuration and provide access to server with address http://localhost:8080

Endpoints

GET /metrics

Prometheus metrics

GET /api/v1/checks

List of existing checks

query strings

POST /api/v1/checks/:name

Add check to queue

Response

200: Returns check report 202: Returns id of created check job

Payload
  • name: string Check name to run
  • params: array Any check parameter
Query strings
  • wait: bool default: false Just return link for report when false
  • view: string default: json options: json, pretty Output format
Example:
curl -X POST \
  -d 'params[TARGET_SCHEMA]=http' \
  -d 'params[TARGET_DOMAIN]=rc.example.com' \
  http://localhost:8080/api/v1/checks/main-page?wait=true&view=pretty

GET /api/v1/reports/:id

Get report

Payload
  • id: string Check report id
Query strings
  • view: string default: json options: json, pretty Output format

GET /api/v1/reports/:name/latest/failed

Get report

Payload
  • name: string Check report name
Query strings
  • schedule: string default: '' Schedule name

  • view: string default: json options: json, pretty Output format

Writing checks

PURR translates scenario steps described in ./data/checks into methods of puppeteer.Page object. You can check puppeteer reference documentation for up-to-date capabilities.

Methods

List of methods which were tested by the PURR dev team

- goto:
    - '{{ TARGET_SCHEMA }}://{{ TARGET_DOMAIN }}/{{ TARGET_PAGE }}/'

- goto:
    - '{{ TARGET_SCHEMA }}://{{ TARGET_DOMAIN }}/{{ TARGET_PAGE }}/'
    - waitUntil: networkidle2

- waitForNavigation:
    - waitUntil: domcontentloaded

- click:
    - '{{ CSS_OR_DOM_SELECTOR }}'

- type:
    - '{{ CSS_OR_DOM_SELECTOR }}'
    - '{{ STRING_TO_TYPE }}'

- waitForSelector:
    - '{{ CSS_OR_DOM_SELECTOR }}'

- setCookie:
    - name: '{{ COOKIE_NAME }}'
      value: '{{ COOKIE_VALUE }}'
      domain: .{{ TARGET_DOMAIN.split('.').slice(-2).join('.') }}

Testing checks

to launch your check run

make check name=main-page

Custom Methods

Custom steps methods are described in src/actions dir and can be executed in checks.

- actions.common.selectorContains:
    - '[data-test="user-profile"]'
    - 'User Name:'

Includes

Feel free to use YAML anchors in your scenarios

.login_via_popup: &login_via_popup
  - click:
    - '[data-test="login"]'
  - waitForSelector:
    - '[data-test="email"]'
  - type:
    - '[data-test="email"]'
    - '{{ USER_EMAIL }}'
  - type:
    - '[data-test="password"]'
    - '{{ USER_PASSWORD }}'
  - click:
    - '[data-test="login-submit"]'


logged-user-dashboard:
  parameters:
    USER_PASSWORD: secret
  steps:
    - goto:
      - '{{ TARGET_URL }}'
      - waitUntil: networkidle2
    <<: *login_via_popup
      parameters:
        USER_EMAIL: root@localhost
    - waitForSelector:
      - '[data-test="user-profile"]'
    - actions.common.selectorContains:
      - '[data-test="user-profile"]'
      - 'User Name:'

Variables

You can specify parameters in checks and suites yaml files under 'parameters' key

parameters:
  TARGET_HOST: localhost

valid-password:
  <<: *login_via_popup
  parameters:
    USER_EMAIL: root@localhost
    USER_PASSOWRD: secret

invalid-password:
  <<: *login_via_popup
  parameters:
    USER_PASSOWRD: invalid

Proxy

To run a check, suite or schedule throw proxy use 'proxy' key

check-page-from-india:
  proxy: 'socks5h://user:password@proxy.service:8080'
  steps:
    - goto:
        - '{{ TARGET_URL }}'
    - waitForSelector:
        - body
    - actions.common.selectorContains:
        - body
        - 'Your location: India'

Development

Main entrypoint for project is src/cli.js.

There are two options for development avalaible.

  • cli command development require only call from cli. docker-compose.single.yml placed for your convinience
  • client-server model. That mode described in docker-compose.server.yml. There we have two services avalaible
    • sever - provides api endpoint and other stuff related to daemon itself
    • worker - queue worker.
make start-dev
make attach-dev

Tests

Run tests:

yarn run test

Mocks

We are using Jest testing framework.

You can mock module like that:

// If `manual` mock exist in dir `__mocks__` along module file, will be used
// automatically.
//
// Mocked module methods return `undefined`, fields return actual value.
jest.mock('../../config');
// Now `config` for all scripts will be `{ concurrency: 9 }`
jest.mock('../../config', () => ({ concurrency: 9 }));

Or like that:

const config = require('../../config');

config.concurrency = 1;
config.getWorkingPath = jest.fn().mockImplementation(() => {
  return '/working/path';
});
Be careful

Methods mock\unmock must be executed before module imports and in the same scope. Mocks state restoring after each test, but only when you did not used jest.mock()