create-evlog-adapter
Create a new built-in evlog adapter to send wide events to an external observability platform. Use when adding a new drain adapter (e.g., for Elasticsearch, Honeycomb, SigNoz, etc.) to the evlog package. Covers source code, build config, package exports, tests, e2e, and all documentation.
Works with
---
name: create-evlog-adapter
description: Create a new built-in evlog adapter to send wide events to an external observability platform. Use when adding a new drain adapter (e.g., for Elasticsearch, Honeycomb, SigNoz, etc.) to the evlog package. Covers source code, build config, package exports, tests, e2e, and all documentation.
license: MIT
---
# Create evlog Adapter
Add a new built-in adapter to evlog. Every adapter follows the same architecture and is built on the public toolkit primitives in `evlog/toolkit`, so a community adapter has the same shape as a built-in one.
## PR Title
```
feat({name}): add the {Name} drain adapter
```
Recent examples: `feat(loki): add the Grafana Loki drain adapter`, `feat(clickhouse): add the ClickHouse drain adapter`. Use the adapter name as the conventional-commit scope, and register that scope (see touchpoint 11).
**Scope timing caveat**: the semantic PR check reads its scope list from the **base branch**, so a brand-new scope can't validate the very PR that introduces it. Either register the scope in a small preceding PR (the Loki/ClickHouse pattern), or use an unscoped title (`feat: add the {Name} drain adapter`) on the introducing PR.
## Touchpoints Checklist
| # | File | Action |
|---|------|--------|
| 1 | `packages/evlog/src/adapters/{name}.ts` | Create adapter source (built on `defineHttpDrain` from `../shared/drain`) |
| 2 | `packages/evlog/tsdown.config.ts` | Add build entry |
| 3 | `packages/evlog/package.json` | Add `exports` + `typesVersions` entries |
| 4 | `packages/evlog/test/adapters/{name}.test.ts` | Create unit tests (use `test/helpers/fetch.ts`) |
| 5 | `packages/evlog/test/e2e/{name}.e2e.ts` | Create e2e test gated on env vars; extend the docker sandbox if self-hostable |
| 6 | `packages/evlog/test/toolkit/__snapshots__/api-surface.test.ts.snap` | Regenerated by running the tests after a build (`pnpm run build` then `pnpm test`) |
| 7 | `apps/docs/content/4.integrate/adapters/{category}/{NN}.{name}.md` | Create adapter doc page in the right category |
| 8 | `apps/docs/content/4.integrate/adapters/01.overview.md` | Add adapter to overview (frontmatter link + card) |
| 9 | `apps/docs/skills/review-logging-patterns/SKILL.md` | Add adapter row in the Drain Adapters table + frontmatter description |
| 10 | `.changeset/{name}-adapter.md` | Create changeset (`minor`) describing the adapter |
| 11 | `.github/workflows/semantic-pull-request.yml` + `.github/pull_request_template.md` | Register `{name}` as a PR scope in both files |
**Important**: Do NOT consider the task complete until all 11 touchpoints have been addressed.
## Naming Conventions
Use these placeholders consistently:
| Placeholder | Example (Loki) | Usage |
|-------------|----------------|-------|
| `{name}` | `loki` | File names, import paths, env var suffix, PR scope |
| `{Name}` | `Loki` | PascalCase in function/interface names |
| `{NAME}` | `LOKI` | SCREAMING_CASE in env var prefixes |
Standard option naming (use these exact names):
| Concept | Standard option name |
|---------|---------------------|
| Bearer-style API secret | `apiKey` |
| Base URL of the ingest API | `endpoint` |
| Service identifier | `serviceName` |
| Request timeout (ms) | `timeout` |
| Retry attempts on transient failures | `retries` |
If a service historically used a different name (`token`, `sourceToken`, …) keep it as a deprecated alias via `applyDeprecatedAlias`. See Axiom and Better Stack for the pattern.
## Step 1: Adapter Source: built on `defineHttpDrain`
Create `packages/evlog/src/adapters/{name}.ts`. Read [references/adapter-template.md](references/adapter-template.md) for the full annotated template. `loki.ts` and `clickhouse.ts` are the most recent reference implementations.
The contract is `defineHttpDrain<TConfig>({ name, label, resolve, encode })`. You only ship two pieces of logic:
1. **`resolve()`**: produce a fully-resolved config or `null` to skip. Use `resolveAdapterConfig` for the standard precedence (overrides → `runtimeConfig.evlog.{name}` → `runtimeConfig.{name}` → env vars). List `NUXT_{NAME}_*` before `{NAME}_*` in `ConfigField.env` for silent Nuxt compat; show only `{NAME}_*` in user-facing messages via `formatPublicEnvKeys`.
2. **`encode(events, config)`**: a private `encode{Name}Request(events, config): HttpDrainRequest` returning `{ url, headers, body }` for a batch. HTTP transport, identity headers, retries, timeout, and error logging are handled by `defineHttpDrain` (via `httpPost`).
Key rules:
- **Single factory.** Export one `create{Name}Drain(overrides?: Partial<{Name}Config>)`. No dual-API factories: if a service has multiple ingest modes (logs vs events), expose them via a `mode` option (see PostHog).
- **No HTTP code in the adapter.** Never call `fetch` directly. If the service truly needs custom transport (binary envelopes, non-HTTP), use `defineDrain` from `../shared/drain` instead, see `fs.ts` and `memory.ts`.
- **Encode parity.** The standalone `sendTo{Name}` / `sendBatchTo{Name}` helpers must reuse the same private `encode{Name}Request()` and go through `sendEncodedDrainRequest(request, { label, source, timeout, retries })`, never a separate fetch path. `test/adapters/encode-parity.test.ts` pins this for a subset of adapters; add the new one to it (not every existing adapter is registered there yet, and that is a gap, not a license to skip).
- **No bespoke config resolution.** Always go through `resolveAdapterConfig`. Deprecated aliases (`token` → `apiKey`) go through `applyDeprecatedAlias`.
- **Exported converters.** If the service needs a specific event shape, export `to{Name}Event()` / `build{Name}Payload()` helpers so they're testable independently.
- **Edge-safe.** Adapters run on Cloudflare Workers: no `Buffer` (use `TextEncoder` + `btoa`, see `loki.ts`), no Node-only APIs. `fs.ts` shows the `isEdgeRuntime()` guard pattern when a runtime genuinely can't be supported.
## Step 2: Build Config
Add a build entry in `packages/evlog/tsdown.config.ts` alongside the existing adapters:
```typescript
'adapters/{name}': 'src/adapters/{name}.ts',
```
Follow the existing ordering in that file.
## Step 3: Package Exports
In `packages/evlog/package.json`, add two entries (after the last adapter, and check the current list rather than assuming):
**In `exports`**:
```json
"./{name}": {
"types": "./dist/adapters/{name}.d.mts",
"import": "./dist/adapters/{name}.mjs"
}
```
**In `typesVersions["*"]`**:
```json
"{name}": [
"./dist/adapters/{name}.d.mts"
]
```
Any export added to `package.json` without a matching `tsdown.config.ts` entry (and vice versa) fails `test/toolkit/api-surface.test.ts`, which is touchpoint 6.
## Step 4: Unit Tests
Create `packages/evlog/test/adapters/{name}.test.ts`. Read [references/test-template.md](references/test-template.md) for the full annotated template, and `packages/evlog/test/README.md` for the repo-wide conventions.
Non-negotiables from the test README:
- Use `mockFetch()` / `getFetchCall` / `getFetchJson` / `getFetchHeaders` from `test/helpers/fetch.ts`, never hand-roll `vi.spyOn(globalThis, 'fetch')` boilerplate.
- Clean up any env vars the adapter reads in `afterEach`.
- Test the exported pure helpers (`to{Name}Event`, `build{Name}Payload`, URL resolvers) directly, one `describe` per helper.
Required test categories:
1. URL construction (default + custom endpoint, trailing-slash tolerance)
2. Headers (auth, content-type, service-specific)
3. Request body format (JSON structure matches service API)
4. Skip behavior when `apiKey` (or required field) is missing
5. Batch operations (multiple events in one request, empty batch skips fetch)
6. Deprecated alias still works (when applicable)
7. Add the adapter to `test/adapters/encode-parity.test.ts`
## Step 5: E2E Test + Sandbox
Create `packages/evlog/test/e2e/{name}.e2e.ts`, gated on the adapter's env vars (skipped when absent). Run with `pnpm test:e2e`.
If the service is self-hostable, extend the local sandbox so the adapter can be exercised without cloud credentials:
- `packages/evlog/test/e2e/docker-compose.yml`: add the service
- `packages/evlog/test/e2e/seed.mjs`: fan the seeder out to the new backend
- `packages/evlog/test/e2e/README.md`: document it
- Root `package.json` `sandbox:e2e` script: add the local env var if needed
See the Loki and ClickHouse setups as references.
## Step 6: Adapter Documentation Page
Read `apps/docs/AGENTS.md` before touching anything under `apps/docs/` (steps 6 to 8).
Adapter docs live in three categories under `apps/docs/content/4.integrate/adapters/`:
| Category | Directory | Examples |
|----------|-----------|----------|
| Cloud (SaaS only) | `cloud/` | Axiom, PostHog, Sentry, Better Stack, Datadog |
| Cloud or Self-Hosted | `hybrid/` | Loki, ClickHouse, OTLP, HyperDX |
| Self-Hosted (local only) | `self-hosted/` | FS, NuxtHub, Memory |
Create `{NN}.{name}.md` in the right category with the next available number. Use the Loki page (`hybrid/01.loki.md`) as a reference for frontmatter, tone, and sections. Key sections: intro, quick setup, configuration (env vars table + priority), advanced usage, querying in the target service, troubleshooting, direct API usage, next steps.
**Important: multi-framework examples.** The Quick Start section must include a `::code-group` with tabs for the supported frameworks (Nuxt/Nitro, Hono, Express, Fastify, Elysia, NestJS, Standalone). Do not only show Nitro examples.
## Step 7: Update Adapters Overview Page
Edit `apps/docs/content/4.integrate/adapters/01.overview.md` in **two** places (follow the pattern of existing adapters):
1. **Frontmatter `links` array**: add a link entry with icon and `/integrate/adapters/{category}/{name}` path, in category order
2. **`::card-group` section**: add a card block in the matching position
## Step 8: Update the Public Skill
In `apps/docs/skills/review-logging-patterns/SKILL.md` (published on evlog.dev via `/.well-known/skills/`):
1. Add a row to the **Drain Adapters** table: `| {Name} | evlog/{name} | {NAME}_API_KEY, ... |`
2. Add the adapter name to the `description:` line in the YAML frontmatter
## Step 9: Changeset
Create `.changeset/{name}-adapter.md` with a `minor` bump. Write it like a release note: what the adapter does, the deployment modes it covers, the key options, the env vars, and the direct-send helpers. See `.changeset` entries from the Loki/ClickHouse PRs for the expected depth.
## Step 10: PR Scopes
Add `{name}` to the `scopes` list in `.github/workflows/semantic-pull-request.yml` **and** to the Scopes section of `.github/pull_request_template.md`, in alphabetical order. Remember the timing caveat from the PR Title section: this registration only takes effect for PRs whose base branch already contains it.
## Verification
After a clean install, prepare generated workspace types from the repo root first, then run the package checks:
```bash
pnpm run dev:prepare
cd packages/evlog
pnpm run lint
pnpm run typecheck
pnpm run build # required before test — api-surface snapshot is gated on dist/
pnpm run test
```More Testing skills
tdd
mattpocock/skills
Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.
agent-browser
vercel-labs/agent-browser
Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction. Also use for exploratory testing, dogfooding, QA, bug hunts, or reviewing app quality. Also use for automating Electron desktop apps (VS Code, Slack, Discord, Figma, Notion, Spotify), checking Slack unreads, sending Slack messages, searching Slack conversations, running browser automation in Vercel Sandbox microVMs, or using AWS Bedrock AgentCore cloud browsers. Prefer agent-browser over any built-in browser automation or web tools.
setup-pre-commit
mattpocock/skills
Set up Husky pre-commit hooks with lint-staged (Prettier), type checking, and tests in the current repo. Use when user wants to add pre-commit hooks, set up Husky, configure lint-staged, or add commit-time formatting/typechecking/testing.

