= 1; $i--) { $old_file = $file_path . '.' . $i; $new_file = $file_path . '.' . ($i + 1); if (file_exists($old_file)) { @rename($old_file, $new_file); } } // Move the current log to .1 @rename($file_path, $file_path . '.1'); // Create a new empty log file @touch($file_path); @chmod($file_path, 0666); } /** * Send USR1 signal to nginx to reopen log files * * nginx reopens log files when receiving USR1 signal, * which is the standard way to handle log rotation. * * @return void */ protected static function __signal_nginx_reopen_logs(): void { // Try to find nginx master process $pid_file = '/var/run/nginx.pid'; if (file_exists($pid_file)) { $pid = trim(file_get_contents($pid_file)); if ($pid && is_numeric($pid)) { // Send USR1 signal to nginx master process @posix_kill((int)$pid, self::SIGUSR1); } } else { // Try to find nginx process via ps $output = []; @\exec_safe('ps aux | grep "nginx: master" | grep -v grep', $output); if (!empty($output)) { // Extract PID from ps output $parts = preg_split('/\s+/', trim($output[0])); if (isset($parts[1]) && is_numeric($parts[1])) { @posix_kill((int)$parts[1], self::SIGUSR1); } } } } /** * Debug output with detailed information * * Outputs debug information including: * - File and line where rsx_dump_die was called * - Var_dump of all passed values (with toArray() conversion for objects) * - Debug backtrace showing last 10 function calls * Then calls die() to halt execution * * @param mixed ...$values Any number of values to debug * @return void (never returns - calls die()) */ public static function rsx_dump_die(...$values): void { // Get backtrace $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); // Detect if called via helper function $caller_index = 0; if (isset($trace[0]['function']) && $trace[0]['function'] === 'rsx_dump_die' && isset($trace[0]['file']) && str_ends_with($trace[0]['file'], '/app/RSpade/helpers.php')) { // Called via helper, skip one level $caller_index = 1; } // Get the actual caller info $caller = $trace[$caller_index] ?? []; $file = $caller['file'] ?? 'unknown'; $line = $caller['line'] ?? 0; // Make path relative to project root $file = str_replace(base_path() . '/', '', $file); // Output header with red color for visibility echo "\n"; // ANSI color codes: \033[31m = red, \033[0m = reset echo "\033[31mrsx_dump_die() at {$file}:{$line}\033[0m\n"; echo str_repeat('-', 80) . "\n"; // Output values concisely foreach ($values as $index => $value) { if (count($values) > 1) { echo "[{$index}] "; } echo static::__format_value_concise($value); echo "\n"; } // Output minimal backtrace (first 3 relevant calls) echo "\nTrace: "; $trace_items = []; for ($i = $caller_index + 1; $i < min(count($trace), $caller_index + 4); $i++) { $item = $trace[$i]; $file = isset($item['file']) ? basename($item['file']) : '?'; $line = $item['line'] ?? 0; $function = $item['function'] ?? '?'; $class = $item['class'] ?? ''; if ($class) { // Simplify class name to last component $parts = explode('\\', $class); $class = end($parts); $function = $class . '::' . $function; } $trace_items[] = "{$function}({$file}:{$line})"; } echo implode(' → ', $trace_items); echo "\n"; // Die to halt execution die(); } /** * Format a value concisely for debugging output * * @param mixed $value The value to format * @param int $depth Current recursion depth * @return string Formatted string representation */ protected static function __format_value_concise($value, int $depth = 0): string { // Limit recursion depth if ($depth > 3) { return '...'; } // Handle scalar values and null if (is_null($value)) { return 'null'; } if (is_bool($value)) { return $value ? 'true' : 'false'; } if (is_int($value) || is_float($value)) { return (string) $value; } if (is_string($value)) { // Truncate long strings and escape special characters if (strlen($value) > 100) { $value = substr($value, 0, 97) . '...'; } return '"' . addslashes($value) . '"'; } // Handle arrays if (is_array($value)) { $count = count($value); if ($count === 0) { return '[]'; } if ($count > 10) { // For large arrays, show count and first 10 keys $keys = array_slice(array_keys($value), 0, 10); $key_str = implode(', ', array_map(function($k) { return is_string($k) ? "'{$k}'" : (string)$k; }, $keys)); return "array({$count}) [{$key_str}, ...]"; } // For small arrays, show compact representation $items = []; foreach ($value as $k => $v) { $formatted = static::__format_value_concise($v, $depth + 1); if (is_string($k)) { $items[] = "'{$k}' => {$formatted}"; } else { $items[] = $formatted; } } return '[' . implode(', ', $items) . ']'; } // Handle objects if (is_object($value)) { $class = get_class($value); // Simplify namespace $parts = explode('\\', $class); $simple_class = end($parts); // Special handling for common objects if ($value instanceof \Illuminate\Support\Collection) { $count = $value->count(); return "Collection({$count})"; } if (method_exists($value, 'toArray')) { try { $array = $value->toArray(); $count = count($array); return "{$simple_class}({$count} props)"; } catch (\Exception $e) { // Fall through } } if (method_exists($value, 'getKey') && method_exists($value, 'getTable')) { // Likely an Eloquent model try { $id = $value->getKey(); return "{$simple_class}(id={$id})"; } catch (\Exception $e) { // Fall through } } return "Object({$simple_class})"; } // Resources and other types if (is_resource($value)) { return 'Resource(' . get_resource_type($value) . ')'; } return gettype($value); } /** * Prepare a value for safe display * * Attempts to convert objects to arrays if they have toArray() method * Prevents infinite recursion and handles exceptions * * @param mixed $value Value to prepare * @return mixed Prepared value */ protected static function __prepare_value_for_display($value) { // Handle scalar values and null if (is_scalar($value) || is_null($value)) { return $value; } // Handle arrays recursively (with depth limit) if (is_array($value)) { return static::__prepare_array_for_display($value, 0, 10); } // Handle objects if (is_object($value)) { // Try toArray() method if (method_exists($value, 'toArray')) { try { $array = $value->toArray(); return ['[' . get_class($value) . '::toArray()]' => $array]; } catch (Exception $e) { // Fall through to default handling } } // Try jsonSerialize() method (for JsonSerializable objects) if ($value instanceof JsonSerializable) { try { $data = $value->jsonSerialize(); return ['[' . get_class($value) . '::jsonSerialize()]' => $data]; } catch (Exception $e) { // Fall through to default handling } } // For Eloquent models, try to get attributes if (method_exists($value, 'getAttributes')) { try { $attributes = $value->getAttributes(); return ['[' . get_class($value) . ' Model]' => $attributes]; } catch (Exception $e) { // Fall through to default handling } } // For collections, convert to array if ($value instanceof \Illuminate\Support\Collection) { try { return ['[Collection]' => $value->toArray()]; } catch (Exception $e) { // Fall through to default handling } } // Default object handling - just return class name to avoid recursion return '[Object: ' . get_class($value) . ']'; } // Resources and other types return $value; } /** * Prepare array for display with recursion protection * * @param array $array Array to prepare * @param int $depth Current depth * @param int $max_depth Maximum depth to recurse * @return array Prepared array */ protected static function __prepare_array_for_display($array, $depth = 0, $max_depth = 10) { if ($depth >= $max_depth) { return '[Max depth reached]'; } $result = []; foreach ($array as $key => $value) { if (is_array($value)) { $result[$key] = static::__prepare_array_for_display($value, $depth + 1, $max_depth); } elseif (is_object($value)) { $result[$key] = static::__prepare_value_for_display($value); } else { $result[$key] = $value; } } return $result; } /** * Normalize a channel name for consistent output * - Convert to uppercase * - Replace non-alphanumeric chars with underscore * - Collapse multiple underscores * - Trim underscores from edges * - Strip any brackets that user might have added * * @param string $channel Raw channel name * @return string Normalized channel name */ protected static function __normalize_channel(string $channel): string { // Strip brackets if present $channel = str_replace(['[', ']'], '', $channel); // Convert to uppercase $channel = strtoupper($channel); // Replace non-alphanumeric with underscore $channel = preg_replace('/[^A-Z0-9]+/', '_', $channel); // Collapse multiple underscores $channel = preg_replace('/_+/', '_', $channel); // Trim underscores from edges $channel = trim($channel, '_'); return $channel; } /** * Get console debug configuration * * @return array Configuration array */ protected static function __get_console_config(): array { if (static::$console_config === null) { $defaults = [ 'enabled' => true, 'outputs' => [ 'cli' => false, 'web' => true, 'ajax' => true, 'laravel_log' => false, ], 'filter_mode' => 'all', // all, whitelist, blacklist, specific 'specific_channel' => null, 'whitelist' => [], 'blacklist' => [], 'include_benchmark' => false, ]; // Deep merge config with defaults (config takes precedence) $config = array_merge($defaults, config('rsx.console_debug', [])); // Ensure outputs exists (in case config had partial data) if (!isset($config['outputs']) || !is_array($config['outputs'])) { $config['outputs'] = $defaults['outputs']; } // Convert string "true"/"false" to boolean for outputs foreach ($config['outputs'] as $key => $value) { if ($value === 'true') { $config['outputs'][$key] = true; } elseif ($value === 'false') { $config['outputs'][$key] = false; } } static::$console_config = $config; } return static::$console_config; } /** * Configure console debug based on HTTP headers from rsx:debug * * This allows the Playwright test runner to pass console debug settings * through HTTP headers since environment variables don't propagate through * the Playwright -> Chrome -> HTTP request chain. * * @return void */ public static function configure_from_headers(): void { // Never process headers in production or from non-loopback IPs if (app()->environment('production') || !is_loopback_ip()) { return; } $request = request(); if (!$request) { return; } // Only apply header configuration if this is a Playwright test request if (!$request->hasHeader('X-Playwright-Test')) { return; } // Get base configuration $config = static::__get_console_config(); // Check for console debug filter header if ($request->hasHeader('X-Console-Debug-Filter')) { $filter = $request->header('X-Console-Debug-Filter'); if ($filter) { $config['filter_mode'] = 'specific'; $config['specific_channel'] = strtoupper($filter); } } // Check for benchmark header if ($request->hasHeader('X-Console-Debug-Benchmark')) { $config['include_benchmark'] = true; } // Check for all channels header if ($request->hasHeader('X-Console-Debug-All')) { $config['filter_mode'] = 'all'; } // Enable CLI output for Playwright tests if console debugging is enabled if ($request->hasHeader('X-Playwright-Console-Debug')) { $config['outputs']['cli'] = true; } // Override the cached configuration static::$console_config = $config; } /** * Set request start time for benchmarking * * @param float|null $time Start time or null to use current time * @return void */ public static function set_request_start_time(?float $time = null): void { static::$request_start_time = $time ?? microtime(true); } /** * Check if console debug is enabled * * @return bool Whether console debug output is enabled */ public static function is_console_debug_enabled(): bool { // Get configuration $config = static::__get_console_config(); // Check if completely disabled if (!$config['enabled']) { return false; } // Check if any output is enabled return $config['outputs']['cli'] || $config['outputs']['web'] || $config['outputs']['ajax'] || $config['outputs']['laravel_log']; } /** * Handle console debug trace mode * * When enabled via config and ?__trace=1 is in GET parameters, captures all * console_debug messages and outputs them as plain text for curl/console viewing. */ public static function _try_enable_trace_mode(): void { // Check if __trace=1 is in GET parameters if (!isset($_GET['__trace']) || !config('rsx.console_debug.enable_get_trace')) { return; } // Import Debugger class $debugger = 'App\RSpade\Core\Debug\Debugger'; // Set trace mode in Debugger self::$trace_mode = true; // Set request start time for benchmarking $debugger::set_request_start_time(defined('LARAVEL_START') ? LARAVEL_START : microtime(true)); // Force console_debug configuration for trace mode config([ 'rsx.console_debug.enabled' => true, 'rsx.console_debug.filter_mode' => 'all', 'rsx.console_debug.include_benchmark' => true, 'rsx.console_debug.outputs.cli' => false, 'rsx.console_debug.outputs.web' => true, 'rsx.console_debug.outputs.ajax' => true, 'rsx.console_debug.outputs.laravel_log' => false, ]); // Start output buffering to capture all output // error_log('OB Level before ob_start: ' . ob_get_level()); ob_start(); // error_log('OB Level after ob_start: ' . ob_get_level()); // Register shutdown handler for trace output register_shutdown_function([$debugger, 'dump_trace_output']); console_debug('TRACE', 'Trace mode activated - capturing all console_debug messages'); } /** * Get elapsed time since request start * * @return string Formatted time string like "05.1234" or "##.####" for >99 seconds */ protected static function __get_benchmark_prefix(): string { if (static::$request_start_time === null) { static::$request_start_time = defined('LARAVEL_START') ? LARAVEL_START : microtime(true); } $elapsed = microtime(true) - static::$request_start_time; if ($elapsed >= 99.0) { return '[##.####]'; } return sprintf('[%02d.%04d]', floor($elapsed), ($elapsed - floor($elapsed)) * 10000); } /** * Check if a channel should be output based on filter configuration * * @param string $channel Normalized channel name * @param array $config Console configuration * @return bool Whether to output this channel */ protected static function __should_output_channel(string $channel, array $config): bool { // Check environment override $env_filter = env('CONSOLE_DEBUG_FILTER'); if ($env_filter !== null) { // Split comma-separated values and normalize $channels = array_map('trim', explode(',', $env_filter)); $channels = array_map('strtoupper', $channels); return in_array($channel, $channels, true); } switch ($config['filter_mode']) { case 'specific': $specific = $config['specific_channel'] ?? ''; if ($specific) { // Split comma-separated values and normalize $channels = array_map('trim', explode(',', $specific)); $channels = array_map('strtoupper', $channels); return in_array($channel, $channels, true); } return false; case 'whitelist': $whitelist = array_map('strtoupper', $config['whitelist'] ?? []); return in_array($channel, $whitelist, true); case 'blacklist': $blacklist = array_map('strtoupper', $config['blacklist'] ?? []); return !in_array($channel, $blacklist, true); case 'all': default: return true; } } /** * Console debug output helper with channel categorization * * Outputs debug information to stderr in CLI mode when SHOW_CONSOLE_DEBUG_CLI is enabled. * In HTTP mode, batches messages for browser console output when SHOW_CONSOLE_DEBUG_HTTP is enabled. * Only outputs when the appropriate flag is set and not in production. * Includes file and line number where console_debug was called from. * Messages are prefixed with the journal category in square brackets. * * @param string $channel Channel category for the debug message (e.g., "DISPATCH", "AUTH", "DB") * @param mixed ...$values Values to output - arbitrary arguments like console.log * @return void */ public static function console_debug(string $channel, ...$values): void { // Don't output in production if (app()->environment('production')) { return; } // Get configuration $config = static::__get_console_config(); // Check if completely disabled if (!$config['enabled']) { return; } // Normalize channel name $channel = static::__normalize_channel($channel); // Check if this channel should be output if (!static::__should_output_channel($channel, $config)) { return; } // Get caller information using debug_backtrace $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); // Detect if called via helper function $caller_index = 0; if (isset($trace[0]['function']) && $trace[0]['function'] === 'console_debug' && isset($trace[0]['file']) && str_ends_with($trace[0]['file'], '/app/RSpade/helpers.php')) { // Called via helper, skip one level $caller_index = 1; } $caller = $trace[$caller_index] ?? null; // Build location prefix $location_prefix = ''; if ($caller && isset($caller['file']) && isset($caller['line'])) { // Get relative path from project root $file = str_replace(base_path() . '/', '', $caller['file']); $location_prefix = "[{$file}:{$caller['line']}] "; } // Add benchmark prefix if enabled $benchmark_prefix = ''; if ($config['include_benchmark']) { $benchmark_prefix = static::__get_benchmark_prefix() . ' '; } // Handle based on context if (app()->runningInConsole()) { // CLI mode - check if CLI output is enabled if (!$config['outputs']['cli']) { return; } // Build channel with prefixes $prefix = $benchmark_prefix . $location_prefix . "[{$channel}]"; // ANSI color codes: \033[36m = light cyan, \033[0m = reset fwrite(STDERR, "\033[36m" . $prefix . "\033[0m"); // Output each value separately foreach ($values as $value) { if (is_string($value) || is_numeric($value) || is_bool($value) || is_null($value)) { // Primitives - output directly $output = is_bool($value) ? ($value ? 'true' : 'false') : (is_null($value) ? 'null' : (string)$value); fwrite(STDERR, ' ' . $output); } else { // Complex types - output as pretty JSON $json_value = static::_prepare_value_for_json($value); $json = json_encode($json_value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); fwrite(STDERR, "\n" . $json); } } fwrite(STDERR, "\n"); } else { // HTTP mode - check if web output is enabled $is_ajax = request()->ajax() || request()->wantsJson(); if ($is_ajax && !$config['outputs']['ajax']) { return; } if (!$is_ajax && !$config['outputs']['web']) { // Check for Playwright header override if (!(isset($_SERVER['HTTP_X_PLAYWRIGHT_CONSOLE_DEBUG']) && $_SERVER['HTTP_X_PLAYWRIGHT_CONSOLE_DEBUG'] === '1')) { return; } } // Prepare values for JSON encoding $prepared_values = []; foreach ($values as $value) { $prepared_values[] = static::_prepare_value_for_json($value); } // Store as structured data: [channel_with_prefixes, [arguments]] $channel_with_prefixes = $benchmark_prefix . $location_prefix . "[{$channel}]"; static::$console_messages[] = [$channel_with_prefixes, $prepared_values]; // Register shutdown function only once and only in non-production (skip in trace mode) if (!static::$shutdown_registered && !app()->environment('production') && !static::$trace_mode) { register_shutdown_function([static::class, 'dump_console_debug_messages_to_html']); static::$shutdown_registered = true; } } // Also log to Laravel log if enabled if ($config['outputs']['laravel_log']) { // For Laravel log, we need a string representation $log_parts = [$benchmark_prefix . $location_prefix . "[{$channel}]"]; foreach ($values as $value) { if (is_string($value) || is_numeric($value)) { $log_parts[] = $value; } else { $log_parts[] = json_encode(static::_prepare_value_for_json($value)); } } Log::debug(implode(' ', $log_parts)); } } /** * Forced console debug - Always outputs in development * * Same as console_debug() but bypasses all filters and configuration. * Always outputs to BOTH CLI and HTTP contexts simultaneously. * Only works in non-production mode - silently ignored in production. * * Use for critical development notices that developers must see, * such as "manifest does not exist, full rebuild required". * * @param string $channel Channel category * @param mixed ...$values Values to output * @return void */ public static function console_debug_force(string $channel, ...$values): void { // Never output in production if (app()->environment('production')) { return; } // Normalize channel name $channel = static::__normalize_channel($channel); // Get caller information $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); // Detect if called via helper function $caller_index = 0; if (isset($trace[0]['function']) && $trace[0]['function'] === 'console_debug_force' && isset($trace[0]['file']) && str_ends_with($trace[0]['file'], '/app/RSpade/helpers.php')) { // Called via helper, skip one level $caller_index = 1; } $caller = $trace[$caller_index] ?? null; // Build location prefix $location_prefix = ''; if ($caller && isset($caller['file']) && isset($caller['line'])) { $file = str_replace(base_path() . '/', '', $caller['file']); $location_prefix = "[{$file}:{$caller['line']}] "; } // CLI output - always output if (app()->runningInConsole()) { $prefix = $location_prefix . "[{$channel}]"; // ANSI color codes: \033[33m = yellow (more visible), \033[0m = reset fwrite(STDERR, "\033[33m" . $prefix . "\033[0m"); foreach ($values as $value) { if (is_string($value) || is_numeric($value) || is_bool($value) || is_null($value)) { $output = is_bool($value) ? ($value ? 'true' : 'false') : (is_null($value) ? 'null' : (string)$value); fwrite(STDERR, ' ' . $output); } else { $json_value = static::_prepare_value_for_json($value); $json = json_encode($json_value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); fwrite(STDERR, "\n" . $json); } } fwrite(STDERR, "\n"); } // HTTP output - always output if (!app()->runningInConsole()) { // Prepare values for JSON $prepared_values = []; foreach ($values as $value) { $prepared_values[] = static::_prepare_value_for_json($value); } // Store message $channel_with_prefixes = $location_prefix . "[{$channel}]"; static::$console_messages[] = [$channel_with_prefixes, $prepared_values]; // Register shutdown function if not already registered if (!static::$shutdown_registered && !static::$trace_mode) { register_shutdown_function([static::class, 'dump_console_debug_messages_to_html']); static::$shutdown_registered = true; } } } /** * Prepare a value for JSON encoding * * Converts PHP objects to arrays if they have toArray() method. * This ensures complex objects are properly serialized for JavaScript consumption. * * @param mixed $value The value to prepare * @return mixed The prepared value */ protected static function _prepare_value_for_json($value) { // Handle null and scalar types directly if (is_null($value) || is_scalar($value)) { return $value; } // Handle arrays recursively if (is_array($value)) { $result = []; foreach ($value as $key => $item) { $result[$key] = static::_prepare_value_for_json($item); } return $result; } // Handle objects if (is_object($value)) { // Check for toArray method if (method_exists($value, 'toArray')) { return static::_prepare_value_for_json($value->toArray()); } // Handle stdClass if ($value instanceof stdClass) { return static::_prepare_value_for_json((array)$value); } // For other objects, convert to array representation // This will include public properties return static::_prepare_value_for_json(get_object_vars($value)); } // Resources and other types return (string)$value; } /** * Dump console debug messages to HTML for browser console output * * Outputs JavaScript that logs batched messages to the browser console. * Clears the message array after output. * In production mode, outputs nothing. * In trace mode, skips output since trace mode has its own handler. * Also registers a shutdown function to output any remaining messages. * * @return void */ public static function dump_console_debug_messages_to_html(): void { // Don't output in production mode if (app()->environment('production')) { return; } // Don't output if suppressed (e.g., for IDE service requests) if (defined('SUPPRESS_CONSOLE_DEBUG_OUTPUT') && SUPPRESS_CONSOLE_DEBUG_OUTPUT) { return; } // Don't output if console HTML is disabled (e.g., for AJAX requests) if (static::$console_html_disabled) { return; } // Don't output in trace mode (trace mode has its own output handler) if (static::$trace_mode) { return; } // Output any accumulated messages if (!empty(static::$console_messages)) { echo ''; // Clear the messages after output static::$console_messages = []; } } /** * Get accumulated console debug messages without outputting them * * Internal framework method - not for end developer use. * Used by exception handler to retrieve messages for error output. * * @internal * @return array Array of console debug messages */ public static function _get_console_messages(): array { return static::$console_messages; } /** * Register the shutdown function for dumping console messages * * This is useful for ensuring console messages are output even after fatal errors. * Only registers once and only in non-production mode. * Skipped in trace mode since trace mode has its own output handler. * * @return void */ public static function register_console_dump_shutdown(): void { if (!static::$shutdown_registered && !app()->environment('production') && !static::$trace_mode) { register_shutdown_function([static::class, 'dump_console_debug_messages_to_html']); static::$shutdown_registered = true; } } /** * Disable console HTML output (for AJAX requests) * * Prevents console.log script blocks from being output to the response. * Messages are still collected and can be retrieved with _get_console_messages() * * @return void */ public static function disable_console_html_output(): void { static::$console_html_disabled = true; } /** * Enable console HTML output (default mode) * * Allows console.log script blocks to be output to the response. * * @return void */ public static function enable_console_html_output(): void { static::$console_html_disabled = false; } /** * Check if console HTML output is disabled * * @return bool */ public static function is_console_html_disabled(): bool { return static::$console_html_disabled; } /** * Ajax endpoint to log console_debug messages from JavaScript * * @param \Illuminate\Http\Request $request * @param array $params * @return array */ public static function log_console_messages(\Illuminate\Http\Request $request, array $params = []): array { // Check if Laravel log output is enabled $config = config('rsx.console_debug', []); if (!($config['enabled'] ?? true) || !($config['outputs']['laravel_log'] ?? false)) { return ['status' => 'disabled']; } // Get messages from request $messages = $request->json('messages', []); if (empty($messages)) { return ['status' => 'no_messages']; } // Log each message foreach ($messages as $message) { if (!isset($message['channel']) || !isset($message['values'])) { continue; } $channel = $message['channel']; $values = $message['values']; // Build log message $log_parts = ["[CONSOLE_DEBUG][JS][{$channel}]"]; // Add location if provided if (!empty($message['location'])) { $log_parts[] = "at {$message['location']}"; } // Add values foreach ($values as $value) { if (is_scalar($value) || is_null($value)) { $log_parts[] = (string)$value; } else { $log_parts[] = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); } } // Add backtrace if provided if (!empty($message['backtrace'])) { $log_parts[] = "\nBacktrace:\n" . implode("\n", $message['backtrace']); } // Write to Laravel log Log::info(implode(' ', $log_parts)); } return ['status' => 'logged', 'count' => count($messages)]; } /** * Ajax endpoint to log browser errors from JavaScript * * @param \Illuminate\Http\Request $request * @param array $params * @return array */ public static function log_browser_errors(\Illuminate\Http\Request $request, array $params = []): array { // Check if browser error logging is enabled if (!config('rsx.log_browser_errors', false)) { return ['status' => 'disabled']; } // Get errors from request $errors = $request->json('errors', []); if (empty($errors)) { return ['status' => 'no_errors']; } // Log each error foreach ($errors as $error) { $log_parts = ['[BROWSER_ERROR]']; // Add error type if (!empty($error['type'])) { $log_parts[] = "[{$error['type']}]"; } // Add message if (!empty($error['message'])) { $log_parts[] = $error['message']; } // Add location info if (!empty($error['filename']) && !empty($error['lineno'])) { $log_parts[] = "at {$error['filename']}:{$error['lineno']}"; if (!empty($error['colno'])) { $log_parts[] = ":{$error['colno']}"; } } // Add URL if (!empty($error['url'])) { $log_parts[] = "URL: {$error['url']}"; } // Build error message $error_message = implode(' ', $log_parts); // Add stack trace if available if (!empty($error['stack'])) { $error_message .= "\nStack trace:\n{$error['stack']}"; } // Add user agent if available if (!empty($error['userAgent'])) { $error_message .= "\nUser Agent: {$error['userAgent']}"; } // Log as error level Log::error($error_message); } return ['status' => 'logged', 'count' => count($errors)]; } public static function is_trace_output_request() { return static::$trace_mode; } /** * Dump trace output for console/curl viewing * * Called as a shutdown handler when trace mode is enabled. * Outputs all console_debug messages as plain text with millisecond timing. * * @return void */ public static function dump_trace_output(): void { console_debug('TRACE', 'Script execution complete'); // Debug to verify this is being called error_log('DUMP_TRACE_OUTPUT CALLED'); // Clean all output buffers while (ob_get_level() > 0) { ob_end_clean(); } // Set content type to plain text if (!headers_sent()) { header('Content-Type: text/plain; charset=UTF-8'); } // Get request start time $start_time = static::$request_start_time ?? (defined('LARAVEL_START') ? LARAVEL_START : $_SERVER['REQUEST_TIME_FLOAT'] ?? microtime(true)); // Output header echo "====== REQUEST TRACE ======\n"; echo 'URL: ' . ($_SERVER['REQUEST_URI'] ?? 'unknown') . "\n"; echo 'Method: ' . ($_SERVER['REQUEST_METHOD'] ?? 'unknown') . "\n"; echo 'Time: ' . date('Y-m-d H:i:s') . "\n"; echo "===========================\n\n"; // Output each console_debug message if (!empty(static::$console_messages)) { foreach (static::$console_messages as $message) { // Messages are [channel_with_prefixes, [arguments]] $channel_line = $message[0]; $arguments = $message[1]; // Extract timing from channel line if present $timing_pattern = '/^\[(\d{2}\.\d{4})\]/'; if (preg_match($timing_pattern, $channel_line, $matches)) { // Format seconds with 3 decimal places $seconds = (float)$matches[1]; $formatted_seconds = number_format($seconds, 3, '.', ''); $channel_line = preg_replace($timing_pattern, "[{$formatted_seconds}s]", $channel_line); } // Output the channel line echo $channel_line; // Output arguments foreach ($arguments as $arg) { if (is_scalar($arg) || is_null($arg)) { $output = is_bool($arg) ? ($arg ? 'true' : 'false') : (is_null($arg) ? 'null' : (string)$arg); echo ' ' . $output; } else { // Complex types - output as JSON on new lines $json = json_encode($arg, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); echo "\n" . $json; } } echo "\n"; } } // Calculate and output total request time in seconds $total_time = microtime(true) - $start_time; $formatted_total = number_format($total_time, 3, '.', ''); echo "\n===========================\n"; echo "Total request time: {$formatted_total}s\n"; echo "===========================\n"; } }