Metadata-Version: 2.4
Name: qa-insight
Version: 1.1.0
Summary: Official QA Insight SDK for Python. Sends test run events (pytest, Robot Framework, behave) to the QA Insight Collector without changing your tests.
Author-email: QA Insight <qainsight.io@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/QA-Insight/qa-insight
Project-URL: Repository, https://github.com/QA-Insight/qa-insight
Project-URL: Bug Tracker, https://github.com/QA-Insight/qa-insight/issues
Keywords: qa,testing,playwright,selenium,pytest,robot,behave,observability
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: pytest
Requires-Dist: pytest>=7.0; extra == "pytest"
Provides-Extra: robot
Requires-Dist: robotframework>=6.0; extra == "robot"
Provides-Extra: behave
Requires-Dist: behave>=1.2.6; extra == "behave"
Provides-Extra: examples
Requires-Dist: pytest>=7.0; extra == "examples"
Requires-Dist: pytest-playwright>=0.4; extra == "examples"
Requires-Dist: playwright>=1.30; extra == "examples"
Requires-Dist: selenium>=4.10; extra == "examples"
Requires-Dist: robotframework>=6.0; extra == "examples"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: ruff>=0.4; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Requires-Dist: twine>=5.0; extra == "dev"
Dynamic: license-file

# qa-insight (Python SDK)

Official [QA Insight](https://github.com/QA-Insight/qa-insight) SDK for **Python**.

Automatically sends the lifecycle of your tests (run start/end and per-test events) to the QA Insight **Collector**, without modifying your tests. Works with **pytest** (including Selenium WebDriver 4 and Playwright), **Robot Framework**, **behave** (BDD) and a **JUnit XML importer** for post-run reporting.

- **Zero runtime dependencies** (stdlib only: `urllib`, `threading`, `queue`, `subprocess`, `re`). Integrations (pytest, robotframework, behave) are optional dependencies.
- Python **3.9+**.
- Never alters your test results; if the Collector is unreachable, it logs a warning and continues.

## Installation

```bash
pip install -e .             # from the repo (development)
pip install -e ".[examples]" # with pytest + Playwright + Selenium + Robot for the examples
```

## Environment variables

| Variable | Default | Description |
|---|---|---|
| `QAI_ENDPOINT` | `http://localhost:3000/collector/v1/events` | QA Insight Collector URL |
| `QAI_API_KEY` | — | QA Insight API key (project + environment) |
| `QAI_ENABLED` | `true` | `false` disables sending |
| `QAI_TIMEOUT_MS` | `2000` | Per-request HTTP timeout |
| `QAI_RETRY_MAX` | `3` | Retries for transient errors (5xx, 429, network) |
| `QAI_FLUSH_TIMEOUT_MS` | `5000` | Max time the final flush waits |
| `QAI_RUN_NAME` | `python-run-<ts>` | Run name |
| `QAI_RUN_ID` | Generated UUID | `externalRunId` (run idempotency) |
| `QAI_OUTBOX_PATH` | `<os.tmpdir()>/qa-insight` | Directory for the durable outbox file (crash recovery) |
| `QAI_FRAMEWORK` | `pytest`/`robot`/`behave`/`junit` | Framework reported in the framework column |
| `QAI_BROWSER` | — | Browser/project (chromium, firefox, webkit, chrome, ...) |

## pytest

The plugin auto-registers (entry point `pytest11`). You only need the environment variables:

```bash
export QAI_ENDPOINT=http://localhost:3000/collector/v1/events
export QAI_API_KEY=qai_...
export QAI_FRAMEWORK=playwright   # or selenium, or pytest
pytest
```

- `pytest_sessionstart` → `RUN_STARTED`
- Each test → `TEST_STARTED` / `TEST_FINISHED` (with `retryIndex` if you use `pytest-rerunfailures`)
- `pytest_sessionfinish` → `RUN_FINISHED`
- Parametrized tests generate unique nodeids → they don't collapse in the backend.

To report the browser per test (browser badges in the UI), set them in a `conftest.py`:

```python
# conftest.py (Playwright)
import pytest
from qa_insight import QaiContext

@pytest.fixture(autouse=True)
def qai_browser(browser):
    QaiContext.set_browser(browser.browser_type.name)
```

```python
# conftest.py (Selenium 4)
import pytest
from qa_insight import QaiContext

@pytest.fixture
def driver():
    from selenium import webdriver
    d = webdriver.Chrome()  # Selenium Manager downloads the driver automatically
    QaiContext.set_webdriver(d)
    yield d
    d.quit()
```

The plugin resolves the browser in this order: `QaiContext.browser` → `QAI_BROWSER` → capabilities of the `QaiContext.webdriver`.

### Cross-browser

- **Playwright**: `pytest --browser chromium --browser firefox --browser webkit` (each browser produces its own run with its badges).
- **Selenium 4**: `pytest --selenium-browser chrome` / `pytest --selenium-browser firefox` (see the example).
- **Robot Framework**: export `QAI_BROWSER=chrome|firefox` and run with `--variable BROWSER:...`.

## Robot Framework

The SDK includes a **library listener** (`QaiInsightLibrary`) that auto-registers, so you don't need
to pass `--listener` on the command line. Just import the library (ideally in a shared resource):

```robotframework
*** Settings ***
Library    qa_insight.robot_library.QaiInsightLibrary
```

On import, the library loads the project's `.env`, emits `RUN_STARTED`, registers the listener for
the whole run (tags, suites, specific tests or individual files) and, on exit, emits
`RUN_FINISHED` reliably through its `close()` method.

Configuration via `.env` (no need to `export`):

```bash
QAI_API_KEY=qai_...
QAI_ENDPOINT=http://localhost:3000/collector/v1/events
QAI_FRAMEWORK=robot
QAI_BROWSER=chrome   # optional: browser badges + Open Browser from SeleniumLibrary
HEADLESS=True        # optional: headless/visible browser mode
```

Run your tests as usual:

```bash
robot tests/                     # everything
robot --include smoke tests/     # by tags
robot --suite my_suite tests/    # by suite
robot --test "my test" tests/    # by test
```

> Alternative (CLI listener): `robot --listener qa_insight.robot_listener.QaiInsightListener tests/`

## behave (BDD)

```python
# features/environment.py
from qa_insight.behave_env import *  # noqa
```

## JUnit XML importer (post-run)

```bash
export QAI_API_KEY=qai_...
qa-insight-import junit.xml --framework pytest
# or for any framework that emits JUnit XML
```

## Manual / no runner usage

```python
from qa_insight import QaiInsight

insight = QaiInsight({"api_key": "qai_...", "framework": "selenium"})
insight.start_run()
insight.start_test("test_login", suite="tests/test_auth.py")
insight.finish_test("test_login", suite="tests/test_auth.py", status="PASSED", duration_ms=1200)
insight.finish_run()
```

## Crash recovery

The SDK is resilient to process death, so a run never stays stuck in `RUNNING`:

- Every event is appended and fsynced to a durable outbox file (`<os.tmpdir()>/qa-insight/<runId>.jsonl` by default) before it is sent.
- If the interpreter dies mid-run (SIGKILL, OOM, crash), the next run recovers the events a previous process failed to deliver and, when that stale run never emitted a terminal event, sends a synthetic `RUN_ABORTED` on its behalf. Re-sends are deduplicated by the Collector via the `Idempotency-Key` (same `eventId`). Files owned by still-running processes are never adopted.
- Outbox files are removed once the run finalizes (`finish_run`/`abort_run` or the shutdown hook).

Persisted events are redacted before touching disk (default secret patterns apply).

## Behavior on network failures

- Non-blocking buffer in a background thread + bounded flush (`QAI_FLUSH_TIMEOUT_MS`) at the end.
- Transient errors (timeout, network, 5xx, 429) are retried with exponential backoff (50 ms·2^n, cap 1 s).
- Permanent errors (4xx) are not retried.
- The API key is **never** printed or included in payloads; `errorMessage`/`stacktrace` are redacted before sending (authorization, bearer, cookie, password, secret, api-key, token, `qai_`, `xox*` + custom literals).
- Payload limited to 100 KB (the stacktrace is truncated if it exceeds it).

## Development

```bash
pip install -e ".[dev]"
ruff check src tests
pytest
```

## License

MIT
