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)
Stores descriptions of every single check
Organizes checks into suites
Specifies check parameters, i.e. target host or cookie values
Define your schedules here
- Defaults from parameters.yml
- Defaults from check/suite
- Params from env
- Explicitly specified params
You can configure PURR behaviour using environmental variables. Please see the ENV.md for details.
- docker
- docker compose
- make
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.
Native docker build
docker build -f $(pwd)/docker/Dockerfile -t ghcr.io/semrush/purr:latest .
Or use predefined make directive
make docker-build
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
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
$ 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
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.
docker compose up -d
docker compose exec worker /app/src/cli.js schedule clean
docker compose exec worker /app/src/cli.js schedule apply
docker compose exec worker /app/src/cli.js schedule clean
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
Prometheus metrics
List of existing checks
Add check to queue
200: Returns check report 202: Returns id of created check job
- name: string Check name to run
- params: array Any check parameter
- wait: bool default: false Just return link for report when false
- view: string default: json options: json, pretty Output format
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 report
- id: string Check report id
- view: string default: json options: json, pretty Output format
Get report
- name: string Check report name
-
schedule: string default: '' Schedule name
-
view: string default: json options: json, pretty Output format
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.
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('.') }}
to launch your check run
make check name=main-page
Custom steps methods are described in src/actions dir and can be executed in checks.
- actions.common.selectorContains:
- '[data-test="user-profile"]'
- 'User Name:'
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:'
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
To run a check, suite or schedule throw proxy use 'proxy' key
check-page-from-india:
proxy: 'socks5h://user:[email protected]:8080'
steps:
- goto:
- '{{ TARGET_URL }}'
- waitForSelector:
- body
- actions.common.selectorContains:
- body
- 'Your location: India'
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
Run tests:
yarn run test
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';
});
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()