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 / feedback / DebugLogEvidenceBudget.php

DebugLogEvidenceBudget.php in 404 Solution trunk, at includes/feedback/DebugLogEvidenceBudget.php

127 lines 5.5 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 // allow-no-test-found: exercised through public report builders in tests/FeedbackTransportSupportPayloadTest.php.
8
9 /**
10 * Allocates the fixed debug-log wire budget between an error anchor and the
11 * existing recent tail. It performs no file I/O; snapshots come from
12 * ABJ_404_Solution_DebugLogReader and crash beacons may supply their own anchor.
13 */
14 final class ABJ_404_Solution_DebugLogEvidenceBudget {
15
16 const SCHEMA_VERSION = 1;
17
18 /**
19 * Shape one reader snapshot for FeedbackDiagnosticsCollector.
20 *
21 * @param array<string, mixed> $snapshot
22 * @return array{debug_log: string, debug_log_evidence: array{schema_version: int,
23 * error_excerpt: string, error_excerpt_in_debug_log: bool,
24 * error_line_number: int, total_evidence_bytes: int}}
25 */
26 public static function fromSnapshot(array $snapshot): array {
27 $tail = isset($snapshot['tail']) && is_string($snapshot['tail'])
28 ? $snapshot['tail'] : '';
29 $lineNumber = isset($snapshot['num']) && is_scalar($snapshot['num'])
30 ? (int)$snapshot['num'] : -1;
31 $anchor = '';
32 $inTail = false;
33
34 if ($lineNumber >= 0) {
35 $fileSize = isset($snapshot['file_size']) && is_scalar($snapshot['file_size'])
36 ? (int)$snapshot['file_size'] : strlen($tail);
37 $tailStart = max(0, $fileSize - strlen($tail));
38 $errorOffset = isset($snapshot['latest_error_offset']) && is_scalar($snapshot['latest_error_offset'])
39 ? (int)$snapshot['latest_error_offset'] : -1;
40 $inTail = $errorOffset >= $tailStart;
41 if (!$inTail) {
42 $context = isset($snapshot['error_context']) && is_string($snapshot['error_context'])
43 ? $snapshot['error_context'] : '';
44 $contextStart = isset($snapshot['error_context_start']) && is_scalar($snapshot['error_context_start'])
45 ? (int)$snapshot['error_context_start'] : $errorOffset;
46 $nonOverlappingBytes = max(0, $tailStart - $contextStart);
47 $anchor = substr(
48 $context,
49 0,
50 min(ABJ_404_Solution_DebugLogReader::ERROR_EXCERPT_MAX_BYTES, $nonOverlappingBytes)
51 );
52 if ($anchor === '') {
53 $line = isset($snapshot['line']) && is_string($snapshot['line']) ? $snapshot['line'] : '';
54 $anchor = substr($line, 0, ABJ_404_Solution_DebugLogReader::ERROR_EXCERPT_MAX_BYTES);
55 }
56 }
57 }
58
59 return self::shape($tail, $anchor, $inTail, $lineNumber);
60 }
61
62 /**
63 * Re-apply the invariant after type-specific extras override collected
64 * fields. Crash beacons use this path to anchor their captured fatal error.
65 *
66 * @param array<string, mixed> $payload
67 * @return array<string, mixed>
68 */
69 public static function normalizePayload(array $payload): array {
70 $tail = isset($payload['debug_log']) && is_string($payload['debug_log'])
71 ? $payload['debug_log'] : '';
72 $evidence = isset($payload['debug_log_evidence']) && is_array($payload['debug_log_evidence'])
73 ? $payload['debug_log_evidence'] : array();
74 $anchor = isset($evidence['error_excerpt']) && is_string($evidence['error_excerpt'])
75 ? substr($evidence['error_excerpt'], 0, ABJ_404_Solution_DebugLogReader::ERROR_EXCERPT_MAX_BYTES)
76 : '';
77 $inTail = !empty($evidence['error_excerpt_in_debug_log']);
78 if ($anchor !== '' && strpos($tail, $anchor) !== false) {
79 $inTail = true;
80 }
81 $lineNumber = isset($evidence['error_line_number']) && is_scalar($evidence['error_line_number'])
82 ? (int)$evidence['error_line_number'] : -1;
83 $source = isset($evidence['source']) && is_string($evidence['source'])
84 ? $evidence['source'] : 'debug_log';
85
86 $shaped = self::shape($tail, $anchor, $inTail, $lineNumber);
87 $payload['debug_log'] = $shaped['debug_log'];
88 $payload['debug_log_evidence'] = $shaped['debug_log_evidence'];
89 $payload['debug_log_evidence']['source'] = $source;
90 return $payload;
91 }
92
93 /**
94 * @return array{debug_log: string, debug_log_evidence: array{schema_version: int,
95 * error_excerpt: string, error_excerpt_in_debug_log: bool,
96 * error_line_number: int, total_evidence_bytes: int}}
97 */
98 public static function emptyEvidence(): array {
99 return self::shape('', '', false, -1);
100 }
101
102 /**
103 * @return array{debug_log: string, debug_log_evidence: array{schema_version: int,
104 * error_excerpt: string, error_excerpt_in_debug_log: bool,
105 * error_line_number: int, total_evidence_bytes: int}}
106 */
107 private static function shape(string $tail, string $anchor, bool $inTail, int $lineNumber): array {
108 if ($inTail) {
109 $anchor = '';
110 }
111 $tailBudget = ABJ_404_Solution_DebugLogReader::REPORT_EVIDENCE_MAX_BYTES - strlen($anchor);
112 if (strlen($tail) > $tailBudget) {
113 $tail = $tailBudget > 0 ? substr($tail, -$tailBudget) : '';
114 }
115 return array(
116 'debug_log' => $tail,
117 'debug_log_evidence' => array(
118 'schema_version' => self::SCHEMA_VERSION,
119 'error_excerpt' => $anchor,
120 'error_excerpt_in_debug_log' => $inTail,
121 'error_line_number' => $lineNumber,
122 'total_evidence_bytes' => strlen($tail) + strlen($anchor),
123 ),
124 );
125 }
126 }
127