• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wp-graphql / wp-graphql / 14841806285

05 May 2025 04:57PM UTC coverage: 84.234% (-0.05%) from 84.287%
14841806285

push

github

actions-user
chore: update changeset for PR #3374

15900 of 18876 relevant lines covered (84.23%)

257.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.85
/src/Utils/Tracing.php
1
<?php
2

3
namespace WPGraphQL\Utils;
4

5
use GraphQL\Type\Definition\ResolveInfo;
6
use WPGraphQL\AppContext;
7

8
/**
9
 * Class Tracing
10
 *
11
 * Sets up trace data to track how long individual fields take to resolve in WPGraphQL
12
 *
13
 * @package WPGraphQL\Utils
14
 *
15
 * phpcs:disable -- PHPStan annotation.
16
 * @phpstan-type FieldTrace array{
17
 *  path?: array<int,int|string>,
18
 *  parentType?: string,
19
 *  fieldName?: string,
20
 *  returnType?: string,
21
 *  startOffset?: float|int,
22
 *  startMicrotime?: float,
23
 *  duration?: float|int,
24
 * }
25
 *
26
 * @phpstan-type SanitizedFieldTrace array{
27
 *  path: array<int,int|string>,
28
 *  parentType: string,
29
 *  fieldName: string,
30
 *  returnType: string,
31
 *  startOffset: int|'',
32
 *  duration: int|'',
33
 * }
34
 *
35
 * @phpstan-type Trace array{
36
 *  version: int,
37
 *  startTime: float,
38
 *  endTime: float,
39
 *  duration: int,
40
 *  execution: array{
41
 *   resolvers: SanitizedFieldTrace[]
42
 *  }
43
 * }
44
 * phpcs:enable
45
 */
