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        /** This filter is documented in app/admin/class-admin.php */
47        if ( is_wp_error( $result ) && $this->surface_errors && apply_filters( Admin::MODERNIZATION_FILTER, true ) ) {
48            throw new \RuntimeException( $result->get_error_message() );
49        }
50
51        if ( is_wp_error( $result ) || empty( $result['data'] ) ) {
52            return array(
53                'startDate'   => $this->start_date,
54                'endDate'     => $this->end_date,
55                'periods'     => array(),
56                'annotations' => array(),
57            );
58        }
59
60        $annotations = $result['data']['annotations'] ?? array();
61        // Sanitize the annotations
62        foreach ( $annotations as $key => $annotation ) {
63            $annotations[ $key ] = array(
64                'timestamp' => $annotation['timestamp'],
65                'text'      => wp_kses_post( $annotation['text'] ),
66            );
67        }
68
69        return array(
70            'startDate'   => $result['data']['_meta']['start'],
71            'endDate'     => $result['data']['_meta']['end'],
72            'periods'     => $result['data']['periods'],
73            'annotations' => $annotations,
74        );
75    }
76
77    private function get_older_history() {
78        $history = array(
79            'startDate'   => $this->start_date,
80            'endDate'     => $this->end_date,
81            'periods'     => array(),
82            'annotations' => array(),
83        );
84
85        foreach ( $this->older_windows as $window ) {
86            $periods = $this->get_older_window_periods( $window );
87            if ( $periods ) {
88                $history['periods'] = $periods;
89                break;
90            }
91        }
92
93        return $history;
94    }
95
96    private function get_older_window_periods( $window ) {
97        $cache_key = self::CACHE_PREFIX . md5( wp_json_encode( array( Connection::wpcom_blog_id(), $window ), JSON_UNESCAPED_SLASHES ) );
98        $cached    = Transient::get( $cache_key );
99        if ( null !== $cached ) {
100            return $cached;
101        }
102
103        $request = new Speed_Score_Graph_History_Request( $window['startDate'], $window['endDate'], array() );
104        $result  = $request->execute();
105        if ( is_wp_error( $result ) ) {
106            throw new \RuntimeException( $result->get_error_message() );
107        }
108
109        // A response without history reads as an empty window, matching the single-window path above.
110        $data = $result['data'] ?? array();
111        if ( ! is_array( $data ) || ( array() !== $data && ! isset( $data['periods'] ) ) ) {
112            throw new \RuntimeException( 'Invalid performance history response.' );
113        }
114        $periods = $data['periods'] ?? array();
115        if ( ! is_array( $periods ) ) {
116            throw new \RuntimeException( 'Invalid performance history periods.' );
117        }
118
119        $found = array();
120        foreach ( $periods as $period ) {
121            if ( ! isset( $period['timestamp'] ) || ! is_numeric( $period['timestamp'] ) ) {
122                throw new \RuntimeException( 'Invalid performance history timestamp.' );
123            }
124            if ( $period['timestamp'] >= $window['startDate'] && $period['timestamp'] <= $window['endDate'] ) {
125                $found[] = $period;
126            }
127        }
128
129        Transient::set( $cache_key, $found, self::OLDER_WINDOW_TTL );
130        return $found;
131    }
132
133    /**
134     * Forget every cached older window, for when registering the site alters what history exists.
135     */
136    public static function clear_cache() {
137        Transient::delete_by_prefix( self::CACHE_PREFIX );
138    }
139
140    public function set( $value ) {
141        $older_windows = $value['olderWindows'] ?? array();
142        if ( true === ( $value['checkOlderWindows'] ?? false ) && ! $older_windows ) {
143            throw new \RuntimeException( 'Older history windows are missing or malformed.' );
144        }
145        if ( count( $older_windows ) > self::OLDER_WINDOW_LIMIT ) {
146            throw new \RuntimeException( 'At most ' . self::OLDER_WINDOW_LIMIT . ' older history windows are supported.' );
147        }
148
149        $this->start_date     = $value['startDate'];
150        $this->end_date       = $value['endDate'];
151        $this->surface_errors = true === ( $value['surfaceErrors'] ?? false );
152        $this->older_windows  = $older_windows;
153    }
154}