, records: int, outcomes: array} $clientAttempts */ public static function compose(array $clientAttempts, string $clickSessionId): string { if (!class_exists('ABJ_404_Solution_FailingSessionEvidence')) { return 'Failing-session diagnostics unavailable: ABJ_404_Solution_FailingSessionEvidence' . ' could not be loaded on this install, so per-session verdicts were not computed here.'; } try { $clientFailingIds = self::clientFailingIds($clientAttempts); $failingIds = self::failingRequestIndex(self::diagnosticSources(), $clientFailingIds); return self::render( ABJ_404_Solution_FailingSessionEvidence::forSupport( $failingIds, $clientFailingIds, $clickSessionId)); } catch (Throwable $e) { return 'Failing-session diagnostics could not be computed: ' . substr($e->getMessage(), 0, 200); } } /** * The two durable diagnostic journals' candidate paths, each guarded the * way the rest of assembly is (a corrupt install can be missing any plugin * file; see the safe-autoloader work for error 18). * * @return array}> */ private static function diagnosticSources(): array { $sources = array(); if (class_exists('ABJ_404_Solution_AjaxRequestTrace')) { $sources[] = ABJ_404_Solution_AjaxTraceJournal::supportCollectionSource(); } if (class_exists('ABJ_404_Solution_CheckpointJournalReader')) { $sources[] = ABJ_404_Solution_CheckpointJournalReader::supportCollectionSource(); } return $sources; } /** * The request ids the browser condemned in its OWN drained buffer. These * belong to the clicking tab by construction -- the browser holds only its * own tab's transport buffer -- so the evidence class can attribute an * untraced one to the click session instead of dropping it. * * @param array{status: string, ids: array, records: int, outcomes: array} $clientAttempts * @return array */ private static function clientFailingIds(array $clientAttempts): array { $outcomes = isset($clientAttempts['outcomes']) && is_array($clientAttempts['outcomes']) ? $clientAttempts['outcomes'] : array(); $failing = array(); foreach ($outcomes as $requestId => $healthy) { if ($healthy === false) { $failing[(string)$requestId] = true; } } return $failing; } /** * Every request id condemned anywhere: unioned across both journals' * failure indexes and the clicking tab's drained-buffer failures. Matches, * by construction, the index the journal excerpts rank on. * * @param array}> $sources * @param array $clientFailingIds * @return array */ private static function failingRequestIndex(array $sources, array $clientFailingIds): array { $failingIds = array(); if (class_exists('ABJ_404_Solution_DiagnosticJournalExcerpt')) { foreach ($sources as $source) { $failingIds += ABJ_404_Solution_DiagnosticJournalExcerpt::failureIndex($source['paths']); } } foreach ($clientFailingIds as $id => $present) { $failingIds[$id] = true; } return $failingIds; } /** * The record as a scannable header line plus one JSON record. * * Over-budget input sheds the per-session id lists and the unresolved list * first -- the reducible detail -- then falls back to the relationship and * counts alone, rather than being cut at a byte offset. A record cut * mid-JSON is unreadable by machine and misleading to a human. * * @param array $record ABJ_404_Solution_FailingSessionEvidence::forSupport(). */ private static function render(array $record): string { $header = 'Failing-session diagnostics -- ' . self::summary($record) . " (JSON):\n"; $reduced = $record; $reducedSessions = array(); foreach (is_array($record['sessions'] ?? null) ? $record['sessions'] : array() as $session) { if (is_array($session)) { unset($session['failing_request_ids']); $session['failing_request_ids_reduced'] = 'over_budget'; } $reducedSessions[] = $session; } $reduced['sessions'] = $reducedSessions; $reduced['unresolved_failing_request_ids'] = array(); $minimal = array( 'status' => self::textOf($record, 'status'), 'click_session_key' => self::textOf($record, 'click_session_key'), 'click_vs_failing' => self::textOf($record, 'click_vs_failing'), 'failing_request_count' => self::countOf($record, 'failing_request_count'), 'sessions_resolved' => self::countOf($record, 'sessions_resolved'), 'reduced' => 'over_budget', ); foreach (array($record, $reduced, $minimal) as $candidate) { $line = json_encode(array(self::FAILING_SESSION_DIAG_KEY => $candidate)); if (is_string($line) && strlen($header) + strlen($line) <= self::MAX_FAILING_SESSION_DIAG_BYTES) { return $header . $line; } } return $header . 'The failing-session diagnostics record could not be encoded for this payload.'; } /** * The one-line version: the relationship first, then how much failing * evidence it was drawn from. 'foreign_sessions_only' over four failing * requests and 'no_failing_sessions' over zero are different findings, so * the counts are part of the summary rather than decoration. * * @param array $record */ private static function summary(array $record): string { return self::textOf($record, 'status') . ': ' . self::textOf($record, 'click_vs_failing') . '; ' . self::countOf($record, 'failing_request_count') . ' failing request(s), ' . self::countOf($record, 'sessions_resolved') . ' session(s) resolved, ' . self::countOf($record, 'unresolved_failing_request_count') . ' unattributed'; } /** * 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; } }