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
Build
docker build -f ./docker/Dockerfile . -t puppeteer-runner:latest
Run single check
docker run -v "${PWD}/storage:/src/app/storage" puppeteer-runner:latest ./src/cli/cli.js check semrush-com
Run suite
docker run -v "${PWD}/storage:/src/app/storage" puppeteer-runner:latest ./src/cli/cli.js suite semrush-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 worker
APP_IMAGE_NAME="puppeteer-runner" APP_IMAGE_VERSION="latest" NGINX_IMAGE_NAME="nginx" docker-compose up
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
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://purr.traefik.lcl/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
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('.') }}
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:[email protected]:8080'
steps:
- goto:
- '{{ TARGET_URL }}'
- waitForSelector:
- body
- actions.common.selectorContains:
- body
- 'Your location: India'
Development
IMPORTANT: It's expected that for convenient experience you will use vscode as an IDE with recommended extensions(configs are available in this repository).
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()