Private/WebsiteNetLog.ps1

# Reading a net log: what a page did, from the file a Chromium browser wrote while it loaded it.
#
# Pure, every function of it: text in, a record of the page out. The Website Kind's Gatherer
# hands it the net log of the browser it started; the tests hand it fixtures trimmed from
# real net logs. Kept apart from the Kind, as BrowserProfile.ps1 is from the browser Kinds,
# because it changes for a different reason: when Chromium writes its net log differently.

# Which sources of a net log lie beneath which: a request, the job that finds it a stream,
# the stream's jobs, what those connect with, and the sockets and name lookups at the
# bottom. The parse follows a source dependency only downwards in this order. Upwards
# would lead from a shared socket back to every other request that used it, Edge's own
# among them. Names, not numbers: the numbers are the document's (see the constants).
$script:WebsiteSourceLayer = @{
    URL_REQUEST                  = 0
    HTTP_STREAM_JOB_CONTROLLER   = 1
    HTTP_STREAM_JOB              = 2
    TRANSPORT_CONNECT_JOB        = 3
    TCP_CONNECT_JOB              = 3
    SSL_CONNECT_JOB              = 3
    SOCKS_CONNECT_JOB            = 3
    HTTP_PROXY_CONNECT_JOB       = 3
    QUIC_SESSION_POOL_DIRECT_JOB = 3
    QUIC_SESSION_POOL_PROXY_JOB  = 3
    QUIC_SESSION_POOL_JOB        = 3
    HTTP2_SESSION                = 3
    QUIC_SESSION                 = 3
    SOCKET                       = 4
    HOST_RESOLVER_IMPL_JOB       = 4
}

function ConvertFrom-WebsiteCertificate {
    <#
    .SYNOPSIS
        Who a certificate is for, who issued it and how long it is valid, from the PEM
        text a net log holds. Nothing for text that is no certificate. Pure.
    .DESCRIPTION
        The names are those of its subject alternative names, where it has them, and else
        the one in its subject: the names a browser holds the address against.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param([AllowNull()][AllowEmptyString()][string]$Pem)

    $body = ($Pem -replace '-----(BEGIN|END) CERTIFICATE-----', '' -replace '\s', '')
    if (-not $body) { return $null }
    try {
        $certificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList (, [Convert]::FromBase64String($body))
        # The extension's text is written in the language of Windows ("DNS-Name=" or
        # "DNS Name="); what stands after each "=" is the name either way.
        $alternative = @($certificate.Extensions | Where-Object { $_.Oid.Value -eq '2.5.29.17' } |
            ForEach-Object { [regex]::Matches($_.Format($false), '=\s*([^,\r\n]+)') | ForEach-Object { $_.Groups[1].Value.Trim() } })
        if (-not $alternative.Count) { $alternative = @($certificate.GetNameInfo([System.Security.Cryptography.X509Certificates.X509NameType]::SimpleName, $false)) }
        [pscustomobject]@{
            Subject   = $certificate.Subject
            Issuer    = $certificate.Issuer
            NotBefore = $certificate.NotBefore
            NotAfter  = $certificate.NotAfter
            Names     = @($alternative | Where-Object { $_ })
        }
    }
    catch { $null }
}

function Get-WebsiteSignIn {
    <#
    .SYNOPSIS
        Whether the page asked for a sign-in, with which schemes, whether the browser was
        allowed to answer with the User's Windows account, and whether the site turned the
        answer down. Nothing for a page that asked for none. Pure.
    .DESCRIPTION
        The schemes are what the 401 answers offered. The rest is in the request's
        authentication controller, which the request names as a source dependency:
        Chromium says there whether it may send the User's own credentials to this site
        (allows_default_credentials - false where the site is not in the Local Intranet
        zone or the AuthServerAllowlist policy), and whether the site rejected what it
        sent. The credentials themselves are never in a net log of the default mode.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param([Parameter(Mandatory)]$Request, [Parameter(Mandatory)][hashtable]$BySource,
          [Parameter(Mandatory)][hashtable]$TypeName, [Parameter(Mandatory)][hashtable]$SourceName)

    if (-not $Request.Challenges.Count) { return $null }
    $allowed = $null
    $rejected = $false
    $controllers = @($Request.Dependencies | Where-Object { $SourceName["$($_.type)"] -eq 'HTTP_AUTH_CONTROLLER' } |
        ForEach-Object { "$($_.id)" } | Select-Object -Unique)
    foreach ($id in $controllers) {
        if (-not $BySource.ContainsKey($id)) { continue }
        foreach ($e in $BySource[$id]) {
            switch ($TypeName["$($e.type)"]) {
                'AUTH_HANDLER_CREATE_RESULT' {
                    $may = $e.params.allows_default_credentials
                    if ($may -eq $true) { $allowed = $true }
                    elseif ($may -eq $false -and $allowed -ne $true) { $allowed = $false }
                }
                'AUTH_HANDLE_CHALLENGE' {
                    if ("$($e.params.authorization_result)" -eq 'reject') { $rejected = $true }
                }
            }
        }
    }
    [pscustomobject]@{
        PSTypeName         = 'Gutcheck.WebsiteSignIn'
        Schemes            = @($Request.Challenges)
        DefaultCredentials = $allowed
        Rejected           = $rejected
    }
}

function Get-WebsiteProxy {
    <#
    .SYNOPSIS
        The proxy the page went through and how the browser found it. Pure.
    .DESCRIPTION
        How the browser was told to find one is its proxy configuration, written to the
        net log whenever it changed: a PAC file (pac_url), automatic detection (WPAD,
        auto_detect), a fixed server (single_proxy or proxy_per_scheme), or none. The one
        in force when the page was requested counts. from_system says it came from the
        User's Windows settings; a configuration without it came from somewhere else,
        and in a Run - where Gutcheck gives the browser no proxy of its own - that is a
        policy.
 
        Each attempt to fetch a PAC file is kept with where it came from: WPAD, which
        failing only means there is none, or the PAC address, which failing is a fault.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [Parameter(Mandatory)][AllowEmptyCollection()]$Events, [Parameter(Mandatory)][hashtable]$BySource,
        [Parameter(Mandatory)][hashtable]$TypeName, [Parameter(Mandatory)][hashtable]$SourceName,
        [string]$Begin, [string]$End, [AllowNull()]$Before, [Parameter(Mandatory)][scriptblock]$NetError,
        [AllowNull()][AllowEmptyString()][string]$Used, [AllowNull()]$ResolveMs, [bool]$SignInAsked
    )

    $config = $null
    foreach ($e in $Events) {
        if ($null -eq $e -or $TypeName["$($e.type)"] -ne 'PROXY_CONFIG_CHANGED') { continue }
        if ($null -ne $Before -and $null -ne $config -and [long]$e.time -gt [long]$Before) { break }
        $config = $e.params.new_config
    }

    $fetches = @(foreach ($id in $BySource.Keys) {
        $list = $BySource[$id]
        if ($SourceName["$($list[0].source.type)"] -ne 'PAC_FILE_DECIDER') { continue }
        $from = $null; $started = $null
        foreach ($e in $list) {
            if ($TypeName["$($e.type)"] -ne 'PAC_FILE_DECIDER_FETCH_PAC_SCRIPT') { continue }
            if ("$($e.phase)" -eq $Begin) { $from = "$($e.params.source)"; $started = [long]$e.time }
            elseif ("$($e.phase)" -eq $End) {
                [pscustomobject]@{
                    Source = $from
                    Wpad   = $from -match '^WPAD'
                    Error  = & $NetError $e.params
                    Ms     = $(if ($null -ne $started) { [long]$e.time - $started })
                }
            }
        }
    })

    if ($null -eq $config -and -not $Used -and -not $fetches.Count) { return $null }

    $pac    = "$($config.pac_url)".Trim()
    $server = @(@($config.single_proxy) + @($config.proxy_per_scheme) | Where-Object { $_ } | ForEach-Object { "$_".Trim('[', ']', ' ') }) -join '; '
    $mode = 'Direct'
    if ($pac) { $mode = 'Script' }
    elseif ($config.auto_detect -eq $true) { $mode = 'Detect' }
    elseif ($server) { $mode = 'Server' }

    [pscustomobject]@{
        PSTypeName  = 'Gutcheck.WebsiteProxy'
        Mode        = $mode
        AutoDetect  = [bool]($config.auto_detect -eq $true)
        PacUrl      = $pac
        Server      = $server
        FromSystem  = $(if ($null -eq $config) { $null } else { [bool]($config.from_system -eq $true) })
        Used        = $Used
        ResolveMs   = $ResolveMs
        PacFetches  = $fetches
        SignInAsked = $SignInAsked
    }
}

