| 1 |
<?php |
| 2 |
|
| 3 |
if (!defined('ABSPATH')) { |
| 4 |
exit; |
| 5 |
} |
| 6 |
|
| 7 |
/** |
| 8 |
* How many bytes of an admin-table response actually REACHED the browser. |
| 9 |
* |
| 10 |
* The exact mirror of ABJ_404_Solution_EncodedTableResponseSize, which answers |
| 11 |
* the same question about the other end of the same response: that class says |
| 12 |
* how many bytes the server encoded, this one says how many the browser |
| 13 |
* measured arriving. Two modules rather than one because the two numbers come |
| 14 |
* from different subsystems, different record kinds and different journals' |
| 15 |
* worth of assumptions, and because either can be present without the other -- |
| 16 |
* which is the whole finding when they disagree. Joining them is |
| 17 |
* ABJ_404_Solution_ResponseBodyDeliveryEvidence's job, not this class's. |
| 18 |
* |
| 19 |
* Read ONLY from Resource Timing's `decodedBodySize`, never from the client |
| 20 |
* report's own `bytes` field. That field is `responseText.length`, a UTF-16 |
| 21 |
* CHARACTER count, and the table payload carries URLs that are not guaranteed |
| 22 |
* ASCII -- comparing a character count against the server's octet count would |
| 23 |
* manufacture a rewritten-in-transit verdict out of an accented redirect. |
| 24 |
* `rt` is also the first field the client trims out of an oversized report |
| 25 |
* (view_updater_transport_telemetry_delivery.js TRIM_FIELD_ORDER), so its |
| 26 |
* absence is ordinary and stays a named absence rather than a number. |
| 27 |
*/ |
| 28 |
final class ABJ_404_Solution_DeliveredTableResponseSize { |
| 29 |
|
| 30 |
/** |
| 31 |
* What the browser said one table request weighed, or a named absence. |
| 32 |
* |
| 33 |
* Pure: it takes journal lines rather than reading them, so a report with |
| 34 |
* no timing entry, a trimmed report, and a report about a different |
| 35 |
* attempt are all directly assertable. |
| 36 |
* |
| 37 |
* The attempt a report is ABOUT is resolved through |
| 38 |
* ABJ_404_Solution_DiagnosticClientVerdict, the one extractor that already |
| 39 |
* maps a browser report to the journal key its attempt was recorded under. |
| 40 |
* Deriving a second keying here is exactly the defect that made the |
| 41 |
* browser's verdicts land on orphan placeholder groups for a whole |
| 42 |
* release. |
| 43 |
* |
| 44 |
* @param array<int, string> $lines Checkpoint JSONL lines, oldest first. |
| 45 |
* @param string $requestId Normalized ledger id of the table attempt. |
| 46 |
* @return array{bytes: int|null, source: string, resource_timing_state: string} |
| 47 |
*/ |
| 48 |
public static function forRequest(array $lines, string $requestId): array { |
| 49 |
$result = array( |
| 50 |
'bytes' => null, |
| 51 |
'source' => ABJ_404_Solution_MeasuredBodyBytes::SOURCE_UNAVAILABLE, |
| 52 |
'resource_timing_state' => ABJ_404_Solution_MeasuredBodyBytes::SOURCE_UNAVAILABLE, |
| 53 |
); |
| 54 |
if ($requestId === '') { |
| 55 |
return $result; |
| 56 |
} |
| 57 |
foreach ($lines as $line) { |
| 58 |
// Lines that cannot possibly match are rejected before the JSON |
| 59 |
// decoder sees them: this pass runs over whole journals. |
| 60 |
if (strpos($line, ABJ_404_Solution_DiagnosticClientVerdict::PRIOR_ATTEMPT_EVENT) === false) { |
| 61 |
continue; |
| 62 |
} |
| 63 |
$record = json_decode($line, true); |
| 64 |
if (!is_array($record) |
| 65 |
|| ABJ_404_Solution_DiagnosticClientVerdict::reportedOutcome($record)['id'] !== $requestId) { |
| 66 |
continue; |
| 67 |
} |
| 68 |
// Latest report wins: a retried attempt is reported more than once |
| 69 |
// and the later report describes the later delivery. |
| 70 |
$report = is_array($record['report'] ?? null) ? $record['report'] : array(); |
| 71 |
$timing = is_array($report['rt'] ?? null) ? $report['rt'] : array(); |
| 72 |
$bytes = ABJ_404_Solution_MeasuredBodyBytes::disclosed($timing['decodedBodySize'] ?? null); |
| 73 |
$state = $report['rtState'] ?? null; |
| 74 |
$result = array( |
| 75 |
'bytes' => $bytes, |
| 76 |
'source' => $bytes === null ? ABJ_404_Solution_MeasuredBodyBytes::SOURCE_UNAVAILABLE : ABJ_404_Solution_MeasuredBodyBytes::SOURCE_RESOURCE_TIMING, |
| 77 |
'resource_timing_state' => is_scalar($state) |
| 78 |
? (string)$state : ABJ_404_Solution_MeasuredBodyBytes::SOURCE_UNAVAILABLE, |
| 79 |
); |
| 80 |
} |
| 81 |
return $result; |
| 82 |
} |
| 83 |
|
| 84 |
} |
| 85 |
|