Skip to content
quarry Artifact [bc86c5883b]
Login

Artifact [bc86c5883b]

Artifact bc86c5883b1f4479a6d22f344baeddde8f26b932ff953022e3481264e3ca688c:


package obs

import (
	"regexp"
	"time"
)

// A swarm is a crawler that sends each request from a different address
// with a browser's name: residential proxies, hundreds of networks and
// a hundred countries in one minute, one request per address, almost
// all of them for pages named by a hash (commit diffs, check-ins). Each
// request alone looks like a person, so the traffic page counted them
// as people. The sweep keeps what such requests share, their network
// ranges and how they presented themselves, for longer than the
// addresses, so patterns can be found and the ranges handled later.

// SwarmKept is how long the swarm's rows are kept: the hourly counts'
// window, long enough to see a pattern recur.
const SwarmKept = 90 * 24 * time.Hour

// SwarmPage is a path the swarm is counted on: one that names a version
// by its hash (what a reader reaches by following a link), or one of
// the pages that read a repository's whole history and so cost the
// forge the most (on 2026-11-10 three swarm requests for a large
// mirror's activity page took it down).
var SwarmPage = regexp.MustCompile(`/[0-9a-f]{10,64}(/(diff|patch|manifest|tags))?$` +
	`|^/r/(?:[^/?]+/){1,2}(?:activity|timeline|search|history(?:/[^?]*)?|archive/[^?]*)(?:\?.*)?$`)

// hashInPath is any hash in a path, for its shape.
var hashInPath = regexp.MustCompile(`/[0-9a-f]{10,64}`)

// PathShape is a path with its hashes taken out, so requests for
// different versions of the same kind of page count together.
func PathShape(path string) string { return hashInPath.ReplaceAllString(path, "/{hash}") }

// SwarmCount is one value and how often it was seen.
type SwarmCount struct {
	Key   string `json:"key"`
	Count int64  `json:"count"`
}

// SwarmDay is one day (or hour) of the swarm.
type SwarmDay struct {
	At       time.Time `json:"at"`
	Requests int64     `json:"requests"`
	Prefixes int64     `json:"prefixes"`
	Networks int64     `json:"networks"`
}

// SwarmPrefix is one network range the swarm came from.
type SwarmPrefix struct {
	Prefix   string    `json:"prefix"`
	ASN      uint32    `json:"asn"`
	Network  string    `json:"network"`
	Country  string    `json:"country"`
	Requests int64     `json:"requests"`
	Days     int64     `json:"days"`
	First    time.Time `json:"first"`
	Last     time.Time `json:"last"`
}

// SwarmNetwork is one network the swarm came from.
type SwarmNetwork struct {
	ASN      uint32 `json:"asn"`
	Network  string `json:"network"`
	Requests int64  `json:"requests"`
	Prefixes int64  `json:"prefixes"`
}

// Swarm is what the sweep has kept, read back.
type Swarm struct {
	From      time.Time      `json:"from"`
	To        time.Time      `json:"to"`
	Requests  int64          `json:"requests"`
	Days      []SwarmDay     `json:"days"`
	Hours     []SwarmDay     `json:"hours"`
	Prefixes  []SwarmPrefix  `json:"prefixes"`
	Networks  []SwarmNetwork `json:"networks"`
	Hints     []SwarmCount   `json:"hints"`
	Langs     []SwarmCount   `json:"langs"`
	HeadSets  []SwarmCount   `json:"headSets"`
	Shapes    []SwarmCount   `json:"shapes"`
	Repos     []SwarmCount   `json:"repos"`
	Agents    []SwarmCount   `json:"agents"`
	Countries []SwarmCount   `json:"countries"`
}

// Swarm reads the swarm's rows for the days given. A forge with history
// switched off has none, and says so with a nil.
func (r *Recorder) Swarm(days int) (*Swarm, error) {
	r.mu.Lock()
	sink := r.sink
	r.mu.Unlock()
	s, ok := sink.(interface {
		Swarm(now time.Time, days int) (*Swarm, error)
	})
	if !ok {
		return nil, nil
	}
	return s.Swarm(time.Now(), days)
}