Skip to content
quarry Artifact [8ccec09bfa]
Login

Artifact [8ccec09bfa]

Artifact 8ccec09bfab69de900d02b4760f951617a4d02784131b77107664202f41e11ea:


package web

import (
	"log"
	"sort"
	"strings"
)

// Reader traffic: who cloned and fetched a repository, with what, and
// how much of that is knowable at all.
//
// The page exists because a clone is the one kind of forge activity
// that leaves no trace in the DAG — it reads and writes nothing — so
// without a tally it is invisible. The counters behind it are written
// by the sync gate (internal/xfer) and read here.
//
// It is an ADMIN page, unlike the bare clone count on the repository's
// front page. The difference is not the number, it is the names: how
// many people cloned a public repository is public information, while
// which accounts did is the repository's business.

// trafficView is one repository's reader traffic, ready to render.
type trafficView struct {
	Repo string
	// Clones and Fetches are the running totals. A clone is a new
	// clone; a fetch is a later sync round, of which one clone
	// produces many.
	Clones, Fetches int
	// Identified is how many clones named a verified account. It is
	// stated next to the total because the ratio is the honest headline
	// — on a public repository most clones name nobody, and a page that
	// hid that would imply the opposite.
	Identified int
	// Clients is the software population, most-used first.
	Clients []trafficRow
	// Readers is the identity breakdown: verified accounts, then
	// claimed names, then the unidentified remainder. Never sorted into
	// one another — see trafficRow.Kind.
	Readers []trafficRow
	// Unidentified is the count of clones that named nobody. Carried
	// separately as well as in Readers so the empty state can speak.
	Unidentified int
}

// trafficRow is one bar: a label, a count, and a width relative to the
// largest row in its own group.
type trafficRow struct {
	Label string
	Count int
	// Pct is the bar width, 0..100, relative to the biggest row in the
	// same group. It is a SHAPE, not a share of the total: with one
	// row at 293 and the rest in single figures, shares would render
	// every real reader as an invisible sliver.
	Pct int
	// Kind is "auth", "claim" or "anon". The template shows a claimed
	// name differently and never merges it with a verified one: nothing
	// signs a pull, so a claim is free to forge, and a page that drew
	// them alike would report a forgery as a fact.
	Kind string
}

// Claimed reports a row naming somebody nothing verified.
func (r trafficRow) Claimed() bool { return r.Kind == "claim" }

// Anon reports the row standing for everyone who named nobody.
func (r trafficRow) Anon() bool { return r.Kind == "anon" }

// TallyFamilies reads a whole counter family. It is separate from
// Tallies' single-counter Total because a forge without it still
// renders every number that existed before — the traffic page is the
// only thing that needs the enumeration, and it degrades to empty.
type TallyFamilies interface {
	Totals(repo, prefix string) (map[string]int64, error)
}

// familiesOf returns one family's counters, or nothing. A counter plane
// that errors renders no breakdown rather than a wrong one: the totals
// beside it are still true.
func familiesOf(t Tallies, repo, prefix string) map[string]int64 {
	f, ok := t.(TallyFamilies)
	if !ok || t == nil {
		return nil
	}
	m, err := f.Totals(repo, prefix)
	if err != nil {
		log.Printf("web: traffic %s/%s: %v", repo, prefix, err)
		return nil
	}
	return m
}

// trafficFor assembles the panel for one repository and one verb —
// "clone" or "fetch".
func trafficFor(t Tallies, repo, verb string) trafficView {
	v := trafficView{Repo: repo}
	v.Clones = tallyOf(t, repo, "clone")
	v.Fetches = tallyOf(t, repo, "fetch")

	v.Clients = clientRows(familiesOf(t, repo, verb+".client"))

	auth := familiesOf(t, repo, verb+".auth")
	claim := familiesOf(t, repo, verb+".claim")
	anon := tallyOf(t, repo, verb+".anon")
	for _, n := range auth {
		v.Identified += int(n)
	}
	v.Unidentified = anon
	v.Readers = readerRows(auth, claim, anon)
	return v
}

