# FastAPI

Serve the ProductLense script from a FastAPI app and forward events asynchronously with httpx, keeping visitor counts and countries correct.

Two async routes serve the script from your domain and, on the Scale plan, forward events. Read the [proxy overview](/docs/proxy/overview) first for what each one fixes.

## The routes

```python title="pl_proxy.py"
import json
import os
import time

import httpx
from fastapi import APIRouter, Request, Response

router = APIRouter()
client = httpx.AsyncClient(timeout=5)
_script = {"body": b"", "at": 0.0}


@router.get("/js/a.js")
async def script() -> Response:
    if time.time() - _script["at"] > 3600:
        r = await client.get("https://api.productlense.app/a.js")
        if r.is_success:
            _script.update(body=r.content, at=time.time())
    if not _script["body"]:
        return Response(status_code=502)
    return Response(_script["body"], media_type="application/javascript",
                    headers={"Cache-Control": "public, max-age=86400"})


@router.post("/m/api/e")
async def events(request: Request) -> Response:
    raw = await request.body()
    if len(raw) > 8192:
        return Response(status_code=413)
    try:
        body = json.loads(raw)
    except ValueError:
        return Response(status_code=400)
    if not isinstance(body, dict):
        return Response(status_code=400)
    extra = {"ip": request.client.host if request.client else "", "ua": request.headers.get("user-agent", "")}
    if isinstance(body.get("b"), list):
        body["b"] = [{**e, **extra} for e in body["b"] if isinstance(e, dict)]
    else:
        body.update(extra)
    r = await client.post("https://api.productlense.app/api/v1/events", json=body,
                          headers={"Authorization": f"Bearer {os.environ['PL_API_KEY']}"})
    return Response(status_code=r.status_code)
```

```python title="main.py"
from fastapi import FastAPI

from pl_proxy import router as productlense

app = FastAPI()
app.include_router(productlense)
```

The route reads the raw body because the browser sends it as `text/plain`. Behind a reverse proxy, run Uvicorn with `--proxy-headers --forwarded-allow-ips=<proxy ip>` so `request.client.host` is the visitor's address and not the proxy's.

## Use it

Serve only the script on any plan:

```html
<script defer data-site="SITE_ID" data-api="https://api.productlense.app" src="/js/a.js"></script>
```

On the Scale plan, set `PL_API_KEY` to a key with the `events` scope and change the tag to `data-api="https://example.com/m"`.

## Check it works

1. Open `https://example.com/js/a.js`: it returns JavaScript.
2. Load a page with the developer tools open. The Network tab shows a `POST` to `api/e` with status `204`.
3. Open the Live page in the ProductLense dashboard. Your visit appears within a few seconds.
