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

wp-graphql / wp-graphql / 15710188483

17 Jun 2025 02:32PM UTC coverage: 84.169% (-0.001%) from 84.17%
15710188483

push

github

actions-user
chore: update changeset for PR #3371

15924 of 18919 relevant lines covered (84.17%)

258.67 hits per line

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

87.74
/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() {
760✔
117

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

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

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

128
                add_action( '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
        public function init_trace(): void {
2✔
148
                $this->request_start_microtime = microtime( true );
2✔
149
                $this->request_start_timestamp = $this->format_timestamp( $this->request_start_microtime );
2✔
150
        }
151

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

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

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

195
                // reset the field trace
196
                $this->field_trace = [];
1✔
197
        }
198

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

207
                return ( microtime( true ) - $start_microtime ) * 1000000;
1✔
208
        }
209

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

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

241
                return $sanitized_trace;
1✔
242
        }
243

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

258
                return esc_html( (string) $input );
1✔
259
        }
260

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

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

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

290
                return $headers;
×
291
        }
292

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

302
                // Get the trace
303
                $trace = $this->get_trace();
2✔
304

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

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

318
                return $response;
2✔
319
        }
320

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

330
        /**
331
         * Determine if the requesting user can see trace data
332
         */
333
        public function user_can_see_trace_data(): bool {
2✔
334
                $can_see = false;
2✔
335

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

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

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

360
        /**
361
         * Get the trace to add to the response
362
         *
363
         * @return Trace
364
         */
365
        public function get_trace(): array {
2✔
366

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

378
                /**
379
                 * Filter the trace
380
                 *
381
                 * @param Trace                    $trace     The trace to return
382
                 * @param \WPGraphQL\Utils\Tracing $instance The Tracing class instance
383
                 */
384
                return apply_filters( 'graphql_tracing_response', $trace, $this );
2✔
385
        }
386
}
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