function Read-WebsiteNetLogDocument {
    <#
    .SYNOPSIS
        A net log's text as a document, mended where it was cut off. Pure.
    .DESCRIPTION
        A browser writes its net log one event a line and closes the brackets when it
        exits. One that was stopped - which is what the time limit does to a page that
        hangs - leaves the last line half written and no closing brackets. Such a log is
        read up to its last whole event, and says that it was cut off.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param([AllowNull()][AllowEmptyString()][string]$Text)

    $none = [pscustomobject]@{ Document = $null; CutOff = $false }
    if (-not "$Text".Trim()) { return $none }

    try { return [pscustomobject]@{ Document = ($Text | ConvertFrom-Json -ErrorAction Stop); CutOff = $false } }
    catch { }

    $lines = $Text -split "`r?`n"
    $head = -1
    for ($i = 0; $i -lt $lines.Count; $i++) {
        if ($lines[$i] -match '^\s*"events"\s*:\s*\[\s*$') { $head = $i; break }
    }
    if ($head -lt 1) { return $none }

    $kept = New-Object System.Collections.Generic.List[string]
    for ($i = $head + 1; $i -lt $lines.Count; $i++) {
        $line = $lines[$i].Trim()
        if (-not $line) { continue }
        if (-not $line.StartsWith('{')) { break }
        $kept.Add($line.TrimEnd(','))
    }
    # The last line is the one the stop may have cut. Read on its own it is cheap to try,
    # where trying the whole document again costs seconds on a large one.
    while ($kept.Count) {
        try { $null = $kept[$kept.Count - 1] | ConvertFrom-Json -ErrorAction Stop; break }
        catch { $kept.RemoveAt($kept.Count - 1) }
    }

    $mended = (($lines[0..$head]) -join "`n") + "`n" + ($kept -join ",`n") + "`n]}"
    try { return [pscustomobject]@{ Document = ($mended | ConvertFrom-Json -ErrorAction Stop); CutOff = $true } }
    catch { return $none }
}

function ConvertTo-WebsiteHostName {
    <#
    .SYNOPSIS
        The host in the forms a net log writes it - "https://host", "host:443", a whole
        URL - as the bare lowercase name. Pure.
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param([AllowNull()][AllowEmptyString()][string]$Value)

    $text = "$Value".Trim()
    if (-not $text) { return '' }
    $uri = $null
    if ($text -match '^[a-z][a-z0-9+.-]*://' -and [uri]::TryCreate($text, [UriKind]::Absolute, [ref]$uri)) {
        return $uri.Host.ToLowerInvariant()
    }
    ($text -replace ':\d+$', '').Trim('[', ']').ToLowerInvariant()
}

