PluginProbe
WDesignKit – AI Templates, Widget Builder & MCP Workflow / 2.7.0
WDesignKit – AI Templates, Widget Builder & MCP Workflow v2.7.0
2.7.0 2.6.6 2.6.5 2.6.4 2.6.3 2.6.2 2.6.1 2.6.0 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5.0 2.4.0 2.3.3 2.3.2 2.3.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 All 129 releases
wdesignkit / includes / admin / import / class-wdkit-import-log.php

class-wdkit-import-log.php in WDesignKit – AI Templates, Widget Builder & MCP Workflow 2.7.0, at includes/admin/import/class-wdkit-import-log.php

232 lines 6.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Import diagnostics log.
4 *
5 * Every failure this runner produces already knows what went wrong — the trouble was that
6 * nothing kept it anywhere a person could read afterwards. The session holds the last 50
7 * errors but is deleted when a run finishes, and error_log() is off on most hosts, so a
8 * failed import left the user with a red triangle and left us with nothing to look at.
9 *
10 * This keeps a small, bounded record that survives the session:
11 *
12 * - one line per stage outcome, one per failure
13 * - capped, non-autoloaded, so it cannot bloat the options table or every page load
14 * - mirrored to error_log() when WP_DEBUG is on, for hosts where that is being watched -
15 * minus `message`/`title`, which routinely carry customer content (a description, a
16 * generated page title that IS the business name) and, unlike the option this also
17 * writes, error_log() commonly ends up in a web-readable wp-content/debug.log
18 *
19 * Read it with:
20 *
21 * wp eval 'print_r( Wdkit_Import_Log::read() );'
22 *
23 * @package Wdesignkit
24 * @since 2.6.5
25 */
26
27 if ( ! defined( 'ABSPATH' ) ) {
28 exit;
29 }
30
31 if ( ! class_exists( 'Wdkit_Import_Log' ) ) {
32
33 /**
34 * Bounded, readable record of what an import did.
35 */
36 class Wdkit_Import_Log {
37
38 /**
39 * Option name. Not autoloaded.
40 */
41 const OPTION = 'wdkit_import_log';
42
43 /**
44 * Entries kept. Old ones fall off the front.
45 */
46 const MAX_ENTRIES = 200;
47
48 /**
49 * Keys never mirrored to error_log().
50 *
51 * The options-table copy this method writes on every call is private: it sits in the
52 * database, behind whatever access the site already requires. error_log() is not -
53 * on most hosts it is wp-content/debug.log, a plain file that plenty of server
54 * configs (Nginx especially; .htaccess does nothing there) serve to anyone who
55 * requests it. Free-text fields end up holding customer content: `message` on an AI
56 * or import failure has carried request/response text, and `title` is a generated
57 * page's title, which routinely IS the business name ("Bright Bakery Co - Home").
58 * Redacting them here does not lose anything - the full entry, message included,
59 * still reaches `read()` and `report()` through the option, which is where this
60 * class's own admin-facing diagnostics already come from.
61 *
62 * @var string[]
63 */
64 private static $redact_from_error_log = array( 'message', 'title' );
65
66 /**
67 * Record one event.
68 *
69 * @param string $event Short event name, e.g. 'stage', 'failure'.
70 * @param array $data Whatever is worth keeping. Scalars only, trimmed.
71 * @return void
72 */
73 public static function add( $event, $data = array() ) {
74 $entry = array(
75 'at' => gmdate( 'Y-m-d H:i:s' ),
76 'event' => (string) $event,
77 );
78
79 foreach ( (array) $data as $key => $value ) {
80 if ( is_scalar( $value ) || null === $value ) {
81 $entry[ (string) $key ] = is_string( $value ) ? mb_substr( $value, 0, 300 ) : $value;
82 } elseif ( is_array( $value ) ) {
83 $entry[ (string) $key ] = mb_substr( (string) wp_json_encode( $value ), 0, 300 );
84 }
85 }
86
87 $log = get_option( self::OPTION, array() );
88
89 if ( ! is_array( $log ) ) {
90 $log = array();
91 }
92
93 $log[] = $entry;
94
95 if ( count( $log ) > self::MAX_ENTRIES ) {
96 $log = array_slice( $log, -self::MAX_ENTRIES );
97 }
98
99 update_option( self::OPTION, $log, false );
100
101 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
102 $mirrored = $entry;
103
104 foreach ( self::$redact_from_error_log as $key ) {
105 if ( isset( $mirrored[ $key ] ) ) {
106 $mirrored[ $key ] = '[redacted - see wp_options wdkit_import_log]';
107 }
108 }
109
110 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
111 error_log( '[wdkit-import] ' . wp_json_encode( $mirrored ) );
112 }
113 }
114
115 /**
116 * The whole log, oldest first.
117 *
118 * @return array[]
119 */
120 public static function read() {
121 $log = get_option( self::OPTION, array() );
122
123 return is_array( $log ) ? $log : array();
124 }
125
126 /**
127 * Human-readable timing breakdown for the last run.
128 *
129 * Answers "where did the time go" without anyone having to read raw JSON:
130 *
131 * wp eval 'echo Wdkit_Import_Log::report();'
132 *
133 * @return string
134 */
135 public static function report() {
136 $log = self::read();
137
138 if ( empty( $log ) ) {
139 return "no import logged\n";
140 }
141
142 $out = '';
143 $stages = array();
144 $tpl = array();
145 $fails = array();
146
147 foreach ( $log as $e ) {
148 $event = isset( $e['event'] ) ? $e['event'] : '';
149
150 if ( 'stage' === $event ) {
151 $name = isset( $e['stage'] ) ? $e['stage'] : '?';
152
153 if ( ! isset( $stages[ $name ] ) ) {
154 $stages[ $name ] = array( 'ms' => 0, 'calls' => 0 );
155 }
156
157 $stages[ $name ]['ms'] += isset( $e['ms'] ) ? (int) $e['ms'] : 0;
158 ++$stages[ $name ]['calls'];
159 } elseif ( 'template' === $event ) {
160 $tpl[] = $e;
161 } elseif ( 'failure' === $event ) {
162 $fails[] = $e;
163 }
164 }
165
166 $out .= "TEMPLATES (slowest first)\n";
167 $out .= sprintf( " %-24s %8s %8s %10s %8s %7s\n", 'title', 'total', 'fetch', 'transform', 'store', 'images' );
168
169 usort(
170 $tpl,
171 function ( $a, $b ) {
172 return ( isset( $b['ms'] ) ? $b['ms'] : 0 ) <=> ( isset( $a['ms'] ) ? $a['ms'] : 0 );
173 }
174 );
175
176 $sum = 0;
177
178 foreach ( $tpl as $t ) {
179 $sum += isset( $t['ms'] ) ? (int) $t['ms'] : 0;
180
181 $out .= sprintf(
182 " %-24s %7dms %7dms %9dms %7dms %7d\n",
183 mb_substr( isset( $t['title'] ) ? $t['title'] : '?', 0, 24 ),
184 isset( $t['ms'] ) ? $t['ms'] : 0,
185 isset( $t['fetch_ms'] ) ? $t['fetch_ms'] : 0,
186 isset( $t['transform_ms'] ) ? $t['transform_ms'] : 0,
187 isset( $t['store_ms'] ) ? $t['store_ms'] : 0,
188 isset( $t['images'] ) ? $t['images'] : 0
189 );
190 }
191
192 $out .= sprintf( " %-24s %7dms across %d templates\n\n", 'TOTAL', $sum, count( $tpl ) );
193
194 $out .= "STAGES (server time only - excludes the browser's CSS passes)\n";
195
196 $stage_total = 0;
197
198 foreach ( $stages as $name => $d ) {
199 $stage_total += $d['ms'];
200
201 $out .= sprintf( " %-28s %7dms over %d request(s)\n", $name, $d['ms'], $d['calls'] );
202 }
203
204 $out .= sprintf( " %-28s %7dms\n", 'TOTAL', $stage_total );
205
206 if ( ! empty( $fails ) ) {
207 $out .= sprintf( "\nFAILURES (%d)\n", count( $fails ) );
208
209 foreach ( array_slice( $fails, 0, 10 ) as $f ) {
210 $out .= sprintf(
211 " %-18s %-26s %s\n",
212 isset( $f['step'] ) ? $f['step'] : '?',
213 isset( $f['code'] ) ? $f['code'] : '?',
214 mb_substr( isset( $f['message'] ) ? $f['message'] : '', 0, 70 )
215 );
216 }
217 }
218
219 return $out;
220 }
221
222 /**
223 * Forget everything. Called when a run starts so one run's log is one run.
224 *
225 * @return void
226 */
227 public static function clear() {
228 delete_option( self::OPTION );
229 }
230 }
231 }
232