purr
                                
                                 purr copied to clipboard
                                
                                    purr copied to clipboard
                            
                            
                            
                        PURR (PUppeteer RunneR) is a devops-friendly tool for browser testing and monitoring.
PURR
- PURR
- Intro
- Configuration
- CLI
- Scheduled jobs
- REST API
- Writing checks
- Development
 
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
- 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.
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:[email protected]: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()