Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.28% covered (warning)
80.28%
57 / 71
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Performance_History_Entry
80.28% covered (warning)
80.28%
57 / 71
66.67% covered (warning)
66.67%
4 / 6
35.45
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get
52.00% covered (warning)
52.00%
13 / 25
0.00% covered (danger)
0.00%
0 / 1
15.08
 get_older_history
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 get_older_window_periods
90.91% covered (success)
90.91%
20 / 22
0.00% covered (danger)
0.00%
0 / 1
12.11
 clear_cache
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2namespace Automattic\Jetpack_Boost\Data_Sync;
3
4use Automattic\Jetpack\Boost_Core\Lib\Transient;
5use Automattic\Jetpack\Boost_Speed_Score\Speed_Score_Graph_History_Request;
6use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Get;
7use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Entry_Can_Set;
8use Automattic\Jetpack\WP_JS_Data_Sync\Contracts\Lazy_Entry;
9use Automattic\Jetpack_Boost\Admin\Admin;
10use Automattic\Jetpack_Boost\Lib\Connection;
11
12class Performance_History_Entry implements Lazy_Entry, Entry_Can_Get, Entry_Can_Set {
13    /**
14     * How many older windows one request may ask about. Kept in step with
15     * olderWindowLimit in _inc/overview/lib/use-history-range.ts.
16     */
17    private const OLDER_WINDOW_LIMIT = 6;
18
19    /**
20     * Daily recording only lands scores in windows that end today, so an older window keeps
21     * whatever it holds until this expiry passes or Connection::register() clears the cache.
22     */
23    private const OLDER_WINDOW_TTL = 12 * 60 * 60;
24
25    private const CACHE_PREFIX = 'older_history_';
26
27    private $start_date;
28    private $end_date;
29    private $surface_errors = false;
30    private $older_windows  = array();
31
32    public function __construct() {
33        // Default to the last 30 days
34        $this->start_date = ( time() - 60 * 60 * 24 * 30 ) * 1000;
35        $this->end_date   = time() * 1000;
36    }
37
38    public function get( $_fallback = false ) {
39        if ( $this->older_windows ) {
40            return $this->get_older_history();
41        }
42
43        $request = new Speed_Score_Graph_History_Request( $this->start_date, $this->end_date, array() );
44        $result  = $request->execute();
45
46        if ( is_wp_error( $result ) && $this->surface_errors && Admin::is_modernization_enabled() ) {
47            throw new \RuntimeException( $result->get_error_message() );
48        }
49
50        if ( is_wp_error( $result ) || empty( $result['data'] ) ) {
51            return array(
52                'startDate'   => $this->start_date,
53                'endDate'     => $this->end_date,
54                'periods'     => array(),
55                'annotations' => array(),
56            );
57        }
58
59        $annotations = $result['data']['annotations'] ?? array();
60        // Sanitize the annotations
61        foreach ( $annotations as $key => $annotation ) {
62            $annotations[ $key ] = array(
63                'timestamp' => $annotation['timestamp'],
64                'text'      => wp_kses_post( $annotation['text'] ),
65            );
66        }
67
68        return array(
69            'startDate'   => $result['data']['_meta']['start'],
70            'endDate'     => $result['data']['_meta']['end'],
71            'periods'     => $result['data']['periods'],
72            'annotations' => $annotations,
73        );
74    }
75
76    private function get_older_history() {
77        $history = array(
78            'startDate'   => $this->start_date,
79            'endDate'     => $this->end_date,
80            'periods'     => array(),
81            'annotations' => array(),
82        );
83
84        foreach ( $this->older_windows as $window ) {
85            $periods = $this->get_older_window_periods( $window );
86            if ( $periods ) {
87                $history['periods'] = $periods;
88                break;
89            }
90        }
91
92        return $history;
93    }
94
95    private function get_older_window_periods( $window ) {
96        $cache_key = self::CACHE_PREFIX . md5( wp_json_encode( array( Connection::wpcom_blog_id(), $window ), JSON_UNESCAPED_SLASHES ) );
97        $cached    = Transient::get( $cache_key );
98        if ( null !== $cached ) {
99            return $cached;
100        }
101
102        $request = new Speed_Score_Graph_History_Request( $window['startDate'], $window['endDate'], array() );
103        $result  = $request->execute();
104        if ( is_wp_error( $result ) ) {
105            throw new \RuntimeException( $result->get_error_message() );
106        }
107
108        // A response without history reads as an empty window, matching the single-window path above.
109        $data = $result['data'] ?? array();
110        if ( ! is_array( $data ) || ( array() !== $data && ! isset( $data['periods'] ) ) ) {
111            throw new \RuntimeException( 'Invalid performance history response.' );
112        }
113        $periods = $data['periods'] ?? array();
114        if ( ! is_array( $periods ) ) {
115            throw new \RuntimeException( 'Invalid performance history periods.' );
116        }
117
118        $found = array();
119        foreach ( $periods as $period ) {
120            if ( ! isset( $period['timestamp'] ) || ! is_numeric( $period['timestamp'] ) ) {
121                throw new \RuntimeException( 'Invalid performance history timestamp.' );
122            }
123            if ( $period['timestamp'] >= $window['startDate'] && $period['timestamp'] <= $window['endDate'] ) {
124                $found[] = $period;
125            }
126        }
127
128        Transient::set( $cache_key, $found, self::OLDER_WINDOW_TTL );
129        return $found;
130    }
131
132    /**
133     * Forget every cached older window, for when registering the site alters what history exists.
134     */
135    public static function clear_cache() {
136        Transient::delete_by_prefix( self::CACHE_PREFIX );
137    }
138
139    public function set( $value ) {
140        $older_windows = $value['olderWindows'] ?? array();
141        if ( true === ( $value['checkOlderWindows'] ?? false ) && ! $older_windows ) {
142            throw new \RuntimeException( 'Older history windows are missing or malformed.' );
143        }
144        if ( count( $older_windows ) > self::OLDER_WINDOW_LIMIT ) {
145            throw new \RuntimeException( 'At most ' . self::OLDER_WINDOW_LIMIT . ' older history windows are supported.' );
146        }
147
148        $this->start_date     = $value['startDate'];
149        $this->end_date       = $value['endDate'];
150        $this->surface_errors = true === ( $value['surfaceErrors'] ?? false );
151        $this->older_windows  = $older_windows;
152    }
153}