Skip to content

Commit 1e39b80

Browse files
committed
docs: Refresh and simplify SDK agent guidance
1 parent d9fa7e3 commit 1e39b80

3 files changed

Lines changed: 74 additions & 69 deletions

File tree

‎AGENTS.md‎

Lines changed: 20 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,32 @@
11
# AGENTS.md
22

3-
Guidance for coding agents working in `posthog-python`.
3+
Guidance for coding agents working on the PostHog Python SDK (`posthog`). Runtime: `posthog/`; main tests: `posthog/test/`.
44

5-
## Repo context
5+
## Start here
66

7-
- This repository contains the PostHog Python SDK, published as `posthog`.
8-
- The main runtime package is `posthog/`; tests live under `posthog/test/`.
9-
- The project uses `uv` for local development. See `CONTRIBUTING.md` for setup.
10-
- Public API changes: follow "Public API changes" in [CONTRIBUTING.md](./CONTRIBUTING.md). As an agent, also:
11-
- When reviewing or fixing someone else's PR, don't ask for or open an issue, but note in the review when an external contributor's PR changes public API without one.
12-
- The author is a PostHog maintainer when the PR's `author_association` is `MEMBER` or `OWNER` (`gh api repos/PostHog/posthog-python/pulls/<number> --jq .author_association`) or, before a PR exists, when `gh api orgs/PostHog/members/$(gh api user --jq .login)` succeeds. If the check fails or can't run, treat the author as an external contributor.
13-
- A published [sdk-spec](https://github.com/PostHog/sdk-specs) that defines the API counts as the agreement, so no issue is needed.
14-
- For an external contributor with no agreed issue and no spec, stop before implementing and draft the issue body for the user to post. Open it only if they ask.
15-
- Before implementing or reviewing SDK behavior, check [PostHog/sdk-specs](https://github.com/PostHog/sdk-specs) for a spec covering it (its README lists every capability). If one exists, use it as the cross-SDK contract for the behavior the PR changes, and call out any divergence in that behavior in the PR description. Don't fix or flag discrepancies between the spec and code the PR doesn't touch. If none exists, carry on.
16-
- Keep edits targeted and follow existing patterns. Prefer adding or updating tests near the behavior you change.
7+
- Use `uv`; follow [CONTRIBUTING.md](./CONTRIBUTING.md) for setup and [checks](./CONTRIBUTING.md#ci-aligned-checks). Run the smallest relevant tests first.
8+
- For `openfeature-provider/` changes, also follow its [contributor guide](./openfeature-provider/CONTRIBUTING.md); root pytest collection does not cover it.
9+
- Read [RELEASING.md](./RELEASING.md) when adding changesets or changing builds/publishing. Releases publish both `posthog` and its generated `posthoganalytics` mirror.
10+
- For public API changes, update/review `references/public_api_snapshot.txt` and run its check as described in the contributor guide.
1711

18-
## Capture protocol (`capture_mode`)
12+
## Public API and specifications
1913

20-
The client supports two ingestion wire protocols, selected by `capture_mode` (precedence: explicit `Client(capture_mode=...)` kwarg > `POSTHOG_CAPTURE_MODE` env var > default).
14+
Follow [Public API changes](./CONTRIBUTING.md#public-api-changes). As an agent, also:
2115

22-
- `"v0"` (default) — legacy `POST /batch/`. Upgrades stay transparent; existing callers are unaffected.
23-
- `"v1"` — `POST /i/v1/analytics/events`: Bearer auth, a typed event `options` object, per-event results, and partial retry.
16+
- When reviewing or fixing someone else's PR, don't ask for or open an issue. Note an external contributor's public API change when it has neither an agreed issue nor an API-defining published spec.
17+
- The author is a PostHog maintainer when the PR's `author_association` is `MEMBER` or `OWNER` (`gh api repos/PostHog/posthog-python/pulls/<number> --jq .author_association`) or, before a PR exists, when `gh api orgs/PostHog/members/$(gh api user --jq .login)` succeeds. If the check fails or can't run, treat the author as an external contributor.
18+
- A published [sdk-spec](https://github.com/PostHog/sdk-specs) that defines the API counts as the agreement, so no issue is needed.
19+
- For an external contributor with no agreed issue and no API-defining spec, stop before implementing and draft the issue body for the user to post. Open it only if they ask. The review/fix exception above still applies.
20+
- Before implementing or reviewing SDK behavior, check [PostHog/sdk-specs](https://github.com/PostHog/sdk-specs) for a covering spec (its README lists every capability). Use it as the cross-SDK contract for changed behavior and call out divergence in the PR description. Don't fix or flag discrepancies in code the PR doesn't touch. If no spec covers it, carry on.
2421

25-
v1 request bodies can additionally be compressed via `capture_compression` (precedence: explicit `Client(capture_compression=...)` kwarg > `POSTHOG_CAPTURE_COMPRESSION` env var > the legacy `gzip` flag > none). Supported values are `"none"`, `"gzip"`, `"deflate"` (zlib-wrapped, RFC 1950, to match the server's decoder and the Go/Rust SDKs), and `"zstd"` (requires the optional `posthog[zstd]` extra; explicit zstd without the package raises, env-var zstd warns and falls back). v0 keeps using its own `gzip` flag; `capture_compression` is v1-only.
22+
## Capture changes
2623

27-
Where the pieces live:
24+
Before changing capture configuration, serialization, routing, or retries, read [docs/capture-protocol.md](./docs/capture-protocol.md) and the relevant tests.
2825

29-
- `posthog/capture_mode.py` — the `CaptureMode` enum and `_resolve_capture_mode()` precedence logic.
30-
- `posthog/capture_compression.py` — the `CaptureCompression` enum and `_resolve_capture_compression()` precedence logic (with `gzip` fallback).
31-
- `posthog/capture_v1.py` — pure transforms (`_to_v1_event`, `_build_v1_batch_body`) and transport (`_post_v1`, `_compress_v1`, `_parse_v1_response`, `_send_v1_batch`, `CaptureV1Error`).
32-
- Public API surface (enforced by `references/public_api_snapshot.txt`): `CaptureMode`, `CaptureCompression` (both re-exported from `posthog`), `CaptureV1Error`, and the two env var names. Everything else in these modules is underscore-private plumbing.
33-
- Routing: `Consumer.request` (async) and `Client._enqueue` (sync) pick the submitter by `capture_mode`.
26+
Preserve v0 defaults/compatibility; strictly typed v1 options and `$set`/`$set_once` relocation; v1-only compression (zlib-wrapped deflate, optional zstd); partial-only per-event retries with stable identity; accumulated drop reporting even on 2xx; terminal v1 `429`; `Retry-After` as a minimum bounded by the shared 30s ceiling; and inline blocking retries with `sync_mode=True`.
3427

35-
v1-specific behavior to preserve when editing: sentinel `$`-properties are lifted into `options` (coerced to native JSON types or omitted — a wrong type 400s the whole batch); top-level `$set`/`$set_once` are relocated into `properties`; only events the server tags `retry` are resent (stable `PostHog-Request-Id`/`created_at`, incrementing `PostHog-Attempt`); a server `drop` is a terminal per-event rejection — drops are accumulated across attempts and surfaced via `CaptureV1Error`/`on_error` even on a 2xx with no retries (a success status is not full delivery); `Retry-After` is a *minimum*, not a replacement (the client waits `max(configured_backoff, min(Retry-After, _MAX_BACKOFF_SECONDS))`); `_MAX_BACKOFF_SECONDS` (30s) is the single ceiling for both the exponential backoff and the `Retry-After` clamp; `429` is terminal.
28+
## Mirror and build safety
3629

37-
Retry blocking matches v0: in the default async mode retries happen on the background consumer thread, but with `sync_mode=True` the partial-retry loop (including its backoff sleeps) runs inline on the calling thread, so a slow/erroring endpoint blocks the caller until retries are exhausted.
38-
39-
## Validation
40-
41-
Useful checks:
42-
43-
```bash
44-
uv run ruff format --check .
45-
uv run ruff check .
46-
uv run mypy --no-site-packages --config-file mypy.ini . | uv run mypy-baseline filter
47-
uv run pytest --verbose --timeout=30
48-
uv run python -W error -c "import posthog"
49-
```
50-
51-
For focused changes, run the smallest relevant `uv run pytest ...` command first.
52-
53-
If public API surface changes, update/check `references/public_api_snapshot.txt` with:
54-
55-
```bash
56-
make public_api_snapshot
57-
make public_api_check
58-
```
59-
60-
## `posthoganalytics` mirror package
61-
62-
This repo also publishes `posthoganalytics`, a generated mirror of `posthog` used by the PostHog app. The mirror is created by copying `posthog/` to `posthoganalytics/` and rewriting absolute imports such as `from posthog.foo import ...` to `from posthoganalytics.foo import ...`.
63-
64-
Important when editing SDK-internal code:
65-
66-
- Prefer relative imports for imports within the SDK package, especially in runtime modules under `posthog/`.
67-
- Good: `from .client import Client`, `from .exception_utils import extract_exception_properties`
68-
- Risky: `from posthog.client import Client`
69-
- Absolute `posthog...` imports inside SDK modules can break the `posthoganalytics` mirror when it is imported inside an application that also has its own `posthog` package/module on `sys.path`.
70-
- Test mirror-sensitive changes by running the normal focused tests and, when relevant, `make prep_local` to generate a local `posthoganalytics` copy for testing in the PostHog app.
71-
- Do not commit generated `posthoganalytics/` directories; they are build/local artifacts.
72-
73-
## Release/build notes
74-
75-
- `make build_release` builds the `posthog` distribution.
76-
- `make build_release_analytics` builds the `posthoganalytics` distribution and temporarily rewrites/copies package files; ensure the working tree is clean before and after running it.
77-
- Release flow publishes both packages; see `RELEASING.md`.
30+
- Prefer relative SDK-internal imports (e.g. `from .client import Client`). Absolute `posthog...` imports can collide with the PostHog app's own package after mirror generation rewrites imports to `posthoganalytics`.
31+
- Run focused tests for mirror-sensitive changes; when app testing is relevant, follow the contributor guide's mirror workflow. **`make prep_local` deletes and recreates `../posthog-python-local`; verify no work there needs preserving before every use.** Do not commit generated `posthoganalytics/` directories.
32+
- `make build_release_analytics` temporarily rewrites/copies source and package files: require a clean working tree before running it and verify the tree is clean afterward.

‎CONTRIBUTING.md‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ uv sync --extra dev --extra test
1818

1919
## CI-aligned checks
2020

21-
Run the same core checks CI uses before opening a PR:
21+
Run the smallest relevant tests first, for example `pytest posthog/test/test_capture_v1.py --timeout=30` for v1 transport changes. Then run these core CI-aligned checks from the repository root in the activated `.venv` populated by the setup commands above:
2222

2323
```bash
2424
ruff format --check .
@@ -28,13 +28,28 @@ pytest --verbose --timeout=30
2828
python -W error -c "import posthog"
2929
```
3030

31+
Without activating `.venv`, prefix Python-tool commands with `uv run --no-sync` after the same setup/sync (including both sides of the mypy pipeline). This uses the populated environment without re-syncing away its selected extras.
32+
33+
For public API changes, regenerate and review `references/public_api_snapshot.txt`, then check it in that environment:
34+
35+
```bash
36+
make public_api_snapshot
37+
make public_api_check
38+
```
39+
40+
These Make targets invoke `python`: keep `.venv` activated, or use `uv run --no-sync make <target>`.
41+
42+
For changes under `openfeature-provider/`, also follow its [contributor guide](./openfeature-provider/CONTRIBUTING.md#local-development). Root pytest collection targets `posthog/test`; it does not substitute for the provider's package-scoped checks.
43+
3144
## Running locally
3245

3346
Assuming you have a [local version of PostHog](https://posthog.com/docs/developing-locally) running, you can run `python3 example.py` to see the library in action.
3447

3548
## Testing changes locally with the PostHog app
3649

37-
Run `make prep_local` to create a sibling folder named `posthog-python-local`. You can then import it into the PostHog app by changing `pyproject.toml` like this:
50+
**Warning:** `make prep_local` deletes and recreates `../posthog-python-local`. Before using it (including every re-run), verify that no work there needs preserving. It creates a renamed SDK copy for local testing; do not commit generated `posthoganalytics/` directories.
51+
52+
You can then import that copy into the PostHog app by changing the app's `pyproject.toml` like this:
3853

3954
```toml
4055
dependencies = [
@@ -43,7 +58,7 @@ dependencies = [
4358
...
4459
]
4560
...
46-
[tools.uv.sources]
61+
[tool.uv.sources]
4762
posthoganalytics = { path = "../posthog-python-local" }
4863
```
4964

@@ -61,4 +76,4 @@ This section is for external contributors. PostHog maintainers (members of the P
6176
- Check first whether an existing option or hook, such as `before_send`, already covers the use case. We avoid offering two ways to do the same thing.
6277
- If a reviewer suggests a different API on your PR, confirm it with them before re-implementing. Treat it as a question, not an instruction.
6378

64-
`make public_api_snapshot` regenerates `references/public_api_snapshot.txt`, and CI runs `make public_api_check` to catch an outdated snapshot. A diff in that file means your change touches public API.
79+
Follow the snapshot update/check commands in [CI-aligned checks](#ci-aligned-checks); CI checks for an outdated snapshot. A diff in `references/public_api_snapshot.txt` means your change touches public API.

‎docs/capture-protocol.md‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Capture protocol reference
2+
3+
Read this before changing capture configuration, serialization, routing, or retries. This documents the Python implementation; it is not a substitute for checking applicable [sdk-specs](https://github.com/PostHog/sdk-specs), nor a claim that a published spec defines every v1 detail below.
4+
5+
## Configuration and wire formats
6+
7+
The analytics client supports two ingestion wire protocols, selected by `capture_mode` (precedence: explicit `Client(capture_mode=...)` kwarg > `POSTHOG_CAPTURE_MODE` env var > default).
8+
9+
- `"v0"` (default) — legacy `POST /batch/`. Upgrades stay transparent; existing callers are unaffected.
10+
- `"v1"` — `POST /i/v1/analytics/events`: Bearer auth, a typed event `options` object, per-event results, and partial retry.
11+
12+
v1 request bodies can additionally be compressed via `capture_compression` (precedence: explicit `Client(capture_compression=...)` kwarg > `POSTHOG_CAPTURE_COMPRESSION` env var > the legacy `gzip` flag > none). Supported values are `"none"`, `"gzip"`, `"deflate"` (zlib-wrapped, RFC 1950, to match the server's decoder and the Go/Rust SDKs), and `"zstd"` (requires the optional `posthog[zstd]` extra; explicit zstd without the package raises, env-var zstd warns and falls back to the legacy `gzip` flag or none). v0 keeps using its own `gzip` flag; `capture_compression` is v1-only.
13+
14+
## Implementation and API map
15+
16+
- [`posthog/capture_mode.py`](../posthog/capture_mode.py) — the `CaptureMode` enum and `_resolve_capture_mode()` precedence logic.
17+
- [`posthog/capture_compression.py`](../posthog/capture_compression.py) — the `CaptureCompression` enum and `_resolve_capture_compression()` precedence logic (with `gzip` fallback).
18+
- [`posthog/capture_v1.py`](../posthog/capture_v1.py) — pure transforms (`_to_v1_event`, `_build_v1_batch_body`) and transport (`_post_v1`, `_compress_v1`, `_parse_v1_response`, `_send_v1_batch`, `CaptureV1Error`).
19+
- Public API surface (enforced by [`references/public_api_snapshot.txt`](../references/public_api_snapshot.txt)): `CaptureMode`, `CaptureCompression` (both re-exported from `posthog`), `CaptureV1Error`, and the env var constants `CAPTURE_MODE_ENV_VAR` / `CAPTURE_COMPRESSION_ENV_VAR` naming the two env vars above. Everything else in these modules is private plumbing, not an additional public API.
20+
- Routing: [`Consumer.request`](../posthog/consumer.py) (async) and [`Client._enqueue`](../posthog/client.py) (sync) pick the submitter by the lane's `capture_mode`. The analytics lane uses the client configuration; the separate AI lane is pinned to v0 and its own endpoint.
21+
22+
## v1 serialization and delivery safeguards
23+
24+
- Sentinel `$`-properties are lifted into `options` (coerced to native JSON types or omitted — a wrong type 400s the whole batch).
25+
- Top-level `$set`/`$set_once` are relocated into `properties`.
26+
- After a per-event response, only events the server tags `retry` are resent. Keep `PostHog-Request-Id` / `created_at` stable across attempts and increment `PostHog-Attempt`. Transport failures and retryable HTTP failures retry the pending batch.
27+
- A server `drop` is a terminal per-event rejection. Drops are accumulated across attempts and surfaced via `CaptureV1Error` / the consumer's `on_error` even on a 2xx with no retries or when later retries succeed: a success status is not full delivery.
28+
- `Retry-After` is a *minimum*, not a replacement: wait `max(configured_backoff, min(Retry-After, _MAX_BACKOFF_SECONDS))` for a positive header. `_MAX_BACKOFF_SECONDS` (30s) is the single ceiling for both the exponential backoff and the `Retry-After` clamp.
29+
- `429` is terminal in v1.
30+
31+
Retry blocking matches v0: in the default async mode retries happen on the background consumer thread, but with `sync_mode=True` the partial-retry loop (including its backoff sleeps) runs inline on the calling thread, so a slow/erroring endpoint blocks the caller until retries are exhausted.
32+
33+
## Relevant tests
34+
35+
Review the existing cases in [`test_capture_mode.py`](../posthog/test/test_capture_mode.py), [`test_capture_compression.py`](../posthog/test/test_capture_compression.py), and [`test_capture_v1.py`](../posthog/test/test_capture_v1.py) for precedence, optional zstd, typed serialization, partial retries, request identity, drops, terminal statuses, and bounded backoff. Routing cases also live in [`test_client.py`](../posthog/test/test_client.py) and [`test_consumer.py`](../posthog/test/test_consumer.py). Follow [contributor validation guidance](../CONTRIBUTING.md#ci-aligned-checks), starting with the smallest relevant tests.

0 commit comments

Comments
 (0)