Documentation
¶
Overview ¶
Package hardiff compares two HTTP Archive captures and reports what changed.
A HAR is a recording of what a browser or a client actually sent, and the usual reason to hold two of them is a question of the form "this worked yesterday and does not today", or "the browser gets through and my client does not". Answering that means finding the same request in both captures and looking at the fields, which is tedious by hand and is what this does.
The part that makes it work is the matching. Two captures of one flow never line up by position: requests arrive in a different order, cache busters and request ids differ, and one capture usually has entries the other does not. See Match.
Nothing here fetches anything or writes to a capture. It reads JSON and returns a description.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Key ¶
Key identifies a request across two captures: the method, and the URL with its query string removed.
Dropping the query is the whole reason this works. Matching on the full URL looks obviously right and is what makes a naive diff useless: a cache buster, a request id or a session token in the query means the same request appears under two different keys, so every one of them is reported as a request removed and an unrelated one added, and the actual difference is buried. The query is compared as a field instead, which is where a change in it belongs.
A URL that will not parse is used as it is. It is somebody's capture, not necessarily somebody's valid URL, and refusing to match is worse than matching on a slightly odd key.
Types ¶
type Content ¶
type Content struct {
Size int64 `json:"size"`
MimeType string `json:"mimeType"`
Text string `json:"text"`
Encoding string `json:"encoding"`
}
Content is a response body. Text may be base64 when Encoding says so, and may be absent entirely: plenty of exporters drop response bodies, and an absent body is not an empty one.
type Entry ¶
type Entry struct {
StartedDateTime string `json:"startedDateTime"`
Request Request `json:"request"`
Response Response `json:"response"`
}
Entry is one request and its response.
type Field ¶
Field is one difference between two matched requests.
InA and InB record presence, which is not the same question as the value. A header sent with an empty value and a header not sent at all are different requests, and telling them apart is most of what this is for.
type HAR ¶
type HAR struct {
Log struct {
Version string `json:"version"`
Entries []Entry `json:"entries"`
} `json:"log"`
}
HAR is the subset of the HAR 1.2 format this package reads.
Deliberately not the whole spec. Timings, cache entries, page groupings and the creator block say nothing about what was sent, and parsing fields nobody compares is a way to fail on a capture that would otherwise have worked. Unknown fields are ignored rather than rejected, because exporters disagree about the optional half of the format.
func Parse ¶
Parse reads a capture from a reader.
A file that is JSON but is not a HAR is rejected by name rather than coming back as an archive with no entries. An empty result is the same shape as a capture that recorded nothing, and those two need opposite responses from whoever is reading the output.
type NameValue ¶
NameValue is a header, a query parameter or a cookie. All three are the same shape in a HAR.
type Options ¶
type Options struct {
// IgnoreHeaders names headers to leave out, case-insensitively. Nothing is
// ignored by default.
//
// The temptation is to ship a list of headers that "always differ", and it
// is worth resisting. Cookies, tokens and user agents differ between two
// captures precisely when the difference is the answer: one capture being
// signed in and the other not is exactly the sort of thing people spend a
// day failing to notice. Hiding it by default would be the tool making that
// mistake on the reader's behalf.
IgnoreHeaders []string
// ResponseHeaders compares the response headers too. Off by default,
// because they carry a date, a request id and a cache state on almost every
// response, and a diff of two captures is usually a question about what was
// sent.
ResponseHeaders bool
// ResponseBodies compares response bodies. Off by default: they are large,
// often absent from an export, and rarely the question.
ResponseBodies bool
// Redact reports which fields differ without printing either value, so the
// result can be pasted somewhere. Captures are full of credentials and a
// diff of two of them is mostly credentials.
Redact bool
// MaxValue truncates a printed value. 0 means 120 characters. Bodies and
// tokens run to kilobytes and a diff nobody can read is not an answer.
MaxValue int
// Hosts narrows the comparison to requests made to these hosts. Empty means
// every host.
//
// A real capture is mostly other people's traffic. Measured on two 5 MB
// recordings of one checkout flow: 106 requests differed, and payment
// iframes, consent banners, analytics and upsell widgets were more than a
// third of them. They differ on every field carrying a session id and none
// of them is ever the question.
//
// A host matches exactly, or as a parent domain on a dot boundary, so
// "example.com" also takes "checkout.example.com" but not "notexample.com".
Hosts []string
}
Options controls what Diff compares and how much of a value it prints. The zero value is a sensible default.
type Pair ¶
type Pair struct {
Key string
A *Entry
B *Entry
// AIndex and BIndex are positions in the original entry lists, so a
// finding can be pointed back at a line in the capture. They are -1 on the
// side where the request is absent.
AIndex int
BIndex int
}
Pair is one request found in both captures, or in only one of them. A nil A means it appears only in B, and the other way round.
func Match ¶
Match lines up the entries of two captures.
Requests sharing a key are paired in the order they were recorded, first with first. That is a guess, and it is the right one: a page that calls the same endpoint several times does so in a stable order, and the alternative, some similarity score over bodies, would pair requests differently depending on their contents and make the output move around for reasons the reader cannot see.
The result is ordered by where the request appears in A, and requests only in B come last, so reading it top to bottom follows the first capture.
type PostData ¶
type PostData struct {
MimeType string `json:"mimeType"`
Text string `json:"text"`
Params []NameValue `json:"params"`
}
PostData is a request body.
type Request ¶
type Request struct {
Method string `json:"method"`
URL string `json:"url"`
HTTPVersion string `json:"httpVersion"`
Headers []NameValue `json:"headers"`
QueryString []NameValue `json:"queryString"`
Cookies []NameValue `json:"cookies"`
PostData *PostData `json:"postData"`
}
Request is what went out.
type Response ¶
type Response struct {
Status int `json:"status"`
StatusText string `json:"statusText"`
HTTPVersion string `json:"httpVersion"`
Headers []NameValue `json:"headers"`
Content *Content `json:"content"`
}
Response is what came back.
type Result ¶
type Result struct {
OnlyInA []Pair
OnlyInB []Pair
Changed []Changed
// Same counts the matched requests with no differences, so a run that finds
// nothing can say how much it looked at. "No differences" and "nothing was
// compared" print the same way otherwise, and they are opposite answers.
Same int
}
Result is what Diff found.
func (Result) Render ¶
Render writes the result as text, with nameA and nameB used to label the two sides.
Requests are grouped under the three questions someone actually has: what did the first capture do that the second did not, what did the second do that the first did not, and where they did the same thing, what was different about it.
type Section ¶
type Section string
Section says which part of a request or response a difference is in.
const ( SectionQuery Section = "query" SectionHeader Section = "header" SectionBody Section = "body" SectionStatus Section = "status" SectionRespHead Section = "response header" SectionRespBody Section = "response body" SectionHTTPVer Section = "http version" SectionMimeType Section = "content type" SectionBodyBytes Section = "body size" )