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

wp-graphql / wp-graphql / 13316763745

13 Feb 2025 08:45PM UTC coverage: 82.712% (-0.3%) from 83.023%
13316763745

push

github

web-flow
Merge pull request #3307 from wp-graphql/release/v2.0.0

release: v2.0.0

195 of 270 new or added lines in 20 files covered. (72.22%)

180 existing lines in 42 files now uncovered.

13836 of 16728 relevant lines covered (82.71%)

299.8 hits per line

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

87.96
/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
class Tracing {
16

17
        /**
18
         * Whether Tracing is enabled
19
         *
20
         * @var bool
21
         */
22
        public $tracing_enabled;
23

24
        /**
25
         * Stores the logs for the trace
26
         *
27
         * @var array<string,mixed>[]
28
         */
29
        public $trace_logs = [];
30

31
        /**
32
         * The start microtime
33
         *
34
         * @var float
35
         */
36
        public $request_start_microtime;
37

38
        /**
39
         * The start timestamp
40
         *
41
         * @var float
42
         */
43
        public $request_start_timestamp;
44

45
        /**
46
         * The end microtime
47
         *
48
         * @var float
49
         */
50
        public $request_end_microtime;
51

52
        /**
53
         * The end timestamp
54
         *
55
         * @var float
56
         */
57
        public $request_end_timestamp;
58

59
        /**
60
         * The trace for the current field being resolved
61
         *
62
         * @var array<string,mixed>
63
         */
64
        public $field_trace = [];
65

66
        /**
67
         * The version of the Apollo Tracing Spec
68
         *
69
         * @var int
70
         */
71
        public $trace_spec_version = 1;
72

73
        /**
74
         * The user role tracing is limited to
75
         *
76
         * @var string
77
         */
78
        public $tracing_user_role;
79

80
        /**
81
         * Initialize tracing
82
         *
83
         * @return void
84
         */
85
        public function init() {
754✔
86

87
                // Check whether Query Logs have been enabled from the settings page
88
                $enabled               = get_graphql_setting( 'tracing_enabled', 'off' );
754✔
89
                $this->tracing_enabled = 'on' === $enabled;
754✔
90

91
                $this->tracing_user_role = get_graphql_setting( 'tracing_user_role', 'manage_options' );
754✔
92

93
                if ( ! $this->tracing_enabled ) {
754✔
94
                        return;
752✔
95
                }
96

97
                add_filter( 'do_graphql_request', [ $this, 'init_trace' ] );
2✔
98
                add_action( 'graphql_execute', [ $this, 'end_trace' ], 99, 0 );
2✔
99
                add_filter( 'graphql_access_control_allow_headers', [ $this, 'return_tracing_headers' ] );
2✔
100
                add_filter(
2✔
101
                        'graphql_request_results',
2✔
102
                        [
2✔
103
                                $this,
2✔
104
                                'add_tracing_to_response_extensions',
2✔
105
                        ],
2✔
106
                        10,
2✔
107
                        1
2✔
108
                );
2✔
109
                add_action( 'graphql_before_resolve_field', [ $this, 'init_field_resolver_trace' ], 10, 4 );
2✔
110
                add_action( 'graphql_after_resolve_field', [ $this, 'end_field_resolver_trace' ], 10 );
2✔
111
        }
112

113
        /**
114
         * Sets the timestamp and microtime for the start of the request
115
         *
116
         * @return float
117
         */
118
        public function init_trace() {
2✔
119
                $this->request_start_microtime = microtime( true );
2✔
120
                $this->request_start_timestamp = $this->format_timestamp( $this->request_start_microtime );
2✔
121

122
                return $this->request_start_timestamp;
2✔
123
        }
124

125
        /**
126
         * Sets the timestamp and microtime for the end of the request
127
         *
128
         * @return void
129
         */
130
        public function end_trace() {
2✔
131
                $this->request_end_microtime = microtime( true );
2✔
132
                $this->request_end_timestamp = $this->format_timestamp( $this->request_end_microtime );
2✔
133
        }
134

135
        /**
136
         * Initialize tracing for an individual field
137
         *
138
         * @param mixed                                $source         The source passed down the Resolve Tree
139
         * @param array<string,mixed>                  $args           The args for the field
140
         * @param \WPGraphQL\AppContext                $context The AppContext passed down the ResolveTree
141
         * @param \GraphQL\Type\Definition\ResolveInfo $info The ResolveInfo passed down the ResolveTree
142
         *
143
         * @return void
144
         */
145
        public function init_field_resolver_trace( $source, array $args, AppContext $context, ResolveInfo $info ) {
1✔
146
                $this->field_trace = [
1✔
147
                        'path'           => $info->path,
1✔
148
                        'parentType'     => $info->parentType->name,
1✔
149
                        'fieldName'      => $info->fieldName,
1✔
150
                        'returnType'     => $info->returnType->name ?? $info->returnType,
1✔
151
                        'startOffset'    => $this->get_start_offset(),
1✔
152
                        'startMicrotime' => microtime( true ),
1✔
153
                ];
1✔
154
        }
155

156
        /**
157
         * End the tracing for a resolver
158
         *
159
         * @return void
160
         */
161
        public function end_field_resolver_trace() {
1✔
162
                if ( ! empty( $this->field_trace ) ) {
1✔
163
                        $this->field_trace['duration'] = $this->get_field_resolver_duration();
1✔
164
                        $sanitized_trace               = $this->sanitize_resolver_trace( $this->field_trace );
1✔
165
                        $this->trace_logs[]            = $sanitized_trace;
1✔
166
                }
167

168
                // reset the field trace
169
                $this->field_trace = [];
1✔
170
        }
171

172
        /**
173
         * Given a resolver start time, returns the duration of a resolver
174
         *
175
         * @return float|int
176
         */
177
        public function get_field_resolver_duration() {
1✔
178
                return ( microtime( true ) - $this->field_trace['startMicrotime'] ) * 1000000;
1✔
179
        }
180

181
        /**
182
         * Get the offset between the start of the request and now
183
         *
184
         * @return float|int
185
         */
186
        public function get_start_offset() {
1✔
187
                return ( microtime( true ) - $this->request_start_microtime ) * 1000000;
1✔
188
        }
189

190
        /**
191
         * Given a trace, sanitizes the values and returns the sanitized_trace
192
         *
193
         * @param array<string,mixed> $trace
194
         *
195
         * @return array<string,mixed>
196
         */
197
        public function sanitize_resolver_trace( array $trace ) {
1✔
198
                $sanitized_trace                = [];
1✔
199
                $sanitized_trace['path']        = ! empty( $trace['path'] ) && is_array( $trace['path'] ) ? array_map(
1✔
200
                        [
1✔
201
                                $this,
1✔
202
                                'sanitize_trace_resolver_path',
1✔
203
                        ],
1✔
204
                        $trace['path']
1✔
205
                ) : [];
1✔
206
                $sanitized_trace['parentType']  = ! empty( $trace['parentType'] ) ? esc_html( $trace['parentType'] ) : '';
1✔
207
                $sanitized_trace['fieldName']   = ! empty( $trace['fieldName'] ) ? esc_html( $trace['fieldName'] ) : '';
1✔
208
                $sanitized_trace['returnType']  = ! empty( $trace['returnType'] ) ? esc_html( $trace['returnType'] ) : '';
1✔
209
                $sanitized_trace['startOffset'] = ! empty( $trace['startOffset'] ) ? absint( $trace['startOffset'] ) : '';
1✔
210
                $sanitized_trace['duration']    = ! empty( $trace['duration'] ) ? absint( $trace['duration'] ) : '';
1✔
211

212
                return $sanitized_trace;
1✔
213
        }
214

215
        /**
216
         * Given input from a Resolver Path, this sanitizes the input for output in the trace
217
         *
218
         * @param mixed $input The input to sanitize
219
         *
220
         * @return int|string|null
221
         */
222
        public static function sanitize_trace_resolver_path( $input ) {
1✔
223
                $sanitized_input = null;
1✔
224
                if ( is_numeric( $input ) ) {
1✔
225
                        $sanitized_input = absint( $input );
×
226
                } else {
227
                        $sanitized_input = esc_html( $input );
1✔
228
                }
229

230
                return $sanitized_input;
1✔
231
        }
232

233
        /**
234
         * Formats a timestamp to be RFC 3339 compliant
235
         *
236
         * @see https://github.com/apollographql/apollo-tracing
237
         *
238
         * @param mixed|string|float|int $time The timestamp to format
239
         *
240
         * @return float
241
         */
242
        public function format_timestamp( $time ) {
2✔
243
                $time_as_float = sprintf( '%.4f', $time );
2✔
244
                $timestamp     = \DateTime::createFromFormat( 'U.u', $time_as_float );
2✔
245

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

249
        /**
250
         * Filter the headers that WPGraphQL returns to include headers that indicate the WPGraphQL
251
         * server supports Apollo Tracing and Credentials
252
         *
253
         * @param string[] $headers The headers to return
254
         *
255
         * @return string[]
256
         */
UNCOV
257
        public function return_tracing_headers( array $headers ) {
×
258
                $headers[] = 'X-Insights-Include-Tracing';
×
259
                $headers[] = 'X-Apollo-Tracing';
×
260
                $headers[] = 'Credentials';
×
261

262
                return $headers;
×
263
        }
264

265
        /**
266
         * Filter the results of the GraphQL Response to include the Query Log
267
         *
268
         * @param mixed|array<string,mixed>|object $response       The response of the GraphQL Request
269
         *
270
         * @return mixed $response
271
         */
272
        public function add_tracing_to_response_extensions( $response ) {
2✔
273

274
                // Get the trace
275
                $trace = $this->get_trace();
2✔
276

277
                // If a specific capability is set for tracing and the requesting user
278
                // doesn't have the capability, return the unmodified response
279
                if ( ! $this->user_can_see_trace_data() ) {
2✔
280
                        return $response;
×
281
                }
282

283
                if ( is_array( $response ) ) {
2✔
284
                        $response['extensions']['tracing'] = $trace;
2✔
285
                } elseif ( is_object( $response ) ) {
×
286
                        // @phpstan-ignore-next-line
287
                        $response->extensions['tracing'] = $trace;
×
288
                }
289

290
                return $response;
2✔
291
        }
292

293
        /**
294
         * Returns the request duration calculated from the start and end times
295
         *
296
         * @return float|int
297
         */
298
        public function get_request_duration() {
2✔
299
                return ( $this->request_end_microtime - $this->request_start_microtime ) * 1000000;
2✔
300
        }
301

302
        /**
303
         * Determine if the requesting user can see trace data
304
         */
305
        public function user_can_see_trace_data(): bool {
2✔
306
                $can_see = false;
2✔
307

308
                // If logs are disabled, user cannot see logs
309
                if ( ! $this->tracing_enabled ) {
2✔
310
                        $can_see = false;
×
311
                } elseif ( 'any' === $this->tracing_user_role ) {
2✔
312
                        // If "any" is the selected role, anyone can see the logs
313
                        $can_see = true;
2✔
314
                } else {
315
                        // Get the current users roles
316
                        $user = wp_get_current_user();
×
317

318
                        // If the user doesn't have roles or the selected role isn't one the user has, the user cannot see roles.
319
                        if ( in_array( $this->tracing_user_role, $user->roles, true ) ) {
×
320
                                $can_see = true;
×
321
                        }
322
                }
323

324
                /**
325
                 * Filter whether the logs can be seen in the request results or not
326
                 *
327
                 * @param bool $can_see Whether the requester can see the logs or not
328
                 */
329
                return apply_filters( 'graphql_user_can_see_trace_data', $can_see );
2✔
330
        }
331

332
        /**
333
         * Get the trace to add to the response
334
         *
335
         * @return array<string,mixed>
336
         */
337
        public function get_trace(): array {
2✔
338

339
                // Compile the trace to return with the GraphQL Response
340
                $trace = [
2✔
341
                        'version'   => absint( $this->trace_spec_version ),
2✔
342
                        'startTime' => (float) $this->request_start_microtime,
2✔
343
                        'endTime'   => (float) $this->request_end_microtime,
2✔
344
                        'duration'  => absint( $this->get_request_duration() ),
2✔
345
                        'execution' => [
2✔
346
                                'resolvers' => $this->trace_logs,
2✔
347
                        ],
2✔
348
                ];
2✔
349

350
                /**
351
                 * Filter the trace
352
                 *
353
                 * @param array<string,mixed>      $trace     The trace to return
354
                 * @param \WPGraphQL\Utils\Tracing $instance The Tracing class instance
355
                 */
356
                return apply_filters( 'graphql_tracing_response', $trace, $this );
2✔
357
        }
358
}
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