Screenshots are a second render

An engineer who cannot reproduce a ticket starts doubting the reporter. Read how the picture was taken first: a page cannot photograph itself.

Two facing glass mirror slabs hold a smaller inner screen, glowing orange inside the left slab

An engineer who cannot reproduce a ticket looks at the screenshot again, sees a page that does not match the app, and quietly concludes the reporter was on something else. A browser gives a page no way to photograph itself, so that picture may have been drawn a second time, by a renderer forbidden to load anything.

A browser will not let a page photograph itself

No API hands a script the pixels its own document is showing without asking a person first, and none captures a single element at all. A page that wants a picture of itself has two routes, and they cost different things.

RouteWhat it needs from the reporterWhat it capturesWhat it cannot do
A frame from the compositorA permission prompt, accepted, in a browser that offers screen captureThe pixels the screen was showing, canvas and video and embedded frames includedExist on a phone, or survive a declined prompt
A second renderNothingThe page rebuilt from its own structure, then drawn again inside an imageRequest a single resource

The right-hand column is where the differences come from. A second render clones the visible document, reuses the CSS the browser has already parsed, and hands the result over as an SVG image that is drawn onto a canvas. On the way through, every style value carrying a URL is stripped and every rule that would pull a file is skipped, so the render triggers no font request and no image request. The capture issues no network call at all, which is a written contract rather than an accident, and it is where the differences begin.

The walk is bounded on the same principle. One capture visits at most 5,000 nodes and finishes inside a 5 second deadline. The image is bounded twice over, by a 1,920 pixel long edge and by a raster budget of 1.6 million pixels, which is usually the one that binds first, and it never rasterizes above a device pixel ratio of 2.

A rebuilt picture says so on the ticket

The caveat travels attached to the image rather than sitting in a settings page nobody opens. A picture the widget’s composer rebuilt arrives labelled as rebuilt from the page structure, with a plain warning that it can differ from the live app, followed by the kinds of content this particular capture could not reproduce, written in a reviewer’s terms instead of in the capture’s own codes. What travels with the rest of the report is documented in evidence capture.

That label changes what a reviewer does next. Two tickets showing the same wrong-looking page are two different pieces of evidence, and only one of them raises a question about the reporter’s browser.

On a phone no browser offers screen capture, so wherever the reporter can attach a file instead, the widget removes its own screenshot button rather than produce a redrawing. The device in their hand already takes a perfect picture, and giving up the button is what sends them to it. That behaviour is pinned by a test, so it is a decision rather than a side effect.

What a redrawn page cannot contain, and what to ask for instead

The omissions are named kinds, and each one implies a different next question.

What the picture could not reproduceWhy a second render cannot hold itWhat to ask for instead
Canvas and WebGLThe drawing lives in a buffer rather than in the documentA screen recording, which replay does not substitute for
VideoThe same buffer problem, with a moving target on top of itA screen recording
Embedded framesA frame is a separate document the capture may not readA report filed from inside the frame’s own page
ImagesFetching one is precisely what the capture may not do, whatever its originThe image URL, and whether it resolves for them
Web fontsA font file is a resource request, so the text is redrawn in a system familyNothing. The layout is the evidence, the typeface is decoration
Form valuesThey are never serialized, deliberatelyThe value that broke it, written into the ticket
Styles the capture could not readA stylesheet from another origin, an import, or a budget spent partway throughThe page loaded in your own browser at their viewport
Part of a very large pageThe walk stops at its node bound rather than running long on somebody else’s pageThe route, and which region they were looking at

Read the right-hand column on its own: every row turns “the screenshot is wrong” into a question somebody can answer, and none of them is “are you sure that happened”. A reviewer who reads the omission first stops spending triage on the reporter’s credibility. The first row is the one that catches people out, because a session replay shows the sequence around a canvas and never the pixels inside it.

The text moves before anything else does

Typography is what a reviewer notices first. The shallow reason is the font substitution above; the deeper one is a reasoning error about CSS that any second render can make.

getComputedStyle reports line-height as a pixel length. A stylesheet that writes line-height: 1.7, which is what nearly every stylesheet writes, gets its ratio back already multiplied by the font size of the element the question was asked about. Copy that resolved number down into a clone and a 30px heading inherits the line box computed for 16px body text. Headings collapse, every line under one starts higher than it should, and because the error accumulates the picture drifts further from the page the further down you read. Measured on one page before it was fixed: 21px adrift at 110px deep, 58px at 450px.

The rule that settles it is short. An inherited property has to be handed down in the form it was authored in, as a ratio, and never in the form the engine resolved it to for one particular element. That is closed here, with a regression test holding it closed. It is worth knowing anyway, because anybody rasterizing a DOM for a thumbnail, a PDF or a visual test has the same trap open in their own code, and a reviewer who has seen it stops reading a shifted heading as proof of a different build.

What this picture will not give you

The capture is the viewport, not the document: what was below the fold is not in the image, and neither is anything finer than the bounds above. Regions marked private are dropped rather than blanked, so the content under them closes up, and the geometry below a redacted block is not the geometry of the live page. On the picture of a marked element, where that matters most, the widget declines to trust its own highlight when a redaction above the crop shifted the flow, and hands over an unmarked picture rather than a mark in the wrong place. A capture that exhausts its deadline attaches nothing at all, and the report goes without a picture rather than with a torn one.

A bug living in a canvas, in a video, or inside an embedded frame will not appear in a rebuilt screenshot, and on a page where no compositor frame is available, asking the reporter to send another one returns the same picture. The weak link in that chain is rarely the reporter and usually the evidence request, which is the half a reviewer can change: read how the picture was made, then ask for the thing the picture could not hold. Steps to reproduce written from a recording beat steps argued from a redrawing, and the argument about whether somebody is misremembering never has to start.

Common questions

Why does the screenshot on a bug report look different from the live app?

A screenshot attached without a permission prompt was rebuilt from the page's own structure and drawn a second time, by a capture that is not allowed to request a single resource. Images, web fonts and canvas content are substituted rather than fetched, and the ticket names the kinds of content that were replaced.

How can I tell whether a bug report screenshot was photographed or rebuilt?

A rebuilt picture is labelled as one. The note sits beside the image itself, says it came from the page structure rather than from the screen, and lists the kinds of content it could not reproduce, written in a reviewer's terms instead of in capture codes.

Should I distrust a bug report whose screenshot does not match the app?

A mismatch is evidence about the capture before it is evidence about the reporter. Read the note on the image first: where the picture was rebuilt, the differences it declares are arithmetic, and the reporter saw an app the picture never contained.

Which bugs is a screenshot the wrong evidence for?

Anything drawn into a canvas or by WebGL, anything playing in a video element, and anything inside an embedded frame. A rebuilt picture holds their box and not their content, and session replay does not record canvas pixels either, so a screen recording is what carries them.

Does a rebuilt screenshot show what the reporter typed into a form?

Form values are never serialized into a rebuilt capture, so an input arrives with its box, its placeholder, and none of the characters typed into it. A bug that turns on one exact value needs the reporter to write that value into the ticket.

The Notarus team

The people who build Notarus. We write up the decisions behind the product when the reasoning is worth more than the changelog line.

Keep reading

Reports with the evidence attached.

Start free, one script tag.

<script src="…/widget.js" data-key="pb_pub_…">