// clientRows turns "zit.22900" keys into "zit 22900" bars. The two
// halves are stored in one subject because they are one fact — this
// build of this client — and split here because that is where a reader
// wants to see them.
func clientRows(m map[string]int64) []trafficRow {
	rows := make([]trafficRow, 0, len(m))
	for k, n := range m {
		kind, version, _ := strings.Cut(k, ".")
		label := kind
		// "_" is the sanitizer's stand-in for a name that was empty —
		// here, a client that sent no version pragma at all.
		if version != "" && version != "_" {
			label = kind + " " + version
		}
		rows = append(rows, trafficRow{Label: label, Count: int(n)})
	}
	return widen(rank(rows))
}

// readerRows lays the three identity states out in one list, in the
// order a reader should take them: proven names first, then asserted
// ones, then everybody who said nothing.
func readerRows(auth, claim map[string]int64, anon int) []trafficRow {
	var rows []trafficRow
	for k, n := range auth {
		rows = append(rows, trafficRow{Label: k, Count: int(n), Kind: "auth"})
	}
	rank(rows)
	var claims []trafficRow
	for k, n := range claim {
		claims = append(claims, trafficRow{Label: k, Count: int(n), Kind: "claim"})
	}
	rows = append(rows, rank(claims)...)
	if anon > 0 {
		rows = append(rows, trafficRow{Label: "unidentified", Count: anon, Kind: "anon"})
	}
	// The widths span the whole list, including the unidentified row,
	// because that row is the one that gives the others their scale.
	return widen(rows)
}

// rank orders rows by count, then by label so equal counts do not
// shuffle between renders of the same numbers. Widths are set
// separately, because a group's rows are not always ranked against the
// same set they are drawn against.
func rank(rows []trafficRow) []trafficRow {
	sort.Slice(rows, func(i, j int) bool {
		if rows[i].Count != rows[j].Count {
			return rows[i].Count > rows[j].Count
		}
		return rows[i].Label < rows[j].Label
	})
	return rows
}

// widen sets each bar relative to the largest row present.
func widen(rows []trafficRow) []trafficRow {
	most := 0
	for _, r := range rows {
		if r.Count > most {
			most = r.Count
		}
	}
	if most == 0 {
		return rows
	}
	for i := range rows {
		rows[i].Pct = rows[i].Count * 100 / most
		// A row that exists is drawn. Rounding a real reader down to a
		// zero-width bar says "none", which is a different claim.
		if rows[i].Pct == 0 && rows[i].Count > 0 {
			rows[i].Pct = 1
		}
	}
	return rows
}

// fleetTrafficRow is one repository's line on the forge-wide page.
type fleetTrafficRow struct {
	Repo                        string
	Clones, Fetches, Identified int
	// Pct is the clone bar, relative to the busiest repository.
	Pct int
}

// fleetTraffic ranks every repository by clones served.
//
// The forge admin's question is a different one from the repository
// admin's: not "who cloned mine" but "what is anyone reading at all",
// which is answered by the shape of the list rather than by any single
// row. Names stay off this page — a forge admin who wants them opens
// the repository's own traffic page, where the gate is the repository's.
func fleetTraffic(t Tallies, repos []string) []fleetTrafficRow {
	rows := make([]fleetTrafficRow, 0, len(repos))
	most := 0
	for _, name := range repos {
		row := fleetTrafficRow{
			Repo:    name,
			Clones:  tallyOf(t, name, "clone"),
			Fetches: tallyOf(t, name, "fetch"),
		}
		for _, n := range familiesOf(t, name, "clone.auth") {
			row.Identified += int(n)
		}
		if row.Clones == 0 && row.Fetches == 0 {
			// A repository nobody has read is a true row and a useless
			// one; the list is long enough without it.
			continue
		}
		if row.Clones > most {
			most = row.Clones
		}
		rows = append(rows, row)
	}
	sort.Slice(rows, func(i, j int) bool {
		if rows[i].Clones != rows[j].Clones {
			return rows[i].Clones > rows[j].Clones
		}
		if rows[i].Fetches != rows[j].Fetches {
			return rows[i].Fetches > rows[j].Fetches
		}
		return rows[i].Repo < rows[j].Repo
	})
	for i := range rows {
		if most > 0 {
			rows[i].Pct = rows[i].Clones * 100 / most
			if rows[i].Pct == 0 && rows[i].Clones > 0 {
				rows[i].Pct = 1
			}
		}
	}
	return rows
}