Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
51.32% covered (warning)
51.32%
39 / 76
42.86% covered (danger)
42.86%
6 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
Analytics
51.32% covered (warning)
51.32%
39 / 76
42.86% covered (danger)
42.86%
6 / 14
97.12
0.00% covered (danger)
0.00%
0 / 1
 init
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
2.00
 init_wpcom_simple
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 apply_options
50.00% covered (danger)
50.00%
1 / 2
0.00% covered (danger)
0.00%
0 / 1
2.50
 boot_shared_services
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 register_sync_bootstrap
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 register_local_api
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 load_dashboard_components
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 register_dashboard_support_routes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 load_build
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 register_admin_page
15.38% covered (danger)
15.38%
2 / 13
0.00% covered (danger)
0.00%
0 / 1
8.45
 is_dashboard_request
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 register_admin_menu
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 register_sidebar_items
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 ensure_script_data
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Analytics package main class.
4 *
5 * @package automattic/jetpack-premium-analytics
6 */
7
8namespace Automattic\Jetpack\PremiumAnalytics;
9
10use Automattic\Jetpack\PremiumAnalytics\Reports\Export\Export;
11use Automattic\Jetpack\PremiumAnalytics\REST\Api_Proxy_Controller;
12use Automattic\Jetpack\PremiumAnalytics\REST\Notices_Controller;
13use Automattic\Jetpack\PremiumAnalytics\Sync\Configuration as Sync_Configuration;
14use Automattic\Jetpack\PremiumAnalytics\Sync\Sync_Status_Tracker;
15use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
16
17/**
18 * Main Analytics class.
19 *
20 * Loads the wp-build output and registers an admin page.
21 * The build interceptor handles full-page rendering via admin_init.
22 */
23class Analytics {
24
25    const PACKAGE_VERSION = '0.1.0-alpha';
26
27    /**
28     * Whether the class has been initialized.
29     *
30     * @var bool
31     */
32    private static $initialized = false;
33
34    /**
35     * Menu title for the admin page.
36     *
37     * @var string
38     */
39    private static $menu_title = 'Analytics';
40
41    /**
42     * Initialize the Analytics app on a connected Jetpack site.
43     *
44     * Registers the full local surface: the site serves the WPCOM data proxy,
45     * notices, sync bootstrap, and the dashboard support routes itself.
46     *
47     * @param array $options Optional configuration options.
48     *                       Supported keys:
49     *                       - menu_title (string): Admin menu label.
50     * @return void
51     */
52    public static function init( $options = array() ) {
53        if ( self::$initialized ) {
54            return;
55        }
56        self::$initialized = true;
57        self::apply_options( $options );
58
59        self::register_sync_bootstrap();
60        self::register_local_api();
61
62        // Piggybacks on the Jetpack Stats module; checks Jetpack connection state.
63        Jetpack_Stats_Tracker::configure();
64
65        self::boot_shared_services();
66        self::register_dashboard_support_routes();
67        self::load_build();
68        self::register_admin_page();
69    }
70
71    /**
72     * Initialize the Analytics app on WordPress.com Simple.
73     *
74     * Simple reaches public-api.wordpress.com directly via WPCOM's apiFetch
75     * bridge, so it registers no local REST surface: no proxy, notices, sync
76     * bootstrap, or dashboard support routes. WPCOM registers those separately.
77     *
78     * @param array $options Optional configuration options.
79     *                       Supported keys:
80     *                       - menu_title (string): Admin menu label.
81     * @return void
82     */
83    public static function init_wpcom_simple( $options = array() ) {
84        if ( self::$initialized ) {
85            return;
86        }
87        self::$initialized = true;
88        self::apply_options( $options );
89
90        self::boot_shared_services();
91        self::load_build();
92        self::register_admin_page();
93    }
94
95    /**
96     * Apply init-time configuration options.
97     *
98     * @param array $options Options passed to the init entry points.
99     * @return void
100     */
101    private static function apply_options( $options ) {
102        if ( ! empty( $options['menu_title'] ) ) {
103            self::$menu_title = $options['menu_title'];
104        }
105    }
106
107    /**
108     * Boot the services and registries every platform needs, regardless of
109     * whether the site serves the dashboard support routes itself.
110     *
111     * @return void
112     */
113    private static function boot_shared_services() {
114        // Emit WooCommerce store events into the Woo pipeline (ClickHouse + proxy).
115        WooCommerce_Analytics_Tracker::configure();
116
117        // CSV report export pipeline (WOOA7S-1581): hooks rest_api_init, so it must
118        // register on all requests. Self-gates on WooCommerce + Jetpack connection.
119        Export::configure();
120
121        self::load_dashboard_components();
122    }
123
124    /**
125     * Register the sync services that feed the local data pipeline.
126     *
127     * @return void
128     */
129    private static function register_sync_bootstrap() {
130        // Keep the shared connection available when another connection-owning plugin is deactivated.
131        Connection_Configuration::configure();
132
133        Sync_Status_Tracker::configure();
134
135        // TEMPORARY (WOOA7S-1550): register the interim woocommerce_analytics sync module so
136        // Sync_Status_Tracker has a full sync to observe. Remove when the shared sync-modules package lands.
137        Sync_Configuration::register();
138    }
139
140    /**
141     * Register the site-served REST API: the WPCOM data proxy and notices.
142     *
143     * Both self-gate on their own rest_api_init hooks.
144     *
145     * @return void
146     */
147    private static function register_local_api() {
148        Api_Proxy_Controller::register();
149        Notices_Controller::register();
150    }
151
152    /**
153     * Load the dashboard components every platform renders with.
154     *
155     * @return void
156     */
157    private static function load_dashboard_components() {
158        // Widget modules for the client's dynamic import() map.
159        require_once __DIR__ . '/widget-modules.php';
160
161        // Default layout's first-load preference injection.
162        require_once __DIR__ . '/dashboard-layout.php';
163
164        // Dashboard sections and their default layout seeding.
165        require_once __DIR__ . '/dashboard-sections.php';
166
167        // Opt-in CSV export settings.
168        require_once __DIR__ . '/csv-exports.php';
169        configure_csv_exports();
170    }
171
172    /**
173     * Serve the dashboard support routes from the site. Simple skips this —
174     * WPCOM calls Dashboard_Support_Routes::register() itself instead.
175     *
176     * @return void
177     */
178    private static function register_dashboard_support_routes() {
179        Dashboard_Support_Routes::register();
180    }
181
182    /**
183     * Load the wp-build output (interceptor, modules, routes, page render).
184     *
185     * Must run before the is_admin() gate: the registry serves REST requests
186     * too (is_admin() false there). Render pieces self-gate on admin_init, so
187     * this is inert off the dashboard.
188     *
189     * @return void
190     */
191    private static function load_build() {
192        $build_entry = __DIR__ . '/../build/build.php';
193        if ( file_exists( $build_entry ) ) {
194            require_once $build_entry;
195        }
196    }
197
198    /**
199     * Register the admin-only render path: polyfills, menu, and page hooks.
200     *
201     * @return void
202     */
203    private static function register_admin_page() {
204        if ( ! is_admin() ) {
205            return;
206        }
207
208        // Polyfills force-replace core handles (wp-private-apis) on wp_default_scripts;
209        // scope to the dashboard page so no other admin page (e.g. block editor) is hit.
210        if ( self::is_dashboard_request() ) {
211            WP_Build_Polyfills::register(
212                'jetpack-premium-analytics',
213                array_merge(
214                    WP_Build_Polyfills::SCRIPT_HANDLES,
215                    WP_Build_Polyfills::MODULE_IDS
216                )
217            );
218        }
219
220        add_action( 'admin_menu', array( static::class, 'register_admin_menu' ) );
221        add_action( 'jetpack-premium-analytics_init', array( static::class, 'register_sidebar_items' ) );
222        add_action( 'jetpack-premium-analytics_init', array( static::class, 'ensure_script_data' ) );
223    }
224
225    /**
226     * Admin page slugs that render the Premium Analytics dashboard.
227     *
228     * Mirrors the slugs the wp-build interceptor renders (full-page and the
229     * wp-admin integrated variant).
230     */
231    const DASHBOARD_PAGE_SLUGS = array( 'jetpack-premium-analytics', 'jetpack-premium-analytics-wp-admin' );
232
233    /**
234     * Whether the current request is rendering a Premium Analytics dashboard page.
235     *
236     * Used to scope the wp-build polyfill registration (which force-replaces core
237     * script handles) to this dashboard, so it never affects other admin pages.
238     * Must be cheap and safe to call at plugin-load time, before current_screen
239     * exists, so it reads the menu page slug directly like the build interceptor does.
240     *
241     * @return bool True when serving a dashboard page in wp-admin.
242     */
243    public static function is_dashboard_request() {
244        if ( ! is_admin() ) {
245            return false;
246        }
247
248        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading the menu page slug to scope asset loading; no state is changed.
249        $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
250
251        return in_array( $page, self::DASHBOARD_PAGE_SLUGS, true );
252    }
253
254    /**
255     * Register the admin menu page.
256     *
257     * Uses the wp-build "wp-admin integrated" variant (`-wp-admin` slug) so the
258     * dashboard renders inside the native wp-admin shell, not the full-page
259     * variant that takes over the screen via admin_init. The render callback
260     * comes from the generated build, with a no-op fallback when it is absent.
261     *
262     * @return void
263     */
264    public static function register_admin_menu() {
265        $render_callback = function_exists( 'jpa_jetpack_premium_analytics_wp_admin_render_page' )
266            ? 'jpa_jetpack_premium_analytics_wp_admin_render_page'
267            : '__return_null';
268
269        add_menu_page(
270            esc_html( self::$menu_title ),
271            esc_html( self::$menu_title ),
272            'manage_options',
273            'jetpack-premium-analytics-wp-admin',
274            $render_callback,
275            'dashicons-chart-bar',
276            2
277        );
278    }
279
280    /**
281     * Register sidebar menu items for the full-page app.
282     *
283     * @return void
284     */
285    public static function register_sidebar_items() {
286        if ( ! function_exists( 'jpa_register_jetpack_premium_analytics_menu_item' ) ) {
287            return;
288        }
289
290        // @phan-suppress-next-line PhanUndeclaredFunction -- Guarded by function_exists() above.
291        jpa_register_jetpack_premium_analytics_menu_item(
292            'dashboard',
293            __( 'Dashboard', 'jetpack-premium-analytics' ),
294            '/'
295        );
296    }
297
298    /**
299     * Emit window.JetpackScriptData on the boot-rendered admin page.
300     *
301     * The wp-build interceptor that renders this page (its page.php template)
302     * reproduces wp-admin/admin-header.php but does not fire the
303     * `admin_print_scripts` action. The jetpack-assets Script_Data class hooks
304     * that action to print `window.JetpackScriptData` — which carries the
305     * connection data the route guards read — so without help the global is
306     * never emitted and the guards cannot tell whether the site is connected.
307     *
308     * Hooked on the page's own init action, this runs only for this page, in
309     * time for the footer scripts to print. Script_Data guards against rendering
310     * twice, so it is a no-op wherever `admin_print_scripts` fires normally.
311     *
312     * @return void
313     */
314    public static function ensure_script_data() {
315        $script_data = 'Automattic\Jetpack\Assets\Script_Data';
316        if ( is_callable( array( $script_data, 'render_script_data' ) ) ) {
317            $script_data::render_script_data();
318        }
319    }
320}