function ConvertFrom-WebsiteNetLog {
    <#
    .SYNOPSIS
        Turns a net log a browser wrote into what the page did: whether it was answered,
        where it ended, and what happened at every host it needed. Pure.
    .DESCRIPTION
        Which requests are the page's: the navigation to the address (Chromium marks it
        "main frame") and every request the page started that carries the page's network
        isolation key - the site of the page, which Chromium partitions everything a page
        loads by. Edge's own requests carry its own key or none, or say that no origin
        started them, and are left out however many of them there are. The net log has no
        dependency from a navigation to what the page then loads; this key is the link.
 
        What happened at a host is read through the source dependencies, from each of the
        page's requests down to the stream it got, the connection that stream used and
        the name lookup and socket under it - downwards only, see WebsiteSourceLayer.
        Racing attempts are normal (Chromium tries QUIC and TCP at once and drops the
        loser), so a host counts as resolved, connected or encrypted when any attempt
        was, and as failed at that step only when none was.
 
        Event types, source types, phases and net error names are read from the
        document's own constants: their numbers change between browser versions.
 
        Problem is 'Unreadable' for a document that is no net log, and 'NoPage' for one
        in which the page was never requested.
    .PARAMETER Url
        The address the browser was asked to load. Picks the navigation where there were
        several; without it, the first is the page.
    #>

    [CmdletBinding()]
    [OutputType([psobject])]
    param(
        [AllowNull()][AllowEmptyString()][string]$Text,
        [AllowNull()][AllowEmptyString()][string]$Url
    )

    $read = Read-WebsiteNetLogDocument -Text $Text
    $record = [ordered]@{
        PSTypeName  = 'Gutcheck.WebsitePage'
        Problem     = $null
        CutOff      = [bool]$read.CutOff
        CaptureMode = $null
        Url         = $null
        FinalUrl    = $null
        Redirects   = @()
        Status      = $null
        Finished    = $false
        Error       = $null
        ElapsedMs   = $null
        Requests    = @()
        Hosts       = @()
        PageHosts   = @()
        SignIn      = $null
        Proxy       = $null
        Timings     = $null
    }

    $document  = $read.Document
    $constants = Get-DataProperty $document 'constants'
    $events    = @(Get-DataProperty $document 'events')
    $eventType = Get-DataProperty $constants 'logEventTypes'
    $source    = Get-DataProperty $constants 'logSourceType'
    $phase     = Get-DataProperty $constants 'logEventPhase'
    if (-not $eventType -or -not $source -or -not $phase -or -not $events.Count) {
        $record.Problem = 'Unreadable'
        return [pscustomobject]$record
    }
    $record.CaptureMode = "$(Get-DataProperty $constants 'logCaptureMode')"

    # Number to name, for the three tables read by name.
    $typeName = @{};   foreach ($p in $eventType.PSObject.Properties) { $typeName["$($p.Value)"] = $p.Name }
    $sourceName = @{}; foreach ($p in $source.PSObject.Properties)    { $sourceName["$($p.Value)"] = $p.Name }
    $errorName = @{}
    $errors = Get-DataProperty $constants 'netError'
    if ($errors) { foreach ($p in $errors.PSObject.Properties) { $errorName["$($p.Value)"] = $p.Name } }
    $begin = "$(Get-DataProperty $phase 'PHASE_BEGIN')"
    $end   = "$(Get-DataProperty $phase 'PHASE_END')"

    $netError = {
        param($Params)
        $code = $Params.net_error
        if ($null -eq $code) { return $null }
        $name = $errorName["$code"]
        [pscustomobject]@{ Name = $(if ($name) { $name } else { $null }); Code = [long]$code }
    }

    # Every event under its source, in the order written, which is the order things happened.
    $bySource = @{}
    foreach ($e in $events) {
        if ($null -eq $e -or $null -eq $e.source) { continue }
        $id = "$($e.source.id)"
        if (-not $bySource.ContainsKey($id)) { $bySource[$id] = New-Object System.Collections.Generic.List[psobject] }
        $bySource[$id].Add($e)
    }

    # The requests, each read once.
    $requests = New-Object System.Collections.Generic.List[psobject]
    $typed = $false
    foreach ($id in $bySource.Keys) {
        $list = $bySource[$id]
        if ($sourceName["$($list[0].source.type)"] -ne 'URL_REQUEST') { continue }
        $r = [ordered]@{
            Id = $id; Urls = New-Object System.Collections.Generic.List[string]; Sites = @(); Initiator = $null
            MainFrame = $false; Status = $null; Redirects = New-Object System.Collections.Generic.List[psobject]
            Error = $null; Finished = $false; Start = $null; End = $null; Dependencies = New-Object System.Collections.Generic.List[psobject]
            Challenges = New-Object System.Collections.Generic.List[string]; ProxyAuth = $false; ReadBegin = $null; WaitMs = $null
        }
        foreach ($e in $list) {
            $name = $typeName["$($e.type)"]
            $at   = "$($e.phase)"
            $p    = $e.params
            switch ($name) {
                'REQUEST_ALIVE' {
                    if ($at -eq $begin) {
                        $r.Start = [long]$e.time
                        $first = "$($p.url)"
                        if ($first -and -not $r.Urls.Count) { $r.Urls.Add($first) }
                    }
                    elseif ($at -eq $end) {
                        $r.Finished = $true
                        $r.End = [long]$e.time
                        $failed = & $netError $p
                        if ($failed) { $r.Error = $failed }
                    }
                }
                'URL_REQUEST_START_JOB' {
                    if ($at -eq $begin) {
                        $hop = "$($p.url)"
                        if ($hop -and (-not $r.Urls.Count -or $r.Urls[$r.Urls.Count - 1] -ne $hop)) { $r.Urls.Add($hop) }
                        $kind = $p.request_type
                        if ($null -ne $kind) { $typed = $true }
                        if ("$kind" -eq 'main frame') { $r.MainFrame = $true }
                        $key = "$($p.network_isolation_key)".Trim()
                        if ($key) { $r.Sites = @($r.Sites) + @(($key -split '\s+')[0]) }
                        if (-not $r.Initiator) { $r.Initiator = "$($p.initiator)" }
                        $r.Status = $null
                    }
                    elseif ($at -eq $end) {
                        $failed = & $netError $p
                        if ($failed -and -not $r.Error) { $r.Error = $failed }
                    }
                }
                { $_ -in 'HTTP_TRANSACTION_READ_RESPONSE_HEADERS', 'URL_REQUEST_FAKE_RESPONSE_HEADERS_CREATED' } {
                    $line = "$(@($p.headers)[0])"
                    if ($line -match '^HTTP/\S+\s+(?<code>\d{3})') { $r.Status = [int]$Matches['code'] }
                    if ($r.Status -eq 407) { $r.ProxyAuth = $true }
                    # Waiting for the answer: from the request sent to its head arriving.
                    if ($null -ne $r.ReadBegin) { $r.WaitMs = [long]$e.time - $r.ReadBegin; $r.ReadBegin = $null }
                    # What a 401 asks for: the schemes it offers, first word of each challenge.
                    if ($r.Status -eq 401) {
                        foreach ($header in @($p.headers)) {
                            if ("$header" -match '^(?i)www-authenticate:\s*(?<scheme>[^\s,]+)') {
                                $scheme = $Matches['scheme']
                                if (-not $r.Challenges.Contains($scheme)) { $r.Challenges.Add($scheme) }
                            }
                        }
                    }
                }
                'HTTP_TRANSACTION_READ_HEADERS' {
                    if ($at -eq $begin) { $r.ReadBegin = [long]$e.time }
                }
                'URL_REQUEST_REDIRECTED' {
                    $current = $null
                    if ($r.Urls.Count) { $current = $r.Urls[$r.Urls.Count - 1] }
                    $r.Redirects.Add([pscustomobject]@{ Url = $current; Status = $r.Status; Location = "$($p.location)" })
                }
            }
            $dependency = $p.source_dependency
            if ($dependency) { $r.Dependencies.Add($dependency) }
        }
        if ($r.Urls.Count) { $requests.Add([pscustomobject]$r) }
    }

    # The page: the navigation to the address asked for. A document from a browser that
    # does not say which request is a navigation falls back to the address alone.
    $wanted = ''
    $uri = $null
    if ($Url -and [uri]::TryCreate($Url.Trim(), [UriKind]::Absolute, [ref]$uri)) { $wanted = $uri.AbsoluteUri }
    $same = {
        param($Request)
        $u = $null
        $wanted -and [uri]::TryCreate($Request.Urls[0], [UriKind]::Absolute, [ref]$u) -and $u.AbsoluteUri -eq $wanted
    }
    $ordered = @($requests | Sort-Object { $_.Start })
    $main = $null
    if ($typed) {
        $navigations = @($ordered | Where-Object { $_.MainFrame })
        $main = @($navigations | Where-Object { & $same $_ }) + @($navigations) | Select-Object -First 1
    }
    else {
        $main = @($ordered | Where-Object { & $same $_ }) | Select-Object -First 1
    }
    if (-not $main) {
        $record.Problem = 'NoPage'
        return [pscustomobject]$record
    }

    # The page's site under each address it passed through, and its origins for a request
    # from a browser that writes no isolation key.
    $sites = @($main.Sites | Where-Object { $_ } | Select-Object -Unique)
    $origins = @($main.Urls | ForEach-Object {
        $u = $null
        if ([uri]::TryCreate($_, [UriKind]::Absolute, [ref]$u)) { $u.GetLeftPart([UriPartial]::Authority) }
    })
    # The key alone is not enough: Edge sends its own services requests under the page's
    # key while the page is open (its form-filling service, observed 2026-10-14), and
    # those say they were started by no origin. What the page asked for names the page.
    $mine = @($ordered | Where-Object {
        $_ -eq $main -or
        (@($_.Sites | Where-Object { $sites -contains $_ }).Count -and $_.Initiator -ne 'not an origin') -or
        (-not @($_.Sites).Count -and $origins -contains $_.Initiator)
    })

    # What happened at each host, walked from the page's requests. A source is walked once
    # for the whole page: a connection two requests shared is one connection.
    $hosts = [ordered]@{}
    $hostOf = {
        param([string]$Name)
        if (-not $hosts.Contains($Name)) {
            $hosts[$Name] = [ordered]@{
                Host = $Name; Requests = 0; Unfinished = 0
                Failures = New-Object System.Collections.Generic.List[psobject]
                Addresses = New-Object System.Collections.Generic.List[string]
                Resolved = $null; ResolveError = $null; Connected = $null; ConnectError = $null
                RemoteAddress = $null; Tls = $null; TlsError = $null; Certificate = $null
                DnsMs = $null; ConnectMs = $null; TlsMs = $null
            }
        }
        $hosts[$Name]
    }
    $layerOf = {
        param($Type)
        $n = $sourceName["$Type"]
        if ($n -and $script:WebsiteSourceLayer.ContainsKey($n)) { return $script:WebsiteSourceLayer[$n] }
        99
    }
    $addAddress = {
        param($Facts, [string]$Address)
        $a = ($Address -replace ':\d+$', '').Trim('[', ']')
        if ($a -and -not $Facts.Addresses.Contains($a)) { $Facts.Addresses.Add($a) }
    }

    $walked = @{}
    # What the walk learns of the proxy: how long the page's requests waited for one to be
    # chosen, which one the page itself was given, and whether a proxy asked for a sign-in.
    $resolveBegin = @{}
    # When each name lookup, connection and handshake began, so that the one that
    # succeeded first can be timed: the page went on with that one.
    $stepBegin = @{}
    $timeStep = {
        param($Facts, [string]$Name, $Key, $At)
        if (-not $stepBegin.ContainsKey($Key)) { return }
        $ms = [long]$At - $stepBegin[$Key]
        if ($null -eq $Facts[$Name] -or $ms -lt $Facts[$Name]) { $Facts[$Name] = $ms }
    }
    $resolveMs = $null
    $used = $null
    $proxySignIn = @($mine | Where-Object { $_.ProxyAuth }).Count -gt 0
    foreach ($request in $mine) {
        $queue = New-Object System.Collections.Generic.Queue[psobject]
        foreach ($d in $request.Dependencies) { $queue.Enqueue([pscustomobject]@{ Id = "$($d.id)"; Type = "$($d.type)"; Host = ''; Direct = $true }) }
        while ($queue.Count) {
            $item = $queue.Dequeue()
            if ($walked.ContainsKey($item.Id) -or -not $bySource.ContainsKey($item.Id)) { continue }
            $mineLayer = & $layerOf $item.Type
            if ($mineLayer -eq 0 -or $mineLayer -ge 99) { continue }
            $walked[$item.Id] = $true

            $context = $item.Host
            $direct  = $item.Direct
            $lookup  = ''
            foreach ($e in $bySource[$item.Id]) {
                $name = $typeName["$($e.type)"]
                $at   = "$($e.phase)"
                $p    = $e.params
                switch ($name) {
                    'HTTP_STREAM_JOB' {
                        if ($at -eq $begin) { $context = ConvertTo-WebsiteHostName "$($p.destination)" }
                    }
                    'PROXY_RESOLUTION_SERVICE' {
                        if ($at -eq $begin) { $resolveBegin[$item.Id] = [long]$e.time }
                        elseif ($at -eq $end -and $resolveBegin.ContainsKey($item.Id)) {
                            $ms = [long]$e.time - $resolveBegin[$item.Id]
                            if ($null -eq $resolveMs -or $ms -gt $resolveMs) { $resolveMs = $ms }
                        }
                    }
                    'PROXY_RESOLUTION_SERVICE_RESOLVED_PROXY_LIST' {
                        if ($request -eq $main -and -not $used) { $used = "$($p.proxy_info)" }
                    }
                    'HTTP_TRANSACTION_READ_TUNNEL_RESPONSE_HEADERS' {
                        if ("$(@($p.headers)[0])" -match '^HTTP/\S+\s+407\b') { $proxySignIn = $true }
                    }
                    'HTTP_STREAM_JOB_CONTROLLER_PROXY_SERVER_RESOLVED' {
                        $chain = "$($p.proxy_chain)"
                        $direct = (-not $chain) -or $chain -match 'direct://'
                    }
                    { $_ -in 'HOST_RESOLVER_MANAGER_JOB', 'HOST_RESOLVER_MANAGER_REQUEST' } {
                        if ($at -eq $begin) { $lookup = ConvertTo-WebsiteHostName "$($p.host)"; $stepBegin["$($item.Id)|dns"] = [long]$e.time }
                        elseif ($at -eq $end -and $context -and $lookup -eq $context) {
                            $facts = & $hostOf $context
                            $failed = & $netError $p
                            if ($failed) {
                                if ($facts.Resolved -ne $true) { $facts.Resolved = $false; $facts.ResolveError = $failed }
                            }
                            else { $facts.Resolved = $true; $facts.ResolveError = $null; & $timeStep $facts 'DnsMs' "$($item.Id)|dns" $e.time }
                        }
                    }
                    'HOST_RESOLVER_DNS_TASK' {
                        if ($at -eq $end -and $context -and $lookup -eq $context) {
                            $facts = & $hostOf $context
                            foreach ($result in @($p.results)) {
                                foreach ($endpoint in @($result.endpoints)) {
                                    & $addAddress $facts "$($endpoint.address)"
                                }
                            }
                        }
                    }
                    'TCP_CONNECT' {
                        if ($context -and $direct) {
                            $facts = & $hostOf $context
                            if ($at -eq $begin) {
                                $stepBegin["$($item.Id)|tcp"] = [long]$e.time
                                foreach ($a in @($p.address_list)) { & $addAddress $facts "$a" }
                            }
                            elseif ($at -eq $end) {
                                $failed = & $netError $p
                                if ($failed) {
                                    if ($facts.Connected -ne $true) { $facts.Connected = $false; $facts.ConnectError = $failed }
                                }
                                else {
                                    $facts.Connected = $true; $facts.ConnectError = $null
                                    & $timeStep $facts 'ConnectMs' "$($item.Id)|tcp" $e.time
                                    $remote = "$($p.remote_address)"
                                    if ($remote -and -not $facts.RemoteAddress) { $facts.RemoteAddress = $remote }
                                    # A name the page had cached is not looked up again, and
                                    # a connection to it is the proof it resolved.
                                    if ($facts.Resolved -ne $true) { $facts.Resolved = $true; $facts.ResolveError = $null }
                                }
                            }
                        }
                    }
                    'SSL_CERTIFICATES_RECEIVED' {
                        if ($context) {
                            $facts = & $hostOf $context
                            if (-not $facts.Certificate) { $facts.Certificate = ConvertFrom-WebsiteCertificate (@($p.certificates)[0]) }
                        }
                    }
                    'SSL_CONNECT' {
                        if ($at -eq $begin) { $stepBegin["$($item.Id)|tls"] = [long]$e.time }
                        if ($at -eq $end -and $context) {
                            $facts = & $hostOf $context
                            $failed = & $netError $p
                            if ($failed) {
                                if (-not $facts.Tls) { $facts.TlsError = $failed }
                            }
                            else {
                                $version = "$($p.version)"
                                if ($version) { $facts.Tls = $version; $facts.TlsError = $null; & $timeStep $facts 'TlsMs' "$($item.Id)|tls" $e.time }
                            }
                        }
                    }
                }
                $dependency = $p.source_dependency
                if ($dependency -and (& $layerOf "$($dependency.type)") -gt $mineLayer) {
                    $queue.Enqueue([pscustomobject]@{ Id = "$($dependency.id)"; Type = "$($dependency.type)"; Host = $context; Direct = $direct })
                }
            }
        }
    }

    # The requests themselves, counted at every host they passed through - a redirect to a
    # sign-in host is a request to it - and judged at the host they ended at.
    $origin = [long]$main.Start
    $record.Requests = @(foreach ($request in $mine) {
        $last = $request.Urls[$request.Urls.Count - 1]
        $name = ConvertTo-WebsiteHostName $last
        foreach ($passed in @($request.Urls | ForEach-Object { ConvertTo-WebsiteHostName $_ } | Select-Object -Unique)) {
            (& $hostOf $passed).Requests++
        }
        # Each request that ended in an error, with that error, as the net log says it.
        # Which of them count as failures is the Judge's to say: some are the browser's
        # own doing.
        $facts = & $hostOf $name
        if (-not $request.Finished) { $facts.Unfinished++ }
        elseif ($request.Error) { $facts.Failures.Add($request.Error) }
        [pscustomobject]@{
            PSTypeName = 'Gutcheck.WebsiteRequest'
            Url        = $last
            Host       = $name
            Status     = $request.Status
            Error      = $request.Error
            Finished   = $request.Finished
            StartMs    = $(if ($null -ne $request.Start) { $request.Start - $origin })
            EndMs      = $(if ($null -ne $request.End) { $request.End - $origin })
        }
    })

    $record.Url       = $main.Urls[0]
    $record.FinalUrl  = $main.Urls[$main.Urls.Count - 1]
    $record.Redirects = @($main.Redirects)
    $record.Status    = $main.Status
    $record.Finished  = $main.Finished
    $record.Error     = $main.Error
    $record.PageHosts = @($main.Urls | ForEach-Object { ConvertTo-WebsiteHostName $_ } | Select-Object -Unique)
    $record.SignIn    = Get-WebsiteSignIn -Request $main -BySource $bySource -TypeName $typeName -SourceName $sourceName
    $record.Proxy     = Get-WebsiteProxy -Events $events -BySource $bySource -TypeName $typeName -SourceName $sourceName `
        -Begin $begin -End $end -Before $main.Start -NetError $netError -Used $used -ResolveMs $resolveMs -SignInAsked $proxySignIn
    if ($main.Finished -and $null -ne $main.End -and $null -ne $main.Start) { $record.ElapsedMs = $main.End - $main.Start }
    $record.Hosts = @(foreach ($name in $hosts.Keys) {
        # Every host the walk reached, those with no request of their own too (an attempt
        # the browser raced and dropped): Requests says which.
        $facts = $hosts[$name]
        $facts['PSTypeName'] = 'Gutcheck.WebsiteHost'
        $facts.Failures  = @($facts.Failures)
        $facts.Addresses = @($facts.Addresses)
        [pscustomobject]$facts
    })

    # The page's own timings: each step at the host it ended at, the wait for its answer,
    # and the whole load to the last of its requests that finished.
    $finalFacts = $null
    $finalHost = ConvertTo-WebsiteHostName $record.FinalUrl
    if ($hosts.Contains($finalHost)) { $finalFacts = $hosts[$finalHost] }
    $ends = @($mine | Where-Object { $_.Finished -and $null -ne $_.End } | ForEach-Object { $_.End - $origin })
    $record.Timings = [pscustomobject]@{
        PSTypeName  = 'Gutcheck.WebsiteTimings'
        DnsMs       = $(if ($finalFacts) { $finalFacts['DnsMs'] })
        ConnectMs   = $(if ($finalFacts) { $finalFacts['ConnectMs'] })
        TlsMs       = $(if ($finalFacts) { $finalFacts['TlsMs'] })
        FirstByteMs = $main.WaitMs
        LoadMs      = $(if ($ends.Count) { ($ends | Measure-Object -Maximum).Maximum })
    }

    [pscustomobject]$record
}