PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.2-a.1
Jetpack – WP Security, Backup, Speed, & Growth v16.2-a.1
16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / jetpack-premium-analytics / src / Sync / class-sync-status-tracker.php
jetpack / jetpack_vendor / automattic / jetpack-premium-analytics / src / Sync Last commit date
class-configuration.php 2 weeks ago class-sync-status-tracker.php 2 weeks ago class-woocommerce-analytics-module.php 2 weeks ago trait-utilities.php 2 weeks ago
class-sync-status-tracker.php
296 lines
1 <?php
2 /**
3 * Analytics-aware sync milestone tracker.
4 *
5 * @package automattic/jetpack-premium-analytics
6 */
7
8 namespace Automattic\Jetpack\PremiumAnalytics\Sync;
9
10 use Automattic\Jetpack\Sync\Modules;
11
12 /**
13 * Listens for the end of the dashboard-gating Jetpack full sync, persists a
14 * one-time milestone option, and exposes that milestone to the frontend both at
15 * page load (via `JetpackScriptData.premium_analytics`) and live on Jetpack's
16 * `/jetpack/v4/sync/status` REST response.
17 *
18 * Which sync gates the dashboard is resolved live from `has_store_data`: the
19 * analytics-module sync ({@see INITIAL_ANALYTICS_SYNC_OPTION}) for store sites,
20 * Jetpack's generic initial full sync ({@see INITIAL_SITE_SYNC_OPTION}) otherwise.
21 * The two are tracked separately so connecting without WooCommerce and activating
22 * it later still waits for store data.
23 *
24 * Why this exists: the extracted dashboard needs to distinguish "first-time
25 * sync, please wait" from "we have data, render it." Jetpack's
26 * /jetpack/v4/sync/status reports current sync state, but not whether the
27 * gating initial sync has completed at least once. The analytics milestone also
28 * lets consumer plugins (e.g. WooCommerce Analytics) fire one-time side-effects
29 * like the full-sync-complete email, via the action hook below.
30 */
31 class Sync_Status_Tracker {
32
33 /**
34 * Milestone (unix ts) for the analytics-module initial full sync. Gates the
35 * dashboard when the site has store data. Separate from
36 * {@see INITIAL_SITE_SYNC_OPTION} so activating WooCommerce after a storeless
37 * connect still waits for store data.
38 */
39 const INITIAL_ANALYTICS_SYNC_OPTION = 'jetpack_premium_analytics_initial_analytics_sync_finished';
40
41 /**
42 * Milestone (unix ts) for Jetpack's generic initial full sync. Gates the
43 * dashboard when the site has no store data.
44 */
45 const INITIAL_SITE_SYNC_OPTION = 'jetpack_premium_analytics_initial_site_sync_finished';
46
47 /**
48 * Default sync-module names whose end-of-sync event flips the milestone.
49 * Currently provided by WooCommerce Analytics, which registers a custom
50 * full-sync module under this key. Consumers can extend or override the set
51 * via the `jetpack_premium_analytics_sync_modules` filter — see
52 * {@see get_analytics_sync_modules()}.
53 *
54 * @var string[]
55 */
56 const ANALYTICS_SYNC_MODULES = array( 'woocommerce_analytics' );
57
58 /**
59 * Action hook fired once when the analytics milestone flips (not the storeless
60 * site milestone). Consumer plugins use this to fire one-time side-effects keyed
61 * to store data (emails, tracking events, etc.).
62 *
63 * @var string
64 */
65 const MILESTONE_ACTION = 'jetpack_premium_analytics_initial_full_sync_finished';
66
67 /**
68 * Jetpack core's sync-status REST route. We enrich this existing response with
69 * the milestone rather than registering a dedicated endpoint.
70 */
71 const SYNC_STATUS_ROUTE = '/jetpack/v4/sync/status';
72
73 /**
74 * Wire up the listener, the script-data filter, and the sync-status enricher.
75 *
76 * Idempotent: safe to call more than once.
77 *
78 * @return void
79 */
80 public static function configure() {
81 add_action( 'jetpack_sync_processed_actions', array( self::class, 'on_sync_processed_actions' ) );
82 add_filter( 'jetpack_admin_js_script_data', array( self::class, 'inject_script_data' ) );
83 add_filter( 'rest_post_dispatch', array( self::class, 'enrich_sync_status_response' ), 10, 3 );
84 }
85
86 /**
87 * Append the milestone to Jetpack core's GET /jetpack/v4/sync/status response
88 * so the dashboard can read it on every poll, not just at page load.
89 *
90 * Page-load script-data ({@see inject_script_data()}) is a one-time snapshot;
91 * reading the milestone here keeps it live for in-session completion without a
92 * dedicated endpoint. Only the already-authorized, successful status payload is
93 * touched — other routes and error responses pass through untouched.
94 *
95 * @param mixed $response Result to send to the client. Usually a WP_REST_Response.
96 * @param mixed $server The REST server instance (unused).
97 * @param mixed $request The request used to generate the response.
98 * @return mixed
99 */
100 public static function enrich_sync_status_response( $response, $server, $request ) {
101 if ( ! $request instanceof \WP_REST_Request
102 || self::SYNC_STATUS_ROUTE !== $request->get_route()
103 || ! $response instanceof \WP_REST_Response
104 || $response->is_error() ) {
105 return $response;
106 }
107
108 $data = $response->get_data();
109 if ( is_array( $data ) ) {
110 $data['initial_full_sync_finished'] = self::gating_milestone( Configuration::is_woocommerce_active() );
111 $response->set_data( $data );
112 }
113
114 return $response;
115 }
116
117 /**
118 * On every batch of processed sync actions, look for the gating
119 * jetpack_full_sync_end and flip the milestone if it hasn't already fired.
120 *
121 * @param array $actions Processed sync actions.
122 * @return void
123 */
124 public static function on_sync_processed_actions( array $actions ): void {
125 $has_store_data = Configuration::is_woocommerce_active();
126
127 // Bail before the per-batch full-sync lookup ($module->get_status() bypasses
128 // the status cache) once the current mode's milestone is set. Resolving the
129 // mode live means activating WooCommerce later resumes the store milestone.
130 if ( self::milestone_reached( $has_store_data ) ) {
131 return;
132 }
133
134 $module = Modules::get_module( 'full-sync' );
135 if ( ! $module ) {
136 return;
137 }
138 '@phan-var \Automattic\Jetpack\Sync\Modules\Full_Sync_Immediately|\Automattic\Jetpack\Sync\Modules\Full_Sync $module';
139
140 self::maybe_set_milestone( $module->get_status(), $actions, $has_store_data );
141 }
142
143 /**
144 * Resolve the configured sync-module names for analytics.
145 *
146 * @return string[]
147 */
148 public static function get_analytics_sync_modules(): array {
149 /**
150 * Filter the sync-module names whose end-of-sync flips the analytics
151 * milestone. Consumer plugins that register custom full-sync modules
152 * can add their module keys here.
153 *
154 * @param string[] $module_names Default: array( 'woocommerce_analytics' ).
155 */
156 return (array) apply_filters( 'jetpack_premium_analytics_sync_modules', self::ANALYTICS_SYNC_MODULES );
157 }
158
159 /**
160 * Pure-logic counterpart to on_sync_processed_actions(): decide whether the
161 * supplied full-sync status + actions list represents the sync that gates the
162 * dashboard ending, and flip the milestone if so.
163 *
164 * Store site: only a full sync whose config includes an analytics module flips
165 * the milestone, so a generic sync can't open the gate before store data lands.
166 * Storeless site: any initial full sync ending flips it. The milestone written
167 * tracks `$has_store_data`, in lockstep with the option the frontend reads.
168 *
169 * Split out so unit tests can exercise the decision without the Jetpack sync
170 * module registry (the caller resolves `$has_store_data` live).
171 *
172 * @param array $full_status Result of Full_Sync_Immediately::get_status().
173 * @param array $actions Processed sync actions.
174 * @param bool $has_store_data Whether the site has store data to sync (WooCommerce
175 * active). Defaults to true: require the analytics module.
176 * @return void
177 */
178 public static function maybe_set_milestone( array $full_status, array $actions, bool $has_store_data = true ): void {
179 if ( self::milestone_reached( $has_store_data ) ) {
180 return;
181 }
182
183 if ( $has_store_data ) {
184 $config = isset( $full_status['config'] ) ? (array) $full_status['config'] : array();
185 $active = array_filter(
186 self::get_analytics_sync_modules(),
187 static function ( $module_name ) use ( $config ) {
188 return ! empty( $config[ $module_name ] );
189 }
190 );
191 if ( ! $active ) {
192 return;
193 }
194 }
195
196 $end_action = self::find_full_sync_end_action( $actions );
197 if ( ! $end_action ) {
198 return;
199 }
200
201 // The last update_status() call in Full_Sync_Immediately::send() runs
202 // after jetpack_full_sync_end fires, so the action's own timestamp is
203 // the most reliable "finished at" value. Note: Year 2038 problem.
204 $finished_at = isset( $end_action[3] ) ? (int) $end_action[3] : 0;
205 if ( $finished_at <= 0 ) {
206 // Defensive: avoid persisting a zero timestamp, which would equal
207 // the "not yet set" sentinel and cause the listener to re-trigger
208 // on the next batch.
209 return;
210 }
211 $full_status['finished'] = $finished_at;
212 update_option( self::gating_option( $has_store_data ), $finished_at );
213
214 if ( ! $has_store_data ) {
215 // No store data, so store-keyed consumer side-effects (e.g. WooCommerce's
216 // full-sync-complete email) must not fire — only the analytics milestone does.
217 return;
218 }
219
220 /**
221 * Fires once when the analytics-relevant initial full sync completes.
222 *
223 * @param array $full_status Final full-sync status (with `finished` timestamp).
224 */
225 do_action( self::MILESTONE_ACTION, $full_status );
226 }
227
228 /**
229 * Inject the milestone and store-data flag into JetpackScriptData so the
230 * dashboard can read them at page load without an extra HTTP roundtrip.
231 *
232 * `has_store_data` tells the frontend which sync gates the dashboard: the
233 * analytics module when WooCommerce is active, or Jetpack's generic initial
234 * full sync when it is not. Computed live so activating WooCommerce later is
235 * picked up on the next page load.
236 *
237 * @param array $data The script data passed by the assets package.
238 * @return array
239 */
240 public static function inject_script_data( array $data ): array {
241 $has_store_data = Configuration::is_woocommerce_active();
242
243 $data['premium_analytics'] = array(
244 'initial_full_sync_finished' => self::gating_milestone( $has_store_data ),
245 'has_store_data' => $has_store_data,
246 );
247
248 return $data;
249 }
250
251 /**
252 * The option name whose timestamp gates the dashboard for the given mode.
253 *
254 * @param bool $has_store_data Whether the site has store data (WooCommerce active).
255 * @return string
256 */
257 private static function gating_option( bool $has_store_data ): string {
258 return $has_store_data ? self::INITIAL_ANALYTICS_SYNC_OPTION : self::INITIAL_SITE_SYNC_OPTION;
259 }
260
261 /**
262 * The gating milestone timestamp for the given mode (0 if not yet reached).
263 *
264 * @param bool $has_store_data Whether the site has store data (WooCommerce active).
265 * @return int
266 */
267 private static function gating_milestone( bool $has_store_data ): int {
268 return (int) get_option( self::gating_option( $has_store_data ), 0 );
269 }
270
271 /**
272 * Whether the milestone gating the given mode has fired.
273 *
274 * @param bool $has_store_data Whether the site has store data (WooCommerce active).
275 * @return bool
276 */
277 private static function milestone_reached( bool $has_store_data ): bool {
278 return self::gating_milestone( $has_store_data ) > 0;
279 }
280
281 /**
282 * Find the jetpack_full_sync_end action in a processed-actions list.
283 *
284 * @param array $actions Actions list.
285 * @return array|null
286 */
287 private static function find_full_sync_end_action( array $actions ): ?array {
288 foreach ( $actions as $action ) {
289 if ( isset( $action[0] ) && 'jetpack_full_sync_end' === $action[0] ) {
290 return $action;
291 }
292 }
293 return null;
294 }
295 }
296