getMessage(), 0, 200); } } /** * The verdict as a scannable header line plus one JSON record. * * Over-budget input sheds the attempt LIST -- the one reducible part -- and * then falls back to the decision alone, rather than being cut at a byte * offset: a record cut mid-JSON is unreadable by machine and misleading to a * human, which is the same failure the drained client buffer already taught * the composer (see SupportEvidenceExcerpt::appendClientTransportTelemetry). * * @param array $record ABJ_404_Solution_DetachAbEvidence::verdictForSession(). */ private static function render(array $record): string { $header = 'Detach A/B verdict -- ' . self::summary($record) . " (JSON):\n"; $reduced = $record; $reduced['attempts'] = array(); $reduced['attempts_reduced'] = 'over_budget'; $minimal = array( 'status' => self::textOf($record, 'status'), 'session_key' => self::textOf($record, 'session_key'), 'verdict' => $record['verdict'] ?? array(), 'reduced' => 'over_budget', ); foreach (array($record, $reduced, $minimal) as $candidate) { $line = json_encode(array(self::DETACH_AB_VERDICT_KEY => $candidate)); if (is_string($line) && strlen($header) + strlen($line) <= self::MAX_DETACH_AB_VERDICT_BYTES) { return $header . $line; } } return $header . 'The verdict record could not be encoded for this payload.'; } /** * The one-line version, so the first thing a reader sees is the decision and * how much evidence it was drawn from. The counts are part of the summary * rather than decoration: a verdict of 'inconclusive' over zero attempts and * one over six attempts are entirely different findings. * * @param array $record */ private static function summary(array $record): string { $verdict = isset($record['verdict']) && is_array($record['verdict']) ? $record['verdict'] : array(); // Read straight off the discriminant the rule decides. This used to // loop the boolean flags to recover the one word they encode, which // meant the renderer carried its own copy of the precedence order and // would have disagreed with the rule if either side gained an outcome. $named = isset($verdict['verdict']) && is_string($verdict['verdict']) && $verdict['verdict'] !== '' ? $verdict['verdict'] : ABJ_404_Solution_DetachAbVerdict::VERDICT_INCONCLUSIVE; return self::textOf($record, 'status') . ': ' . $named . '; ' . self::countOf($record, 'attempts_with_mode') . ' attempt(s) with a mode, ' . self::countOf($record, 'attempts_resolved') . ' resolved by the browser, ' . self::countOf($record, 'attempts_unresolved') . ' still unreported'; } /** * One record field as a string, or '' when it is absent or not scalar. * * @param array $record */ private static function textOf(array $record, string $field): string { $value = $record[$field] ?? null; return is_scalar($value) ? (string)$value : ''; } /** * One record field as an integer, or 0 when it is absent or not scalar. * * @param array $record */ private static function countOf(array $record, string $field): int { $value = $record[$field] ?? null; return is_scalar($value) ? (int)$value : 0; } }