Documentation
¶
Overview ¶
Package render drives a headless Chromium session: it loads a page, waits for it to stop moving, sweeps the viewport through the full scroll height, and hands back a deduplicated capture of everything that was ever on screen.
Index ¶
- Variables
- func ChromiumPath(explicit string) string
- func DefaultBlockHosts() []string
- func KillBrowsers() int
- func LibraryNotes(found []string) []string
- func LibraryWeight(found []string) (float64, string)
- type Asset
- type Browser
- type CanvasShot
- type FrameProbe
- type LibrarySpec
- type NavGuard
- type Options
- type Result
- type Timing
- type Trace
Constants ¶
This section is empty.
Variables ¶
var ErrChromiumNotFound = errors.New(
"no Chromium or Chrome binary found\n" +
" sieve needs a Chromium-based browser to render pages.\n" +
" Install Google Chrome, Chromium or Microsoft Edge, or point sieve at one:\n" +
" sieve distill <url> --chrome /path/to/chrome\n" +
" SIEVE_CHROME=/path/to/chrome sieve distill <url>")
ErrChromiumNotFound is returned when no browser binary could be located. It carries instructions rather than a stack trace, because a missing Chromium is the single most common first-run failure.
var Version = "0.2.0"
Version identifies the build. It is stamped into every trace so an artifact can be tied back to the code that produced it.
A var rather than a const so a release build can stamp a commit onto it.
Functions ¶
func ChromiumPath ¶
ChromiumPath reports the browser sieve would use, for `sieve doctor`.
func DefaultBlockHosts ¶
func DefaultBlockHosts() []string
DefaultBlockHosts is the analytics, advertising and session-replay traffic that contributes nothing to a page's content and a great deal to its load time. Blocking happens at name resolution, so the cost is zero per request rather than one interception round trip per request.
Session replay tools in particular (FullStory, Hotjar, Clarity) both slow the page and record the sweep, so declining to talk to them is the courteous default as well as the fast one.
func KillBrowsers ¶
func KillBrowsers() int
KillBrowsers terminates every browser this process launched, and their children, without waiting for anything to agree.
Called from the watchdog immediately before the process exits. It is deliberately violent: whatever state the browser is in, we have already concluded it is not answering, and asking politely is the bet that just lost. It reports how many it killed so the watchdog can say so.
func LibraryNotes ¶
LibraryNotes returns the maintenance notes for the detected libraries.
func LibraryWeight ¶
LibraryWeight returns the highest weight among the detected libraries, which is what the escalation scorer consumes. The maximum rather than the sum: one scroll hijacker is already decisive, and five reveal libraries are not five times as decisive.
Types ¶
type Browser ¶
type Browser struct {
// contains filtered or unexported fields
}
Browser owns one Chromium process. Reuse it across pages: process startup is 200-600ms, which dominates the cost of a small page.
func Launch ¶
Launch starts a browser process configured for extraction rather than for browsing: no GPU rasterisation surprises, no first-run dialogs, no background networking, and name resolution for tracker hosts pointed at nothing.
func (*Browser) ChromiumVersion ¶
ChromiumVersion reports the browser build, for diagnostics and traces.
func (*Browser) Close ¶
func (b *Browser) Close()
Close shuts the browser down. It is safe to call more than once.
Cancelling the allocator asks chromedp to stop the browser and then waits for the process to actually exit. That wait has no bound, and a Chromium that has stopped responding never provides one: one corpus run reached this line after a perfectly good extraction and did not return to its caller for another fourteen minutes, with the artifact already written to disk.
So the polite shutdown gets a deadline, and past it the process tree is killed outright. Nothing is lost by doing so -- everything this browser was for has already been read out of it -- and the alternative is a command that never ends.
func (*Browser) ProbeFrameProduction ¶
func (b *Browser) ProbeFrameProduction(ctx context.Context) (FrameProbe, error)
ProbeFrameProduction verifies the least obvious precondition in the whole renderer.
A headless tab that is not being composited never runs the rendering steps. requestAnimationFrame stops firing, and so does IntersectionObserver, which is what nearly every scroll-reveal animation uses to decide when to show content. Under that condition a sweep completes quickly, reports no errors, and produces an artifact containing the hero and nothing else -- the exact failure this project exists to prevent, arriving silently.
The probe runs on a *secondary* tab on purpose. That is where the failure occurs: the first tab a browser opens composites fine, so a check that used it would pass on a machine where every real sweep is starved.
func (*Browser) SetOptions ¶
SetOptions adjusts per-sweep settings on a running browser.
The flags a Chromium process was launched with cannot change, so only the sweep-level knobs are honoured -- checkpoint counts, budgets, gates. This is what lets one warm browser serve both a single post-settle capture and a full sweep without paying process startup twice.
func (*Browser) Sweep ¶
Sweep renders one page and returns the deduplicated capture.
The shape of the work is: navigate, wait for the page to stop moving, then walk the viewport down the document taking a full extraction at each stop, folding each one into a running deduplicated set. It ends when the set stops growing, when the document ends, or when a budget runs out, whichever comes first.
type CanvasShot ¶
type CanvasShot struct {
Path string
PNG []byte
Checkpoint int
// Uniform is set when the crop is a single flat colour, which means there
// is nothing in it for a vision model to describe and the expensive path
// should be skipped.
Uniform bool
}
CanvasShot is a rasterised canvas region awaiting recovery.
type FrameProbe ¶
type FrameProbe struct {
// Ticks is how many animation frames fired within the probe window.
Ticks int `json:"ticks"`
// IO reports whether an IntersectionObserver callback was delivered.
IO bool `json:"intersection_observer"`
// Elapsed is how long the probe took.
Elapsed time.Duration `json:"elapsed"`
}
FrameProbe reports whether the browser is actually producing frames.
type LibrarySpec ¶
type LibrarySpec struct {
// Name is the identifier reported in the artifact.
Name string `json:"n"`
// Global is a dotted path on `window` whose existence proves the library is
// loaded, e.g. "gsap.ScrollTrigger".
Global string `json:"g,omitempty"`
// Selector is a CSS selector that proves it, for libraries that leave a
// marker in the DOM rather than a global.
Selector string `json:"s,omitempty"`
// Weight is how strongly this library predicts that a cheap fetch will miss
// content, from 0 to 1. A scroll hijacker is decisive; a tooltip library
// says nothing.
Weight float64 `json:"w"`
// Class groups detectors for reporting: scroll, reveal, 3d, text, router.
Class string `json:"c"`
// Note explains what breaks, and is surfaced in `sieve doctor`.
Note string `json:"note,omitempty"`
}
LibrarySpec is one detector for an animation, scroll or 3D library.
These live in a data file rather than in code on purpose. The tail of animation-library behaviours is endless and each new one is discovered by a site breaking; a contributor should be able to add a detector with a fixture and a pull request against JSON, without touching Go and without waiting for a release.
func LibrarySpecs ¶
func LibrarySpecs() ([]LibrarySpec, error)
LibrarySpecs returns the versioned detector set.
type NavGuard ¶
NavGuard vets a URL the browser is about to navigate to. It is called for the initial navigation and again for every redirect, because a URL that passed once says nothing about where it points after three hops.
Returning a non-nil error aborts that navigation. The safety package supplies the real implementation; render only defines the shape so the two packages stay independent.
type Options ¶
type Options struct {
// ChromePath is an explicit browser binary. Empty means auto-detect.
ChromePath string
// Headless runs without a visible window. Turning it off is a debugging aid.
Headless bool
// NoSandbox is required inside most containers. It weakens the browser's
// own isolation, so it is off unless asked for.
NoSandbox bool
ViewportW int
ViewportH int
DeviceScale float64
UserAgent string
// AcceptLanguage is sent on every request and also drives the page's
// navigator.languages, which some sites use to pick content.
AcceptLanguage string
// matters is whether a document arrived, not whether every straggling image
// finished.
NavTimeout time.Duration
// FirstSettle bounds the wait for entrance animations to finish before the
// first capture. It is larger than SettleTimeout because a page gets one
// chance to finish loading and settling, and capturing a hero mid-fade
// records it at whatever opacity it happened to have.
FirstSettle time.Duration
// SettleTimeout bounds the wait for animation settle after a scroll step. A
// page with a permanently looping animation never settles, so this is the
// value that actually ends the wait on such sites -- which is why it is
// small. The sweep takes many cheap looks rather than a few expensive ones.
SettleTimeout time.Duration
// SettleFloor is the shortest that wait may be compressed to when the sweep
// is rationing its remaining time across the rest of the document.
SettleFloor time.Duration
// RevealFloor is the settle wait used when text is being captured and none
// of it has ever been legible. On such a page the sweep is outrunning the
// animation, and the remedy is fewer, slower stops rather than more.
RevealFloor time.Duration
// SettleFrames is how many consecutive animation frames must show no
// layout change before the page counts as settled.
SettleFrames int
// LoadBudget bounds how long sieve will wait for a page to arrive and stop
// moving, before it starts reading.
//
// It is deliberately separate from Budget, and it is not deducted from it.
// Charging a site's own loading time to the extraction meant that a page
// with an intro film or a preloader -- exactly the class of page this tool
// exists for -- handed the sweep whatever was left, and what was left was
// often one capture of a loading screen. A budget for reading a page should
// start when there is a page to read.
LoadBudget time.Duration
// SweepBudget bounds the in-page checkpoint loop. The loop rations itself
// against this: it plans its step size and its per-checkpoint settle wait
// so that the document is covered within it.
SweepBudget time.Duration
// Budget bounds the whole render for one page, navigation included.
Budget time.Duration
// Passes is how many times the document is walked.
//
// Two is the useful default and the reason is that almost every scroll
// reveal on the web fires once and stays fired. The first pass is as much a
// trigger as a capture; the second sees at full opacity what the first
// could only catch mid-fade, and costs almost nothing to transmit because
// everything it re-observes is already known. It is a far better use of a
// second than dwelling a second longer at every checkpoint of one pass,
// because dwelling only helps the section being dwelt on.
Passes int
// StepRatio is the fraction of a viewport height advanced per checkpoint.
// Below 1.0 so that content revealed at the boundary of two viewports is
// seen fully at least once.
StepRatio float64
// MaxCheckpoints caps the sweep on pages that grow as you scroll.
MaxCheckpoints int
// StableCheckpoints is K: stop once this many consecutive checkpoints have
// added no new unique nodes.
StableCheckpoints int
// MaxScrollPx caps total distance travelled, which is the guard against an
// infinite-scroll feed.
MaxScrollPx float64
// NodeBudget caps nodes captured per checkpoint.
NodeBudget int
// LatentBudget caps hidden-content nodes per checkpoint. It is separate
// from NodeBudget so that a page with an enormous hidden menu tree cannot
// crowd out the content the reader can actually see.
LatentBudget int
// CollectCorpus retains inline JSON, hydration blobs and script string
// literals as a confirm-only index for canvas recovery. It is never a
// source of content -- see the corroborate package for why that rule is
// absolute.
CollectCorpus bool
// MaxCorpusBytes bounds that index.
MaxCorpusBytes int
// ReducedMotion emulates prefers-reduced-motion. Running a second pass with
// it on and comparing the two is the cheapest available check on whether
// the reveal machinery was understood correctly.
ReducedMotion bool
// Locale and Timezone are pinned rather than inherited from the host, so
// that the same URL rendered on two machines produces the same artifact.
Locale string
Timezone string
// BlockHosts are host patterns resolved to nothing by the browser, which
// removes analytics and ad traffic before a connection is opened rather
// than after. Wildcards are allowed: "*.doubleclick.net".
BlockHosts []string
// BlockURLPatterns are additional URLPattern-syntax rules applied by the
// browser's network layer, for path-level blocking that a host rule cannot
// express.
BlockURLPatterns []string
// CaptureCanvas enables viewport screenshots at checkpoints where a canvas
// covers at least CanvasShareGate of the viewport. Screenshots are cropped
// to the canvas and kept only for canvas recovery.
CaptureCanvas bool
// before it is worth rasterising.
CanvasShareGate float64
// CollectAssets keeps response bodies for scene-graph formats so canvas
// recovery can read node names out of them without refetching.
CollectAssets bool
// MaxAssetBytes and MaxAssetsTotal bound that collection.
MaxAssetBytes int64
MaxAssetsTotal int64
// Proxy routes browser traffic through an HTTP proxy.
Proxy string
// Logf receives progress lines. Nil discards them.
Logf func(format string, args ...any)
}
Options configures a render session. The zero value is not usable; start from DefaultOptions and adjust.
func DefaultOptions ¶
func DefaultOptions() Options
DefaultOptions returns settings tuned for design-heavy marketing and portfolio sites, which is the class of page this tool exists for.
func (*Options) ScaleTo ¶
ScaleTo fits the render's time budget inside the wall clock the caller is willing to spend on a whole page, holding back a margin for the fetch, the graph and the emit.
The sub-budgets move together and stay in proportion, because they are not independent: a navigation allowance larger than the sweep budget guarantees a page that loads slowly is never swept, and a first-settle wait that is a large fraction of the total guarantees the same. Scaling one number is also the only way `--timeout` can mean what it says. Before this, `--timeout 10m` left the sweep stopping at its own hardcoded two and a half minutes, and `--timeout 10s` left it cheerfully planning for a hundred and fifty.
type Result ¶
type Result struct {
RequestedURL string
FinalURL string
Status int64
Merged *capture.Merged
// OpenedDisclosures names the tabs and accordions sieve pressed open, so a
// reader can tell content that was on screen from content that had to be
// revealed.
OpenedDisclosures []string
// Assets holds response bodies for scene-graph formats, kept so canvas
// recovery can read them without a second fetch.
Assets []Asset
// CanvasShots maps a canvas node path to a PNG cropped to that canvas at
// the checkpoint where it filled the most of the viewport.
CanvasShots map[string]*CanvasShot
// Corpus is the confirm-only membership index over the text the site
// shipped. It is never a source of content; see the corroborate package.
Corpus *corroborate.Index
// Scene is what walking the live 3D scene graph produced, if anything.
Scene *capture.SceneIntrospection
// Libraries are the animation, scroll and 3D libraries detected in the page.
Libraries []string
// EnteredGate names an entry screen sieve pressed through, when it did.
EnteredGate string
// EntryGate names an interstitial the visitor must dismiss before the site
// begins -- the "click to enter" screen. It is empty when there is none.
//
// It matters because the alternative is silence. hatom.com sits behind one,
// and the artifact reported nine blocks with no indication that the page had
// not started: indistinguishable, to a reader, from a site that simply has
// nothing on it.
EntryGate string
// Trace is everything needed to reproduce this render.
Trace Trace
Timing Timing
// ReachedBottom reports that the sweep saw the end of the document rather
// than running out of budget on the way there.
ReachedBottom bool
// Notes records anything that limited the sweep, so the artifact can be
// honest about its own coverage instead of silently reporting a partial
// page as complete.
Notes []string
// Blocked records that the site refused this client, and how it was
// detected. Partial and honest beats empty.
Blocked bool
BlockedReason string
}
Result is everything one page yielded.
type Timing ¶
type Timing struct {
// Load is how long the page took to arrive and stop moving. It is reported
// separately because it is the site's time, not sieve's, and the extraction
// budget does not start until it has elapsed.
Load time.Duration `json:"load_ms"`
FirstSettle time.Duration `json:"first_settle_ms"`
Sweep time.Duration `json:"sweep_ms"`
Total time.Duration `json:"total_ms"`
Checkpoints int `json:"checkpoints"`
Passes int `json:"passes"`
SettleWaits int `json:"settle_waits"`
SettleMiss int `json:"settle_timeouts"`
}
Timing breaks the wall clock down so a slow site can be diagnosed without re-running with a profiler attached.
type Trace ¶
type Trace struct {
SieveVersion string `json:"sieve_version"`
CaptureHash string `json:"capture_script_sha256"`
Chromium string `json:"chromium"`
UserAgent string `json:"user_agent"`
ViewportW int `json:"viewport_w"`
ViewportH int `json:"viewport_h"`
DeviceScale float64 `json:"device_scale"`
Locale string `json:"locale"`
Timezone string `json:"timezone"`
ReducedMotion bool `json:"reduced_motion"`
StepRatio float64 `json:"step_ratio"`
SettleFrames int `json:"settle_frames"`
SettleTimeout string `json:"settle_timeout"`
FirstSettle string `json:"first_settle"`
SettleFloor string `json:"settle_floor"`
SweepBudget string `json:"sweep_budget"`
Passes int `json:"passes"`
MaxCheckpoints int `json:"max_checkpoints"`
StableCheckpts int `json:"stable_checkpoints"`
Flags []string `json:"flags"`
BlockHostsHash string `json:"block_hosts_sha256"`
}
Trace is the complete set of inputs that determined a render's output.
A trace missing any of these is not replayable, and an artifact whose trace is not replayable cannot support a bug report: the maintainer would have to reproduce against the live site, on their own machine, with their own Chromium, which is precisely the situation that makes browser-dependent projects expensive to maintain.