<!-- Generated by docs/build.py from contract.html. Edit the HTML, not this file. -->

# The contract

The URL you POST to, and the payload. Everything else is yours to decide.

Which cloud, which log source, which language, whether you buffer or send per request, whether you use Terraform at all — none of that is part of the contract. This page is.

## The request

*one event per HTTP request*

```
POST https://analytics-staging.obsero.ai/v1/events

content-type:     application/json
x-obsero-domain:  acme.com          # which site this event belongs to
x-obsero-key:     <tracking ID>     # proves you own it

{
  "path": "/pricing",
  "method": "GET",
  "statusCode": 200,
  "headers": {
    "host": "acme.com",
    "user-agent": "Mozilla/5.0 ...; compatible; ChatGPT-User/1.0; +https://openai.com/bot",
    "accept": "*/*",
    "signature-agent": "\"https://chatgpt.com\"",
    "signature-input": "sig1=(\"@authority\" \"signature-agent\");created=...",
    "signature": "sig1=:...:"
  }
}
```

| field | rules |
|---|---|
| `path` | Path **only**. `/pricing`, never `/pricing?ref=x` and never a full URL. Leaving the query string on makes every distinct URL its own page in the analytics — the most common mistake by far. |
| `method` | Uppercase. `"GET"`. |
| `statusCode` | A number, not a string. `200`. |
| `headers` | Lowercase names → verbatim values. This is what classification runs on, so send as many as you can get. |

## Headers worth carrying

Roughly in order of value:

| header | what it buys you |
|---|---|
| `user-agent` | everything starts here |
| `from` | GPTBot, Googlebot |
| `signature-agent`, `signature-input`, `signature` | Web Bot Auth — **proves** an agent is who it claims to be, rather than taking a User-Agent at face value |
| `accept`, `accept-language` | browsers vs `*/*` bots |
| `sec-ch-ua`, `sec-ch-ua-platform`, `sec-ch-ua-mobile` | client hints — browsers only |
| `sec-fetch-dest`, `sec-fetch-mode`, `sec-fetch-site` | presence alone is the signal |

> **Warning: Never forward these**
>
> `authorization`, `cookie`, `set-cookie`. Both adapters strip them before anything leaves your account. Keep that behaviour if you rewrite one.

## obsero.mjs — the whole contract in one file

Dependency-free, about 150 lines: `buildEvent`, `sendEvent`, `validateEvent`. Run it to send a test event and confirm your tracking ID works before you deploy anything:

```
node obsero.mjs --token <tracking-id> --domain acme.com
```

Then, in your own code, this is genuinely all it takes:

*the entire integration*

```
import { buildEvent, sendEvent } from "./obsero.mjs";

await sendEvent(
  buildEvent({
    path: req.url,
    method: req.method,
    statusCode: res.statusCode,
    headers: req.headers,
  }),
  { token: process.env.OBSERO_SITE_TOKEN, domain: "acme.com" },
);
```

If you can call that from your edge middleware, your origin server or your log processor, you are done. You do not need anything else in this repo — and if you are on Cloudflare, Fastly, Akamai or a bare origin, this *is* the integration. Do not port a cloud directory you do not need.

`buildEvent` does the two things that are easy to get wrong for you: it strips the query string from `path` (and tolerates being handed an absolute URL), and it lowercases, trims and filters the headers.

## Check it before you ship it

`validateEvent` is the machine-checkable form of everything on this page. It returns a list of problems, empty when the payload is good:

```
cd aws            # or gcp
make test
```

That runs the **real** adapter parser over sample log lines and checks each resulting payload against `validateEvent`. No cloud account needed. If it passes, Obsero will accept what you produce.

## Adapting an adapter

Both adapters have the same three-part shape, and only one part is the contract:

```
1. THE CONTRACT     build the payload and POST it     <- keep this
2. LOG PARSING      log line/entry -> event           <- yours to rewrite
3. TRANSPORT        unwrap the batch, ack it          <- yours to rewrite
```

- `aws/ingestion/lambda/adapter/index.mjs`
- `gcp/ingestion/adapter/index.mjs`

Change parts 2 and 3 to match your log source, leave part 1 alone, then run `make test`. Fixtures live in each cloud's `test/adapter.test.mjs` — add one for any new log shape rather than testing against live traffic.

## Getting a tracking ID

Ask Obsero for one, then keep it out of the repo. Both `site/terraform/` directories read it from a gitignored `terraform.tfvars`:

```
obsero_site_token = "..."
```

`setup.sh` writes that file for you, `chmod 600`. The deployed adapters read `x-obsero-domain` off each request's own Host header, so there is no domain to configure — only the token.

## Code on this page

- [`obsero.mjs`](https://github.com/Obsero-ai/obsero-public/blob/main/obsero.mjs) -- buildEvent, validateEvent, sendEvent
- [`aws/test/adapter.test.mjs`](https://github.com/Obsero-ai/obsero-public/blob/main/aws/test/adapter.test.mjs) -- the AWS adapter checked against the contract
- [`gcp/test/adapter.test.mjs`](https://github.com/Obsero-ai/obsero-public/blob/main/gcp/test/adapter.test.mjs) -- the GCP adapter checked against the contract
