@pieowl/bots
v0.1.1
Published
Server-side helpers for Pieowl: report AI crawler and search bot visits from your server, and serve the tracking script through your own domain.
Maintainers
Readme
@pieowl/bots
Server-side helpers for Pieowl:
- Bot tracking. AI crawlers and search bots (GPTBot, ClaudeBot, PerplexityBot, Googlebot…) don't run JavaScript, so the browser script never sees them. This package reports them from your server to Pieowl's AI & bots report.
- Ad-blocker proxy. Serve the tracking script and send events through your own domain, so fewer events are lost to ad blockers.
For page views and events, add the HTML script to your site. This package is only for what the browser can't see.
Uses only fetch, so it runs on Node.js, edge runtimes, Cloudflare Workers, Deno and Bun. No dependencies on any framework.
npm install @pieowl/botsBot tracking needs your project's client ID and client secret. Keep the secret on the server (environment variables below).
How it works
- Only requests whose user agent looks like a bot are reported. Everything else returns at once, with no network call.
- It never blocks, slows or changes your response, and never throws. Reports are sent in the background, in batches (50 requests or every second).
- Pieowl classifies the bot and checks the IP against the bot owner's published ranges, so fake "GPTBot" user agents are flagged. The IP is used only for this check and is not stored.
Next.js
import { trackBots } from '@pieowl/bots/next';
export const proxy = trackBots({
clientId: process.env.PIEOWL_CLIENT_ID!,
clientSecret: process.env.PIEOWL_CLIENT_SECRET!,
});On Next.js 15 or older, name the file middleware.ts and export it as middleware. To skip static files, add export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] };.
Already have a proxy? Call trackBotRequest in it and keep your own response:
import { trackBotRequest } from '@pieowl/bots';
import { type NextFetchEvent, type NextRequest, NextResponse } from 'next/server';
export function proxy(request: NextRequest, event: NextFetchEvent) {
trackBotRequest(request, { clientId: '…', clientSecret: '…' }, event);
return NextResponse.next();
}Express
import { trackBots } from '@pieowl/bots/express';
app.use(trackBots({
clientId: process.env.PIEOWL_CLIENT_ID,
clientSecret: process.env.PIEOWL_CLIENT_SECRET,
}));Reported after the response is sent, with its status code. The IP is req.ip, so behind a proxy or load balancer set app.set('trust proxy', true). Works with Connect and any (req, res, next) server too.
Hono
import { trackBots } from '@pieowl/bots/hono';
app.use(trackBots({
clientId: process.env.PIEOWL_CLIENT_ID!,
clientSecret: process.env.PIEOWL_CLIENT_SECRET!,
}));On Cloudflare Workers, read the keys with import { env } from 'cloudflare:workers'.
Cloudflare Workers
Wrap your fetch handler. The report stays alive with ctx.waitUntil, and includes the response status.
import { env } from 'cloudflare:workers';
import { withBotTracking } from '@pieowl/bots';
export default {
fetch: withBotTracking((request) => fetch(request), {
clientId: env.PIEOWL_CLIENT_ID,
clientSecret: env.PIEOWL_CLIENT_SECRET,
}),
};Cloudflare Pages (functions/_middleware.ts):
import { trackBotRequest } from '@pieowl/bots';
export const onRequest: PagesFunction<Env> = (context) => {
trackBotRequest(context.request, {
clientId: context.env.PIEOWL_CLIENT_ID,
clientSecret: context.env.PIEOWL_CLIENT_SECRET,
}, context);
return context.next();
};withBotTracking works for any (request, ...) => Response handler, e.g. Deno (Deno.serve(withBotTracking(handler, options))) and Bun.
Any other framework
trackBotRequest(request, options, context?) takes a Fetch Request, or { url, headers, method?, ip?, status? } when your server doesn't have one. Pass context (anything with waitUntil) on serverless and edge platforms.
import { trackBotRequest } from '@pieowl/bots';
import { defineMiddleware } from 'astro:middleware';
export const onRequest = defineMiddleware((context, next) => {
trackBotRequest(context.request, {
clientId: import.meta.env.PIEOWL_CLIENT_ID,
clientSecret: import.meta.env.PIEOWL_CLIENT_SECRET,
});
return next();
});import { trackBotRequest } from '@pieowl/bots';
export default defineEventHandler((event) => {
trackBotRequest({ url: getRequestURL(event).href, headers: getRequestHeaders(event) }, {
clientId: process.env.PIEOWL_CLIENT_ID!,
clientSecret: process.env.PIEOWL_CLIENT_SECRET!,
});
});import { trackBotRequest } from '@pieowl/bots';
import { env } from '$env/dynamic/private';
export const handle = async ({ event, resolve }) => {
trackBotRequest(event.request, {
clientId: env.PIEOWL_CLIENT_ID,
clientSecret: env.PIEOWL_CLIENT_SECRET,
});
return resolve(event);
};In Remix / React Router, call it at the top of your root loader with request. In Vercel Edge Middleware, call trackBotRequest(request, options, context) with the middleware's context argument.
No JavaScript server? Send the requests yourself: POST https://api.pieowl.com/bot with the headers pieowl-client-id and pieowl-client-secret and a body of { "requests": [{ "userAgent", "path", "host?", "method?", "status?", "ip?", "timestamp?" }] } (up to 100 per call).
Ad-blocker proxy
Serve the script and forward events through your own domain. The handler answers <route>/pieowl.js and forwards <route>/track… to Pieowl with the visitor's IP and user agent, so locations and devices stay correct.
import { createRouteHandler } from '@pieowl/bots/route-handler';
export const { GET, POST } = createRouteHandler();import { createRouteHandler } from '@pieowl/bots/express';
app.use('/api/pieowl', createRouteHandler());The Fetch handler from @pieowl/bots/route-handler works anywhere with a Request: Hono (app.all('/api/pieowl/*', (c) => handler(c.req.raw))), Astro endpoints, SvelteKit +server.ts, Nuxt (export default fromWebHandler(createRouteHandler())), Cloudflare Workers.
Then point the script at your route: load /api/pieowl/pieowl.js and set apiUrl: '/api/pieowl' in init.
API
| Import | Export | Use |
|---|---|---|
| @pieowl/bots | trackBotRequest(request, options, context?) | Report one request. Returns a promise that resolves when its batch was sent; never rejects. |
| @pieowl/bots | withBotTracking(handler, options) | Wrap a Fetch handler; reports bots with the response status. |
| @pieowl/bots/next | trackBots(options) | Next.js proxy / middleware. |
| @pieowl/bots/express | trackBots(options), createRouteHandler(options?) | Express / Connect middleware, ad-blocker proxy. |
| @pieowl/bots/hono | trackBots(options) | Hono middleware. |
| @pieowl/bots/route-handler | createRouteHandler(options?) | Ad-blocker proxy as a Fetch handler. |
Options: clientId, clientSecret, apiUrl (default https://api.pieowl.com). The route handler takes only apiUrl.