46
class Tracing {
47

48
        /**
49
         * Whether Tracing is enabled
50
         *
51
         * @var bool
52
         */
53
        public $tracing_enabled;
54

55
        /**
56
         * Stores the logs for the trace
57
         *
58
         * @var SanitizedFieldTrace[]
59
         */
60
        public $trace_logs = [];
61

62
        /**
63
         * The start microtime
64
         *
65
         * @var float
66
         */
67
        public $request_start_microtime;
68

69
        /**
70
         * The start timestamp
71
         *
72
         * @var float
73
         */
74
        public $request_start_timestamp;
75

76
        /**
77
         * The end microtime
78
         *
79
         * @var float
80
         */
81
        public $request_end_microtime;
82

83
        /**
84
         * The end timestamp
85
         *
86
         * @var float
87
         */
88
        public $request_end_timestamp;
89

90
        /**
91
         * The trace for the current field being resolved
92
         *
93
         * @var FieldTrace
94
         */
95
        public $field_trace = [];
96

97
        /**
98
         * The version of the Apollo Tracing Spec
99
         *
100
         * @var int
101
         */
102
        public $trace_spec_version = 1;
103

104
        /**
105
         * The user role tracing is limited to
106
         *
107
         * @var string
108
         */
109
        public $tracing_user_role;
110

111
        /**
112
         * Initialize tracing
113
         *
114
         * @return void
115
         */
116
        public function init() {
754✔
117

118
                // Check whether Query Logs have been enabled from the settings page
119
                $enabled               = get_graphql_setting( 'tracing_enabled', 'off' );
754✔
120
                $this->tracing_enabled = 'on' === $enabled;
754✔
121

122
                $this->tracing_user_role = get_graphql_setting( 'tracing_user_role', 'manage_options' );
754✔
123

124
                if ( ! $this->tracing_enabled ) {
754✔
125
                        return;
752✔
126
                }
127

128
                add_filter( 'do_graphql_request', [ $this, 'init_trace' ] );
2✔
129
                add_action( 'graphql_execute', [ $this, 'end_trace' ], 99, 0 );
2✔
130
                add_filter( 'graphql_access_control_allow_headers', [ $this, 'return_tracing_headers' ] );
2✔
131
                add_filter(
2✔
132
                        'graphql_request_results',
2✔
133
                        [
2✔
134
                                $this,
2✔
135
                                'add_tracing_to_response_extensions',
2✔
136
                        ],
2✔
137
                        10,
2✔
138
                        1
2✔
139
                );
2✔
140
                add_action( 'graphql_before_resolve_field', [ $this, 'init_field_resolver_trace' ], 10, 4 );
2✔
141
                add_action( 'graphql_after_resolve_field', [ $this, 'end_field_resolver_trace' ], 10 );
2✔
142
        }
143

144
        /**
145
         * Sets the timestamp and microtime for the start of the request
146
         *
147
         * @return float
148
         */
149
        public function init_trace() {
2✔
150
                $this->request_start_microtime = microtime( true );
2✔
151
                $this->request_start_timestamp = $this->format_timestamp( $this->request_start_microtime );
2✔
152

153
                return $this->request_start_timestamp;
2✔
154
        }
155

156
        /**
157
         * Sets the timestamp and microtime for the end of the request
158
         *
159
         * @return void
160
         */
161
        public function end_trace() {
2✔
162
                $this->request_end_microtime = microtime( true );
2✔
163
                $this->request_end_timestamp = $this->format_timestamp( $this->request_end_microtime );
2✔
164
        }
165

166
        /**
167
         * Initialize tracing for an individual field
168
         *
169
         * @param mixed                                $source         The source passed down the Resolve Tree
170
         * @param array<string,mixed>                  $args           The args for the field
171
         * @param \WPGraphQL\AppContext                $context The AppContext passed down the ResolveTree
172
         * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down the ResolveTree
173
         *
174
         * @return void
175
         */
176
        public function init_field_resolver_trace( $source, array $args, AppContext $context, ResolveInfo $info ) {
1✔
177
                $this->field_trace = [
1✔
178
                        'path'           => $info->path,
1✔
179
                        'parentType'     => $info->parentType->name,
1✔
180
                        'fieldName'      => $info->fieldName,
1✔
181
                        'returnType'     => $info->returnType->name ?? $info->returnType,
1✔
182
                        'startOffset'    => $this->get_start_offset(),
1✔
183
                        'startMicrotime' => microtime( true ),
1✔
184
                ];
1✔
185
        }
186

187
        /**
188
         * End the tracing for a resolver
189
         *
190
         * @return void
191
         */
192
        public function end_field_resolver_trace() {
1✔
193
                if ( ! empty( $this->field_trace ) ) {
1✔
194
                        $this->field_trace['duration'] = $this->get_field_resolver_duration();
1✔
195
                        $sanitized_trace               = $this->sanitize_resolver_trace( $this->field_trace );
1✔
196
                        $this->trace_logs[]            = $sanitized_trace;
1✔
197
                }
198

199
                // reset the field trace
200
                $this->field_trace = [];
1✔
201
        }
202

203
        /**
204
         * Given a resolver start time, returns the duration of a resolver
205
         *
206
         * @return float
207
         */
208
        public function get_field_resolver_duration() {
1✔
209
                $start_microtime = $this->field_trace['startMicrotime'] ?? 0;
1✔
210

211
                return ( microtime( true ) - $start_microtime ) * 1000000;
1✔
212
        }
213

214
        /**
215
         * Get the offset between the start of the request and now
216
         *
217
         * @return float
218
         */
219
        public function get_start_offset() {
1✔
220
                return ( microtime( true ) - $this->request_start_microtime ) * 1000000;
1✔
221
        }
222

223
        /**
224
         * Given a trace, sanitizes the values and returns the sanitized_trace
225
         *
226
         * @param array<string,mixed> $trace
227
         *
228
         * @return SanitizedFieldTrace
229
         */
230
        public function sanitize_resolver_trace( array $trace ) {
1✔
231
                $sanitized_trace                = [];
1✔
232
                $sanitized_trace['path']        = ! empty( $trace['path'] ) && is_array( $trace['path'] ) ? array_map(
1✔
233
                        [
1✔
234
                                $this,
1✔
235
                                'sanitize_trace_resolver_path',
1✔
236
                        ],
1✔
237
                        $trace['path']
1✔
238
                ) : [];
1✔
239
                $sanitized_trace['parentType']  = ! empty( $trace['parentType'] ) ? esc_html( $trace['parentType'] ) : '';
1✔
240
                $sanitized_trace['fieldName']   = ! empty( $trace['fieldName'] ) ? esc_html( $trace['fieldName'] ) : '';
1✔
241
                $sanitized_trace['returnType']  = ! empty( $trace['returnType'] ) ? esc_html( $trace['returnType'] ) : '';
1✔
242
                $sanitized_trace['startOffset'] = ! empty( $trace['startOffset'] ) ? absint( $trace['startOffset'] ) : '';
1✔
243
                $sanitized_trace['duration']    = ! empty( $trace['duration'] ) ? absint( $trace['duration'] ) : '';
1✔
244

245
                return $sanitized_trace;
1✔
246
        }
247

248
        /**
249
         * Given input from a Resolver Path, this sanitizes the input for output in the trace
250
         *
251
         * @param int|string|float|null $input The input to sanitize
252
         *
253
         * @return int|string
254
         *
255
         * @phpstan-return ( $input is int|float|numeric-string ? int : string )
256
         */
257
        public static function sanitize_trace_resolver_path( $input ) {
1✔
258
                if ( is_numeric( $input ) ) {
1✔
259
                        return absint( $input );
×
260
                }
261

262
                return esc_html( (string) $input );
1✔
263
        }
264

265
        /**
266
         * Formats a timestamp to be RFC 3339 compliant
267
         *
268
         * @see https://github.com/apollographql/apollo-tracing
269
         *
270
         * @param string|float|int $time The timestamp to format
271
         *
272
         * @return float
273
         */
274
        public function format_timestamp( $time ) {
2✔
275
                $time_as_float = sprintf( '%.4f', (string) $time );
2✔
276
                $timestamp     = \DateTime::createFromFormat( 'U.u', $time_as_float );
2✔
277

278
                return ! empty( $timestamp ) ? (float) $timestamp->format( 'Y-m-d\TH:i:s.uP' ) : (float) 0;
2✔
279
        }
280

281
        /**
282
         * Filter the headers that WPGraphQL returns to include headers that indicate the WPGraphQL
283
         * server supports Apollo Tracing and Credentials
284
         *
285
         * @param string[] $headers The headers to return
286
         *
287
         * @return string[]
288
         */
289
        public function return_tracing_headers( array $headers ) {
×
290
                $headers[] = 'X-Insights-Include-Tracing';
×
291
                $headers[] = 'X-Apollo-Tracing';
×
292
                $headers[] = 'Credentials';
×
293

294
                return $headers;
×
295
        }
296

297
        /**
298
         * Filter the results of the GraphQL Response to include the Query Log
299
         *
300
         * @param array<string,mixed>|object|mixed $response The response of the GraphQL Request
301
         *
302
         * @return array<string,mixed>|object|mixed
303
         */
304
        public function add_tracing_to_response_extensions( $response ) {
2✔
305

306
                // Get the trace
307
                $trace = $this->get_trace();
2✔
308

309
                // If a specific capability is set for tracing and the requesting user
310
                // doesn't have the capability, return the unmodified response
311
                if ( ! $this->user_can_see_trace_data() ) {
2✔
312
                        return $response;
×
313
                }
314

315
                if ( is_array( $response ) ) {
2✔
316
                        $response['extensions']['tracing'] = $trace;
2✔
317
                } elseif ( is_object( $response ) ) {
×
318
                        // @phpstan-ignore-next-line
319
                        $response->extensions['tracing'] = $trace;
×
320
                }
321

322
                return $response;
2✔
323
        }
324

325
        /**
326
         * Returns the request duration calculated from the start and end times
327
         *
328
         * @return float
329
         */
330
        public function get_request_duration() {
2✔
331
                return ( $this->request_end_microtime - $this->request_start_microtime ) * 1000000;
2✔
332
        }
333

334
        /**
335
         * Determine if the requesting user can see trace data
336
         */
337
        public function user_can_see_trace_data(): bool {
2✔
338
                $can_see = false;
2✔
339

340
                // If logs are disabled, user cannot see logs
341
                if ( ! $this->tracing_enabled ) {
2✔
342
                        $can_see = false;
×
343
                } elseif ( 'any' === $this->tracing_user_role ) {
2✔
344
                        // If "any" is the selected role, anyone can see the logs
345
                        $can_see = true;
2✔
346
                } else {
347
                        // Get the current users roles
348
                        $user = wp_get_current_user();
×
349

350
                        // If the user doesn't have roles or the selected role isn't one the user has, the user cannot see roles.
351
                        if ( in_array( $this->tracing_user_role, $user->roles, true ) ) {
×
352
                                $can_see = true;
×
353
                        }
354
                }
355

356
                /**
357
                 * Filter whether the logs can be seen in the request results or not
358
                 *
359
                 * @param bool $can_see Whether the requester can see the logs or not
360
                 */
361
                return apply_filters( 'graphql_user_can_see_trace_data', $can_see );
2✔
362
        }
363

364
        /**
365
         * Get the trace to add to the response
366
         *
367
         * @return Trace
368
         */
369
        public function get_trace(): array {
2✔
370

371
                // Compile the trace to return with the GraphQL Response
372
                $trace = [
2✔
373
                        'version'   => absint( $this->trace_spec_version ),
2✔
374
                        'startTime' => (float) $this->request_start_microtime,
2✔
375
                        'endTime'   => (float) $this->request_end_microtime,
2✔
376
                        'duration'  => absint( $this->get_request_duration() ),
2✔
377
                        'execution' => [
2✔
378
                                'resolvers' => $this->trace_logs,
2✔
379
                        ],
2✔
380
                ];
2✔
381

382
                /**
383
                 * Filter the trace
384
                 *
385
                 * @param Trace                    $trace     The trace to return
386
                 * @param \WPGraphQL\Utils\Tracing $instance The Tracing class instance
387
                 */
388
                return apply_filters( 'graphql_tracing_response', $trace, $this );
2✔
389
        }
390
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc