Metadata-Version: 2.5
Name: apifyhub-sentinel
Version: 0.3.1
Summary: Drop-in error tracking for Apify actors. One wrapper: with sentinel(apify_hub_key=...).
Project-URL: Homepage, https://apifyhub.com
Project-URL: Documentation, https://apifyhub.com/features/sentinel
Author: apifyhub
License: MIT
Keywords: actor,apify,error-tracking,monitoring,sentinel
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# apifyhub-sentinel

Drop-in **error tracking** for [Apify](https://apify.com) actors — the Python
port of [`@apifyhub/sentinel`](https://www.npmjs.com/package/@apifyhub/sentinel).
It captures exceptions, **groups them
into issues** by fingerprint, carries breadcrumbs, the run's input and the full
Apify run context (run/actor/build ids, consumer, origin), and reports each to
your apifyHub dashboard.

Both ports ship as one product under **one version number** and produce
identical fingerprints, so the same bug raised from a JS actor and a Python
actor lands in the same issue.

## Install

```bash
pip install apifyhub-sentinel
```

Requires Python 3.9+. Zero runtime dependencies (stdlib `urllib` only).

## Usage

Wrap your actor body — any raise is captured, grouped, flushed, and re-raised
unchanged:

```python
import os
from apifyhub_sentinel import sentinel

with sentinel(apify_hub_key=os.environ["APIFYHUB_KEY"]):
    ...  # the actor's normal code
```

Or wire it manually for finer control:

```python
from apifyhub_sentinel import init, capture_exception, add_breadcrumb, flush

init(apify_hub_key=os.environ["APIFYHUB_KEY"])

add_breadcrumb(category="http", message="GET /search", level="info")

try:
    do_work()
except Exception as exc:
    capture_exception(exc)   # grouped into an issue by fingerprint
    flush()                  # ensure delivery before the container exits
    raise
```

### Setup

1. Generate an **apifyHub key** on your [apifyhub.com](https://apifyhub.com) profile.
2. Add it as a **secret env var** named `APIFYHUB_KEY` (Apify Console → your actor →
   Settings → Environment variables).
3. Deploy. Crashes and captured errors now show up as issues on your dashboard.

## API

| Function | Purpose |
| --- | --- |
| `init(apify_hub_key, **opts)` | Initialise; installs global excepthooks (unless `capture_unhandled=False`). Idempotent. |
| `sentinel(apify_hub_key, **opts)` | Context manager: init + capture/flush/re-raise around the body. |
| `capture_exception(exc, ...)` | Report an exception. Returns the event id. |
| `capture_message(msg, ...)` | Report a plain message at a given `level`. |
| `add_breadcrumb(...)` | Append a timeline entry shown with the next error. |
| `set_tag(k, v)` / `set_context(name, obj)` / `set_user(...)` | Attach metadata to subsequent events. |
| `flush(timeout_s=2.0)` | Await in-flight sends. Returns whether it drained in time. |

### Options

Every option `init()` takes is also accepted by `sentinel()`:

| Option | Default | Meaning |
| --- | --- | --- |
| `apify_hub_key` | required | Bearer token; the server SHA-256 hashes it and matches the owner's hash. |
| `endpoint` | production | Override the ingest URL. |
| `debug` | `False` | Print internal failures to stderr. |
| `force_outside_apify` | `False` | Send even when `APIFY_IS_AT_HOME != "1"` (local testing). |
| `capture_unhandled` | `True` | Install `sys.excepthook` / `threading.excepthook` handlers. |
| `capture_input` | `True` | Fetch the run's INPUT record and attach it as `contexts.input`. |
| `sample_rate` | `1.0` | Drop a fraction of events client-side. |
| `max_breadcrumbs` | `50` | Ring-buffer size per scope. |
| `release` / `environment` | `None` | Free-form labels carried on every event. |
| `before_send` | `None` | Last-chance hook to scrub or drop an event (return `None` to drop). |

## Input capture

The run's `INPUT` record is fetched once at init on a background thread and
attached to every event as `contexts.input`, so you can see what the actor was
asked to do when it broke. Notes:

- **Secret input fields stay encrypted.** Only the Apify SDK's `get_input()`
  decrypts them, so no plaintext secrets reach the dashboard.
- Oversized inputs (>16KB) are truncated.
- Best-effort: a crash before the fetch resolves ships without input.
- Turn it off with `capture_input=False`.

## Differences from the JS port

Behaviour is deliberately identical; only the idioms differ.

- `with sentinel(...)` replaces `await withSentinel(options, fn)`.
- Options are snake_case keyword arguments rather than an options object.
- `flush()` is synchronous and returns `bool` (no `await`).
- Uncaught errors are caught via `sys.excepthook` and `threading.excepthook`.
  Both chain to the previous hook, so the traceback still prints and the process
  still exits non-zero — matching the JS port, which restores Node's default
  crash semantics by hand.
- Stack frames come from `traceback.extract_tb`, which is already newest-last,
  so unlike the JS port there is no reversal step.

## Tests

```bash
./run_tests.sh
```

Runs the unit tests, then the **cross-port parity check**: `tests/check_parity.mjs`
computes fingerprints with the JS source and `tests/compare_parity.py` asserts
the Python port produces the same 8-char hex for every case. That check is the
thing that keeps one bug from splitting into two issues on the dashboard, so run
it after touching `_fingerprint.py` or the JS `fingerprint.ts`.
