getMessage()); return self::noEncodedSize('journal_error'); } } /** * The same answer built from journal lines a caller already holds. * * Exists so a caller that reads the checkpoint journal for its own reasons * can join against the EXACT lines it read. Two independent reads of the * same growing file can straddle a concurrent append, which pairs a record * from one instant with a record from another and reports a comparison * neither read actually saw. ABJ_404_Solution_ResponseBodyDeliveryEvidence * is that caller: it reads the checkpoint journal to find delivery records * and needs the emitted size drawn from the same snapshot. * * The trace and checkpoint journals stay separate parameters because they * are separate files answering separate halves of the join; only the * checkpoint half is shared with that caller. * * @param array $traceLines * @param array $checkpointLines * @return array{bytes: int|null, source: string, request_id: string} */ public static function fromLines(array $traceLines, array $checkpointLines, string $sessionId): array { $sessionId = substr($sessionId, 0, 64); if ($sessionId === '') { return self::noEncodedSize('unavailable'); } $tableRequestIds = self::tableRequestIdsInSession($traceLines, $sessionId); if ($tableRequestIds === array()) { return self::noEncodedSize('session_not_traced'); } return self::latestEncodedSizeForRequests( $checkpointLines, $tableRequestIds, self::noEncodedSize('no_encoded_size_recorded')); } /** * The no-size answer, carrying the reason it is the answer. * * @return array{bytes: int|null, source: string, request_id: string} */ private static function noEncodedSize(string $reason): array { return array('bytes' => null, 'source' => $reason, 'request_id' => ''); } /** * @param array $traceLines * @return array */ private static function tableRequestIdsInSession(array $traceLines, string $sessionId): array { $requestIds = array(); foreach ($traceLines as $line) { if (strpos($line, 'ajaxUpdatePaginationLinks') === false || strpos($line, $sessionId) === false) { continue; } $record = json_decode($line, true); if (!is_array($record)) { continue; } $action = is_scalar($record['action'] ?? null) ? (string)$record['action'] : ''; $part = is_scalar($record['part'] ?? null) ? (string)$record['part'] : ''; $recordSessionId = is_scalar($record['session_id'] ?? null) ? (string)$record['session_id'] : ''; if ($action !== 'ajaxUpdatePaginationLinks' || $part !== 'table' || $recordSessionId !== $sessionId) { continue; } $requestId = is_scalar($record['request_id'] ?? null) ? (string)$record['request_id'] : ''; if (preg_match('/^[A-Za-z0-9]{8,64}$/', $requestId) === 1) { $requestIds[$requestId] = true; } } return $requestIds; } /** * @param array $checkpointLines * @param array $requestIds * @param array{bytes: int|null, source: string, request_id: string} $unavailable * @return array{bytes: int|null, source: string, request_id: string} */ private static function latestEncodedSizeForRequests( array $checkpointLines, array $requestIds, array $unavailable ): array { $latest = $unavailable; foreach ($checkpointLines as $line) { if (strpos($line, '"event":"json_encode"') === false) { continue; } $record = json_decode($line, true); if (!is_array($record)) { continue; } $event = is_scalar($record['event'] ?? null) ? (string)$record['event'] : ''; $requestId = is_scalar($record['request_id'] ?? null) ? (string)$record['request_id'] : ''; $bytes = is_numeric($record['bytes'] ?? null) ? (int)$record['bytes'] : 0; if ($event !== 'json_encode' || !isset($requestIds[$requestId]) || $bytes <= 0) { continue; } $latest = array( 'bytes' => $bytes, 'source' => 'session_json_encode', 'request_id' => $requestId, ); } return $latest; } }