# Recipe: BigQuery timeout → BugTape issue

Turn a synthetic (or real) BigQuery deadline into one grouped server issue an MCP client can open with `get_repro_context`.

**Helper:** [`bugtape_report.py`](./bugtape_report.py) (stdlib `urllib` — optional `requests` note below)  
**Floor:** [`evidence-floor.md`](./evidence-floor.md)  
**No PyPI package** — copy the module into your job repo or vendor it.

## Why this beats “paste Datadog into chat”

1. Stable `url` (`job://analytics/bq/load`) + `DeadlineExceededError:` title → **one issue**, many occurrences.
2. Stack + redacted metadata travel with the occurrence.
3. Coding agent uses MCP: `list_recommended_critical` → `get_repro_context` — not a log paste.
4. Keep Datadog for estate APM/logs if present; **do not** skip BugTape for the agent loop.

## Minimal hook

```python
from bugtape_report import report_failure, DeadlineExceededError

# Real code often catches google.api_core.exceptions.DeadlineExceeded —
# map it the same way (title will be DeadlineExceeded: … / DeadlineExceededError: …).

def load_clients():
    try:
        run_bigquery_load()
    except Exception as exc:  # narrow to DeadlineExceeded in production
        report_failure(
            exc,
            url="job://analytics/bq/load",  # stable — never interpolate job ids here
            user_id=tenant_id,              # optional affected user
            metadata={"job": "nightly_client_rollup", "attempt": attempt},
            # description/metadata/query text are redacted by default
        )
        raise
```

Environment:

```bash
export BUGTAPE_KEY=bt_test_…          # test project first
export BUGTAPE_RELEASE="$(git rev-parse --short HEAD)"
# optional: BUGTAPE_ENDPOINT=https://app.bugtape.ai/v1/ingest
python3 bugtape_report.py             # ships the built-in synthetic demo
```

## Redaction defaults

The helper scrubs SQL-shaped spans, `project.dataset.table` tokens, `gs://` paths, and `client_id=`-style pairs from `description`, `metadata`, and stack text. Turn off only on a scrubbed twin: `redact=False`.

Never send raw row samples or parquet paths that embed client ids.

## Optional `requests` variant

Same contract; swap the transport if the job already depends on `requests`:

```python
import os, requests, traceback

def report_with_requests(exc, *, url: str):
    title = f"{type(exc).__name__}: {exc}"
    if not title.split(":", 1)[0].endswith("Error"):
        title = f"{type(exc).__name__}Error: {exc}"
    requests.post(
        os.environ.get("BUGTAPE_ENDPOINT", "https://app.bugtape.ai/v1/ingest"),
        timeout=5,
        headers={"X-BugTape-Key": os.environ["BUGTAPE_KEY"]},
        json={
            "title": title,
            "platform": "server",
            "release": os.environ.get("BUGTAPE_RELEASE", "unknown"),
            "environment": "production",
            "url": url,
            "events": [{
                "type": "error",
                "data": {"message": title, "stack": traceback.format_exc()},
            }],
        },
    )
```

Prefer `bugtape_report.py` when you want default redaction without copying regexes.

## Local-stack / MCP proof

1. Point `BUGTAPE_KEY` at a local or hosted **test** project.
2. Run `python3 bugtape_report.py` once → `201` with `deduplicated: false`.
3. Run again → same fingerprint, `deduplicated: true`.
4. Pro PAT → MCP `list_recommended_critical` → `get_repro_context` on that issue.
5. Confirm packet has stack/metadata and **no** DOM replay (structured miss).

## Do not

- Install the browser SDK on analytics.ofx.com client grids for this failure class.
- Batch “40 timeouts” into one POST.
- Teach “use Datadog instead” for the fix loop.
