PluginProbe
404 Solution / trunk
404 Solution vtrunk
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / diagnostics / BodyDeliveryObservations.php

BodyDeliveryObservations.php in 404 Solution trunk, at includes/diagnostics/BodyDeliveryObservations.php

213 lines 8.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if (!defined('ABSPATH')) {
4 exit;
5 }
6
7 /**
8 * What the checkpoint journal actually says, as three typed maps.
9 *
10 * The boundary between a site-wide JSONL file and the emitted-against-
11 * delivered comparison built on top of it. Every record here arrived as text
12 * written by an older plugin version, a different browser, another admin tab,
13 * or a write that was cut off mid-line, so the single question this class
14 * exists to answer is which fields can be READ and which cannot -- before any
15 * of them reach a causal verdict.
16 *
17 * That question has teeth. `streamFlushReachedSapi` and
18 * `streamWhitespaceBytes` both feed
19 * ABJ_404_Solution_ResponseBodyRewriteVerdict, and each has a value that is a
20 * real, consequential observation (`false` = this host cannot stream; `0` = the
21 * step put no prefix on the wire) sitting immediately next to the value that
22 * means the record did not say. Coercion between those two is not a rounding
23 * error, it is a fabricated finding about an intermediary that did nothing,
24 * which is exactly the misreading that cost the 2026-08-27 Azure report a
25 * week. So the rules below accept only what each field is DEFINED to carry and
26 * answer null for everything else, rather than taking the nearest plausible
27 * interpretation.
28 *
29 * Pure: it takes lines rather than reading them, so a truncated record, a
30 * foreign session's receipts and a field that changed meaning between versions
31 * are all directly assertable without a journal on disk that happens to
32 * contain one.
33 */
34 final class ABJ_404_Solution_BodyDeliveryObservations {
35
36 /** The browser's per-step receipt, written by ABJ_404_Solution_Ajax_CanaryLadder. */
37 const RECEIPT_EVENT = 'canary_step_client_receipt';
38
39 /** The post-encode size record written by ABJ_404_Solution_AjaxResponseEmitter. */
40 const ENCODE_EVENT = 'json_encode';
41
42 /** The `stream` step's own flush findings, written by ABJ_404_Solution_AjaxCanaryStepRunner. */
43 const STREAM_FLUSH_EVENT = 'canary_stream_flush_outcome';
44
45 /**
46 * The latest receipt this session's browser filed for each ladder step.
47 *
48 * Latest wins: a step the client retried reports twice, and the run the
49 * `interpret` step is closing is the later one.
50 *
51 * @param array<int, string> $lines
52 * @return array<string, array{request_id: string, delivered_bytes: int|null,
53 * resource_timing_state: string}>
54 */
55 public static function receiptsInSession(array $lines, string $sessionKey): array {
56 $receipts = array();
57 foreach ($lines as $line) {
58 if ($sessionKey === '' || strpos($line, self::RECEIPT_EVENT) === false) {
59 continue;
60 }
61 $record = json_decode($line, true);
62 if (!is_array($record) || ($record['event'] ?? '') !== self::RECEIPT_EVENT
63 || self::scalarField($record, 'session_key') !== $sessionKey) {
64 continue;
65 }
66 $step = self::scalarField($record, 'step');
67 $requestId = ABJ_404_Solution_AjaxRequestLedger::normalizeId(
68 $record['step_request_id'] ?? null, '');
69 if ($step === '' || $requestId === ''
70 || !in_array($step, ABJ_404_Solution_CanaryLadderStep::DISPATCHED, true)) {
71 continue;
72 }
73 $receipts[$step] = array(
74 'request_id' => $requestId,
75 'delivered_bytes' => ABJ_404_Solution_MeasuredBodyBytes::disclosed($record['decoded_body_bytes'] ?? null),
76 'resource_timing_state' => self::scalarField($record, 'resource_timing_state'),
77 );
78 }
79 return $receipts;
80 }
81
82 /**
83 * Encoded response size by request id, latest write per id.
84 *
85 * @param array<int, string> $lines
86 * @return array<string, int>
87 */
88 public static function encodedSizesIn(array $lines): array {
89 $sizes = array();
90 foreach ($lines as $line) {
91 if (strpos($line, '"event":"' . self::ENCODE_EVENT . '"') === false) {
92 continue;
93 }
94 $record = json_decode($line, true);
95 if (!is_array($record) || ($record['event'] ?? '') !== self::ENCODE_EVENT) {
96 continue;
97 }
98 $requestId = self::scalarField($record, 'request_id');
99 $bytes = ABJ_404_Solution_MeasuredBodyBytes::disclosed($record['bytes'] ?? null);
100 if ($requestId === '' || $bytes === null) {
101 continue;
102 }
103 $sizes[$requestId] = $bytes;
104 }
105 return $sizes;
106 }
107
108 /**
109 * The `stream` step's own flush findings by request id.
110 *
111 * Both fields matter to a different consumer: `whitespace_bytes` corrects
112 * the emitted count, and `reached_sapi` is what lets
113 * `streamingBufferCausal` require that the streaming step actually
114 * streamed instead of assuming it did.
115 *
116 * @param array<int, string> $lines
117 * @return array<string, array{reached_sapi: bool|null, whitespace_bytes: int|null, session_key: string}>
118 */
119 public static function streamFlushOutcomesIn(array $lines): array {
120 $outcomes = array();
121 foreach ($lines as $line) {
122 if (strpos($line, self::STREAM_FLUSH_EVENT) === false) {
123 continue;
124 }
125 $record = json_decode($line, true);
126 if (!is_array($record) || ($record['event'] ?? '') !== self::STREAM_FLUSH_EVENT) {
127 continue;
128 }
129 $requestId = self::scalarField($record, 'request_id');
130 if ($requestId === '') {
131 continue;
132 }
133 // Absent or unreadable fields become NULL, never false and never
134 // 0. Both feed causal verdicts, so collapsing "the record does not
135 // say" into "the flush did not reach the SAPI", or into "no
136 // whitespace was emitted", manufactures an observation out of a
137 // record this code could not read. An understated emitted count in
138 // particular is the exact signature this class reports as a body
139 // rewritten in transit.
140 $outcomes[$requestId] = array(
141 'reached_sapi' => self::flushReachedSapi(
142 $record['streamFlushReachedSapi'] ?? null),
143 'whitespace_bytes' => self::whitespaceByteCount(
144 $record['streamWhitespaceBytes'] ?? null),
145 'session_key' => self::scalarField($record, 'session_key'),
146 );
147 }
148 return $outcomes;
149 }
150
151 /**
152 * Did the flush reach the SAPI, or does the record not readably say?
153 *
154 * Only the values the field is DEFINED to carry are answers: a real
155 * boolean, or 0/1 as some JSON encoders render one. Testing `=== 1` over
156 * anything numeric is not a guard, because it answers `false` for every
157 * other number -- a 2 from a field that changed meaning, a -1 sentinel, a
158 * truncated write -- and `false` here is the positive claim that the host
159 * could not stream. That is the same manufactured observation the absence
160 * check above exists to prevent, arriving by a different route.
161 *
162 * @param mixed $value
163 */
164 private static function flushReachedSapi($value): ?bool {
165 if (is_bool($value)) {
166 return $value;
167 }
168 if ($value === 1 || $value === '1') {
169 return true;
170 }
171 if ($value === 0 || $value === '0') {
172 return false;
173 }
174 return null;
175 }
176
177 /**
178 * How many whitespace bytes the `stream` step put on the wire ahead of the
179 * JSON, or null when the record does not readably say.
180 *
181 * A negative value is refused rather than clamped to zero. Zero is a real
182 * and consequential observation -- the step emitted no prefix, so the
183 * encoded size IS the whole body -- and reaching it by clamping a sentinel
184 * understates the emitted total by the prefix length. Understated emitted
185 * against a real delivered count is precisely what this class reports as a
186 * body rewritten in transit, so the clamp could invent an intermediary out
187 * of a number the journal never meant as a count. Fractions are refused
188 * for the same reason: a byte count that is not an integer did not come
189 * from the counter this field is fed by.
190 *
191 * @param mixed $value
192 */
193 private static function whitespaceByteCount($value): ?int {
194 if (is_int($value)) {
195 return $value >= 0 ? $value : null;
196 }
197 if (is_string($value) && preg_match('/^[0-9]+$/', $value) === 1) {
198 return (int)$value;
199 }
200 return null;
201 }
202
203 /**
204 * One record field as a string, or '' when it is absent or not scalar.
205 *
206 * @param array<array-key, mixed> $record
207 */
208 private static function scalarField(array $record, string $field): string {
209 $value = $record[$field] ?? null;
210 return is_scalar($value) ? (string)$value : '';
211 }
212 }
213