aicrawl

package
v0.76.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 19, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package aicrawl aggregates $ai_crawl events: the AI crawlers' own visits to the customer's site, recorded server-side.

This is the half of AI visibility that a JavaScript pixel physically cannot see. GPTBot, ClaudeBot and PerplexityBot execute no JavaScript, so they never fire a $pageview — every client-side analytics tool on the market reports zero of them, and the GEO monitors that would care never touch the customer's server. The fix is a few lines of edge middleware that report the hit from the request itself; the events land on the same instance as everything else, which is what lets the four stages of the loop finally sit on one log:

crawled → readable → named in the answer → sent a human who converted

Every incumbent holds exactly one of those.

Event contract (properties on $ai_crawl):

crawler   string — normalized name, e.g. "GPTBot", "ClaudeBot"
operator  string — who runs it: "OpenAI", "Anthropic", "Perplexity", ...
purpose   string — "training" | "search" | "user" (see Purpose*)
path      string — request path, query stripped by the middleware
status    number — HTTP status the crawler received
site      string — hostname, same key the dashboard's site selector uses

Index

Constants

View Source
const (
	PurposeTraining = "training"
	PurposeSearch   = "search"
	PurposeUser     = "user"
)

The three reasons an AI company fetches a page, kept apart because they mean completely different things to the person reading the report:

  • training: your words may end up in a future model. No traffic, no attribution.
  • search: you are being indexed for answers. This is the one that feeds citations.
  • user: a person asked an assistant about you RIGHT NOW and it fetched this page to answer them. That is a live human intent signal, not a robot.

Lumping them together is the mistake most crawler dashboards make: it turns the most valuable signal on the list into a rounding error inside "bot traffic".

View Source
const CrawlEvent = "$ai_crawl"

CrawlEvent is the event name the edge middleware writes. Exported because several packages must recognise it: ingest exempts it from the bot filter (it is BY DEFINITION a bot hit, reported by the server), and the billing/pulse counters exclude it so a crawl wave never reads as a traffic spike or an invoice.

Variables

This section is empty.

Functions

func Identify

func Identify(ua string) (name, operator, purpose string, ok bool)

Identify classifies a user agent. ok is false for anything that isn't a known AI fetcher — including Googlebot, which is a search crawler people already have tools for and would only dilute this report.

func Known

func Known(ua string) bool

Known reports whether ua is a recognised AI crawler.

Types

type CrawlerRow

type CrawlerRow struct {
	Crawler  string `json:"crawler"`
	Operator string `json:"operator"`
	Purpose  string `json:"purpose"`
	Hits     int    `json:"hits"`
	Pages    int    `json:"pages"`     // distinct paths it fetched
	Errors   int    `json:"errors"`    // hits it received a 4xx/5xx for
	LastAt   string `json:"last_at"`   // RFC3339, "" if never
	LastPath string `json:"last_path"` // the newest path it fetched
}

CrawlerRow is one crawler's activity over the window.

type DayPoint

type DayPoint struct {
	Day      string `json:"day"` // YYYY-MM-DD
	Hits     int    `json:"hits"`
	Training int    `json:"training"`
	Search   int    `json:"search"`
	User     int    `json:"user"`
}

DayPoint is one day of the crawl trend, split by purpose so a training sweep never hides the fact that live user fetches went to zero.

type GapRow

type GapRow struct {
	Path     string `json:"path"`
	Views    int    `json:"views"` // human pageviews over the window
	Visitors int    `json:"visitors"`
}

GapRow is a page real visitors read that no AI crawler has ever fetched. The most actionable row in the module: it is a specific URL, it has proven human demand, and no assistant can cite what it has never read.

type OperatorRow

type OperatorRow struct {
	Operator string `json:"operator"`
	Hits     int    `json:"hits"`
	Pages    int    `json:"pages"`
	Training int    `json:"training"`
	Search   int    `json:"search"`
	User     int    `json:"user"`
	LastAt   string `json:"last_at"`
}

OperatorRow rolls the crawlers up to the company behind them — the level a reader actually thinks in ("has OpenAI read my site", not "has OAI-SearchBot").

type PathRow

type PathRow struct {
	Path     string `json:"path"`
	Hits     int    `json:"hits"`
	Crawlers int    `json:"crawlers"`
	Errors   int    `json:"errors"`
	LastAt   string `json:"last_at"`
}

PathRow is one page's crawl coverage. Crawlers is the count of DISTINCT crawlers that fetched it, which is the number that matters: a page every operator has read is a page that can be cited by every assistant.

type Result

type Result struct {
	Days      int  `json:"days"`
	Reporting bool `json:"reporting"` // any $ai_crawl event ever seen
	Hits      int  `json:"hits"`
	Pages     int  `json:"pages"`
	// Probes are requests dropped as vulnerability scans wearing a bot user-agent (/.env,
	// /@fs/etc/passwd and friends). Reported rather than silently discarded: an operator whose
	// total quietly shrank is a number nobody can reconcile, and "we ignored 11 fake requests" is
	// a more trustworthy line than a total that happens to be right.
	Probes    int           `json:"probes"`
	Crawlers  []CrawlerRow  `json:"crawlers"`
	Operators []OperatorRow `json:"operators"`
	Paths     []PathRow     `json:"paths"`
	Trend     []DayPoint    `json:"trend"`
	Gaps      []GapRow      `json:"gaps"`
	Training  int           `json:"training"`
	Search    int           `json:"search"`
	User      int           `json:"user"`
	Errors    int           `json:"errors"`
	FirstAt   string        `json:"first_at"`
	LastAt    string        `json:"last_at"`
	Note      string        `json:"note,omitempty"`
}

Result is the whole report. Empty is a legitimate answer and Reporting says so — "no crawler has ever fetched this site" and "nobody has installed the reporter" look identical in the data and mean opposite things, so they are never conflated.

func Compute

func Compute(evs []event.Event, days int, asof time.Time) Result

Compute aggregates the window ending at asof. Human pageviews are read from the same slice to find the coverage gaps, so the caller passes ALL events, not a filtered set.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL