PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-premium-analytics/src/class-analytics.php +655 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,655 @@
1 +<?php
2 +/**
3 + * Analytics package main class.
4 + *
5 + * @package automattic/jetpack-premium-analytics
6 + */
7 +
8 +namespace Automattic\Jetpack\PremiumAnalytics;
9 +
10 +use Automattic\Jetpack\Admin_UI\Admin_Menu;
11 +use Automattic\Jetpack\Connection\Manager as Connection_Manager;
12 +use Automattic\Jetpack\PremiumAnalytics\Reports\Export\Export;
13 +use Automattic\Jetpack\PremiumAnalytics\REST\Api_Proxy_Controller;
14 +use Automattic\Jetpack\PremiumAnalytics\REST\Notices_Controller;
15 +use Automattic\Jetpack\PremiumAnalytics\Sync\Configuration as Sync_Configuration;
16 +use Automattic\Jetpack\PremiumAnalytics\Sync\Sync_Status_Tracker;
17 +use Automattic\Jetpack\Status\Host;
18 +use Automattic\Jetpack\WP_Build_Polyfills\WP_Build_Polyfills;
19 +
20 +/**
21 + * Main Analytics class.
22 + *
23 + * Loads the wp-build output and registers the dashboard's admin page.
24 + */
25 +class Analytics {
26 +
27 + const PACKAGE_VERSION = '0.11.0';
28 +
29 + /**
30 + * Whether the class has been initialized.
31 + *
32 + * @var bool
33 + */
34 + private static $initialized = false;
35 +
36 + /**
37 + * Menu title override for the admin page. Null falls back to the package's own
38 + * translated label, resolved on admin_menu — init runs far too early to translate.
39 + *
40 + * @var string|\Closure|null
41 + */
42 + private static $menu_title = null;
43 +
44 + /**
45 + * The menu label once resolved, so the menu and the missing-build notice can't
46 + * disagree if a caller hands us a closure that returns something different
47 + * each call. Reset whenever $menu_title is assigned.
48 + *
49 + * @var string|null
50 + */
51 + private static $resolved_menu_title = null;
52 +
53 + /**
54 + * Path to the wp-build entry point. Null uses the generated build.
55 + *
56 + * A test seam: `build/` is gitignored and test-php runs no build step, so tests redirect this
57 + * instead. Private, so — unlike the widget manifest's path — it needs no filter to stay out of reach.
58 + *
59 + * @var string|null
60 + */
61 + private static $build_entry = null;
62 +
63 + /**
64 + * Initialize the Analytics app on a connected Jetpack site.
65 + *
66 + * Registers the full local surface: the site serves the WPCOM data proxy,
67 + * notices, sync bootstrap, and the dashboard support routes itself.
68 + *
69 + * Hosts call this on every request once the flag is on, never only on admin ones: the
70 + * store-event tracker listens on the front end. {@see self::load_dashboard_surface()} is what
71 + * keeps the admin-only work off those requests.
72 + *
73 + * @param array $options Optional configuration options.
74 + * Supported keys:
75 + * - menu_title (string|\Closure): Admin menu label. Defaults to
76 + * the package's own translated label. Pass a closure to supply
77 + * a translated label of your own: it runs on admin_menu, where
78 + * a textdomain can load, unlike init time.
79 + * @return void
80 + */
81 + public static function init( $options = array() ) {
82 + if ( self::$initialized ) {
83 + return;
84 + }
85 + self::$initialized = true;
86 + self::apply_options( $options );
87 +
88 + self::register_sync_bootstrap();
89 + self::register_local_api();
90 +
91 + // Piggybacks on the Jetpack Stats module; checks Jetpack connection state.
92 + Jetpack_Stats_Tracker::configure();
93 +
94 + self::boot_shared_services();
95 + self::register_dashboard_support_routes();
96 + self::load_dashboard_surface();
97 + }
98 +
99 + /**
100 + * Load the dashboard render surface, on the requests that can render it.
101 + *
102 + * With the rollout flag on, init() runs on every request — including every WPCOM public-api
103 + * request on Simple — so this stays gated for visitors who never use it (REST excluded; see load_build()).
104 + *
105 + * @return void
106 + */
107 + private static function load_dashboard_surface() {
108 + if ( ! self::renders_admin_chrome() ) {
109 + return;
110 + }
111 +
112 + self::load_dashboard_components();
113 + self::load_build();
114 + self::remove_full_page_interceptor();
115 + self::register_admin_page();
116 + }
117 +
118 + /**
119 + * Whether this request can render an admin screen.
120 + *
121 + * Core also sets is_admin() on admin-ajax.php and admin-post.php, which render no dashboard
122 + * and get no handlers from this package, so neither needs the build parsed.
123 + *
124 + * @return bool
125 + */
126 + private static function renders_admin_chrome() {
127 + if ( ! is_admin() || wp_doing_ajax() ) {
128 + return false;
129 + }
130 +
131 + // wp-includes/vars.php sets $pagenow before plugins load.
132 + return 'admin-post.php' !== ( $GLOBALS['pagenow'] ?? '' );
133 + }
134 +
135 + /**
136 + * Initialize the Analytics app on WordPress.com Simple.
137 + *
138 + * Simple reaches public-api.wordpress.com directly via WPCOM's apiFetch bridge, registering
139 + * no local REST surface (no proxy, notices, sync bootstrap, or dashboard routes) — WPCOM handles those.
140 + *
141 + * @param array $options Optional configuration options.
142 + * Supported keys:
143 + * - menu_title (string|\Closure): Admin menu label. Defaults to
144 + * the package's own translated label. Pass a closure to supply
145 + * a translated label of your own: it runs on admin_menu, where
146 + * a textdomain can load, unlike init time.
147 + * @return void
148 + */
149 + public static function init_wpcom_simple( $options = array() ) {
150 + if ( self::$initialized ) {
151 + return;
152 + }
153 + self::$initialized = true;
154 + self::apply_options( $options );
155 +
156 + self::boot_shared_services();
157 + self::load_dashboard_surface();
158 + }
159 +
160 + /**
161 + * Apply init-time configuration options.
162 + *
163 + * @param array $options Options passed to the init entry points.
164 + * @return void
165 + */
166 + private static function apply_options( $options ) {
167 + if ( ! empty( $options['menu_title'] ) ) {
168 + self::$menu_title = $options['menu_title'];
169 + self::$resolved_menu_title = null;
170 + }
171 + }
172 +
173 + /**
174 + * Boot the services every platform needs, whether or not the site serves the
175 + * dashboard support routes itself.
176 + *
177 + * @return void
178 + */
179 + private static function boot_shared_services() {
180 + // On every request: flags are read and toggled outside the admin too.
181 + if ( ! function_exists( __NAMESPACE__ . '\\register_dashboard_feature_flags' ) ) {
182 + require_once __DIR__ . '/dashboard-policy.php';
183 + }
184 + register_dashboard_feature_flags();
185 +
186 + // Must be hooked before admin_menu and rest_api_init check the capability.
187 + Capabilities::register();
188 +
189 + // Emit WooCommerce store events into the Woo pipeline (ClickHouse + proxy).
190 + WooCommerce_Analytics_Tracker::configure();
191 +
192 + // CSV report export pipeline (WOOA7S-1581): hooks rest_api_init, so it must
193 + // register on all requests. Self-gates on WooCommerce + Jetpack connection.
194 + Export::configure();
195 +
196 + self::register_script_data();
197 +
198 + // The posts and pages list tables link their views column here.
199 + Post_List_Link::register();
200 + }
201 +
202 + /**
203 + * URL of a dashboard route on this site.
204 + *
205 + * The SPA path travels in `p`, encoded here since add_query_arg() leaves values alone and a
206 + * raw `?` inside it would read as an outer query param.
207 + *
208 + * @since 0.4.0
209 + *
210 + * @param string $path Route path, e.g. `/post/123`.
211 + * @return string
212 + */
213 + public static function dashboard_url( $path = '/' ) {
214 + return admin_url( 'admin.php?page=' . self::MENU_PAGE_SLUG . '&p=' . rawurlencode( $path ) );
215 + }
216 +
217 + /**
218 + * Announce to Jetpack's other surfaces that this dashboard is the site's analytics UI,
219 + * so they link here instead of the Stats page.
220 + *
221 + * @return void
222 + */
223 + private static function register_script_data() {
224 + add_filter( 'jetpack_admin_js_script_data', array( static::class, 'add_script_data' ) );
225 + }
226 +
227 + /**
228 + * Runs on nearly every admin page load, so the payload stays to two strings,
229 + * a bool, and one capability check.
230 + *
231 + * @param array $data The script data.
232 + * @return array The script data with the analytics key added.
233 + */
234 + public static function add_script_data( $data ) {
235 + $data['analytics'] = array(
236 + 'enabled' => true,
237 + 'page_slug' => self::MENU_PAGE_SLUG,
238 + 'can_view' => current_user_can( Capabilities::VIEW_ANALYTICS ),
239 + 'timezone' => self::site_timezone(),
240 + );
241 +
242 + return $data;
243 + }
244 +
245 + /**
246 + * Prefers `timezone_string` over `gmt_offset`, matching the dashboard's own `siteTimeZone()`:
247 + * analytics links point at past dates, so a fixed offset applied to the far side of a
248 + * daylight-saving transition shifts the day.
249 + *
250 + * @return string An IANA timezone name, or a `+HH:MM` UTC offset.
251 + */
252 + private static function site_timezone() {
253 + $timezone_string = get_option( 'timezone_string' );
254 +
255 + if ( is_string( $timezone_string ) && $timezone_string !== '' ) {
256 + return $timezone_string;
257 + }
258 +
259 + return self::format_gmt_offset( (float) get_option( 'gmt_offset' ) );
260 + }
261 +
262 + /**
263 + * Format a GMT offset in hours as `+HH:MM`.
264 + *
265 + * @param float $offset The offset in hours, e.g. 5.5 or -8.
266 + * @return string The formatted offset.
267 + */
268 + private static function format_gmt_offset( $offset ) {
269 + $sign = $offset < 0 ? '-' : '+';
270 + $absolute = abs( $offset );
271 + $hours = (int) floor( $absolute );
272 + $minutes = (int) round( ( $absolute - $hours ) * 60 );
273 +
274 + return sprintf( '%s%02d:%02d', $sign, $hours, $minutes );
275 + }
276 +
277 + /**
278 + * Register the sync services that feed the local data pipeline.
279 + *
280 + * @return void
281 + */
282 + private static function register_sync_bootstrap() {
283 + // Keep the shared connection available when another connection-owning plugin is deactivated.
284 + Connection_Configuration::configure();
285 +
286 + Sync_Status_Tracker::configure();
287 +
288 + // Opts in to the shared woocommerce_analytics sync module so Sync_Status_Tracker has a full sync to observe.
289 + Sync_Configuration::register();
290 + }
291 +
292 + /**
293 + * Register the site-served REST API: the WPCOM data proxy and notices.
294 + *
295 + * Both self-gate on their own rest_api_init hooks.
296 + *
297 + * @return void
298 + */
299 + private static function register_local_api() {
300 + Api_Proxy_Controller::register();
301 + Notices_Controller::register();
302 + }
303 +
304 + /**
305 + * Load the dashboard components every platform renders with.
306 + *
307 + * Admin-only, via load_dashboard_surface(); boot_routes() requires these
308 + * again for REST.
309 + *
310 + * @return void
311 + */
312 + private static function load_dashboard_components() {
313 + /*
314 + * Every include below is guarded on a symbol the target file declares.
315 + *
316 + * Two copies of this package can be loaded in one request — WPCOM Simple ships
317 + * one under jetpack-plugin and another under jetpack-mu-wpcom-plugin. The
318 + * autoloader dedupes classes by version, but these files declare functions and
319 + * constants at file scope, so they are absent from the classmap entirely and
320 + * reach us through `require_once`, which dedupes by path and not by symbol.
321 + * Once a class from one copy and a class from the other both run their
322 + * includes, PHP fatals on the redeclared functions. The guards make the second
323 + * copy's include a no-op, which also keeps the files' file-scope side effects
324 + * (add_filter() calls, registry bootstrapping) from running twice.
325 + */
326 +
327 + // Widget modules for the client's dynamic import() map.
328 + if ( ! function_exists( __NAMESPACE__ . '\\register_widget_modules_rest_route' ) ) {
329 + require_once __DIR__ . '/widget-modules.php';
330 + }
331 +
332 + // Default layout primitives and the bundled defaults' seed.
333 + if ( ! function_exists( __NAMESPACE__ . '\\get_dashboard_default_widget_instance' ) ) {
334 + require_once __DIR__ . '/dashboard-layout.php';
335 + }
336 +
337 + // Dashboard section API, then the package's own sections registered through it.
338 + if ( ! function_exists( __NAMESPACE__ . '\\register_dashboard_section' ) ) {
339 + require_once __DIR__ . '/dashboard-sections.php';
340 + }
341 + if ( ! function_exists( __NAMESPACE__ . '\\register_default_dashboard_sections' ) ) {
342 + require_once __DIR__ . '/default-dashboard-sections.php';
343 + }
344 + // An older copy of the package may have loaded dashboard-sections.php under the previous name.
345 + if ( function_exists( __NAMESPACE__ . '\\configure_dashboard_sections_script_data' ) ) {
346 + configure_dashboard_sections_script_data();
347 + }
348 +
349 + // Default-on CSV export settings and server-side disable filter.
350 + if ( ! function_exists( __NAMESPACE__ . '\\configure_csv_exports' ) ) {
351 + require_once __DIR__ . '/csv-exports.php';
352 + }
353 + configure_csv_exports();
354 +
355 + // VideoPress availability for the client's video routes. The widget layer
356 + // reads the same signal through widget-type-support.php.
357 + if ( ! function_exists( __NAMESPACE__ . '\\configure_videopress_availability' ) ) {
358 + require_once __DIR__ . '/videopress-availability.php';
359 + }
360 + configure_videopress_availability();
361 +
362 + // The composition flag's answer, read by the dashboard policy; the file is
363 + // already loaded by boot_shared_services().
364 + configure_dashboard_policy();
365 + }
366 +
367 + /**
368 + * Serve the dashboard support routes from the site. Simple skips this —
369 + * WPCOM calls Dashboard_Support_Routes::register() itself instead.
370 + *
371 + * @return void
372 + */
373 + private static function register_dashboard_support_routes() {
374 + Dashboard_Support_Routes::register();
375 + }
376 +
377 + /**
378 + * Load the wp-build output (interceptor, modules, routes, page render).
379 + *
380 + * Admin-only, via load_dashboard_surface(). REST does not need it:
381 + * boot_routes() and ensure_widget_registry_ready() load what they use.
382 + *
383 + * @return void
384 + */
385 + private static function load_build() {
386 + $build_entry = self::$build_entry ?? __DIR__ . '/../build/build.php';
387 + if ( file_exists( $build_entry ) ) {
388 + require_once $build_entry;
389 + require_once __DIR__ . '/sdk-module.php';
390 + }
391 + }
392 +
393 + /**
394 + * Unhook wp-build's full-page render interceptor — security-relevant: it renders
395 + * `?page=jetpack-premium-analytics` from admin_init with no capability check, and only
396 + * renders_admin_chrome() gates the admin-post.php/admin-ajax.php paths that reach admin_init
397 + * without Core's own slug check.
398 + *
399 + * Because remove_action() no-ops on a callback name it can't find, a wp-build rename would
400 + * silently restore this entry point — hence the _doing_it_wrong() below when that happens.
401 + *
402 + * @return void
403 + */
404 + private static function remove_full_page_interceptor() {
405 + if ( remove_action( 'admin_init', 'jpa_jetpack_premium_analytics_intercept_render' ) ) {
406 + return;
407 + }
408 +
409 + if ( function_exists( 'jpa_jetpack_premium_analytics_intercept_render' ) ) {
410 + _doing_it_wrong(
411 + __METHOD__,
412 + 'The Premium Analytics full-page interceptor could not be unhooked: wp-build changed the generated callback name or its admin_init priority.',
413 + ''
414 + );
415 + }
416 + }
417 +
418 + /**
419 + * Absolute path to the generated widget manifest.
420 + *
421 + * On the class, not beside its readers in widget-modules.php: two copies of this package can
422 + * load in one request, and only classes get the autoloader's version dedupe (see load_dashboard_components()).
423 + *
424 + * @return string
425 + */
426 + public static function widget_manifest_path() {
427 + /**
428 + * Filters the path to the generated widget manifest.
429 + *
430 + * @param string $path Absolute path to the generated widget manifest.
431 + */
432 + return apply_filters(
433 + 'jetpack_premium_analytics_widgets_manifest_path',
434 + __DIR__ . '/../build/widgets.php'
435 + );
436 + }
437 +
438 + /**
439 + * Register the admin-only render path: polyfills, menu, and page hooks.
440 + *
441 + * @return void
442 + */
443 + private static function register_admin_page() {
444 + // Polyfills force-replace core handles (wp-private-apis) on wp_default_scripts;
445 + // scope to the dashboard page so no other admin page (e.g. block editor) is hit.
446 + if ( self::is_dashboard_request() ) {
447 + WP_Build_Polyfills::register(
448 + 'jetpack-premium-analytics',
449 + array_merge(
450 + WP_Build_Polyfills::SCRIPT_HANDLES,
451 + WP_Build_Polyfills::MODULE_IDS
452 + )
453 + );
454 +
455 + add_action( 'admin_enqueue_scripts', array( static::class, 'enqueue_i18n_loader' ) );
456 + add_action( 'admin_enqueue_scripts', array( static::class, 'enqueue_tracks_transport' ) );
457 + add_filter( 'jetpack_admin_js_script_data', array( static::class, 'add_tracks_identity_script_data' ), 20 );
458 + }
459 +
460 + add_action( 'admin_menu', array( static::class, 'register_admin_menu' ) );
461 + }
462 +
463 + /**
464 + * The admin page slug the dashboard menu registers. Published in script data
465 + * so no caller has to hard-code it.
466 + */
467 + const MENU_PAGE_SLUG = 'jetpack-premium-analytics-wp-admin';
468 +
469 + /**
470 + * Whether the current request is rendering the Premium Analytics dashboard.
471 + *
472 + * Scopes the wp-build polyfill registration (which force-replaces core script handles) to
473 + * this dashboard; reads the menu slug directly, not current_screen, to stay safe at plugin-load time.
474 + *
475 + * @return bool True when serving the dashboard page in wp-admin.
476 + */
477 + public static function is_dashboard_request() {
478 + if ( ! is_admin() ) {
479 + return false;
480 + }
481 +
482 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading the menu page slug to scope asset loading; no state is changed.
483 + $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : '';
484 +
485 + return self::MENU_PAGE_SLUG === $page;
486 + }
487 +
488 + /**
489 + * Register the admin menu page.
490 + *
491 + * Uses wp-build's `-wp-admin` variant so Core applies the menu capability check. Reports the
492 + * page and widget artifacts independently since the build loader includes each conditionally.
493 + *
494 + * Queued through Admin_Menu rather than registered here, so the entry is reachable by the
495 + * `jetpack_admin_menu_visibility` filter.
496 + *
497 + * @return void
498 + */
499 + public static function register_admin_menu() {
500 + $can_render = function_exists( 'jpa_jetpack_premium_analytics_wp_admin_render_page' );
501 + $has_widget_manifest = file_exists( self::widget_manifest_path() );
502 +
503 + $missing = array();
504 + if ( ! $can_render ) {
505 + // Named by symbol, not by file: build/pages.php is only a loader, and the
506 + // callback can also go missing to a renamed page slug or an absent build entry.
507 + $missing[] = 'the jpa_jetpack_premium_analytics_wp_admin_render_page() callback, generated under build/pages/';
508 + }
509 + if ( ! $has_widget_manifest ) {
510 + $missing[] = 'build/widgets.php (the widget manifest)';
511 + }
512 +
513 + if ( $missing ) {
514 + // Surfaced here rather than only on the page itself, so a partial deploy shows up on
515 + // the first admin request instead of waiting for someone to open the dashboard.
516 + _doing_it_wrong(
517 + __METHOD__,
518 + // esc_html() only to satisfy WordPress.Security.EscapeOutput, which treats
519 + // this argument as output; every entry is a literal from just above.
520 + 'The Premium Analytics build output is incomplete: ' . esc_html( implode( ', ', $missing ) ) . '. The package build did not run, or ran only partially, for this deploy.',
521 + ''
522 + );
523 + }
524 +
525 + $render_callback = $can_render
526 + ? 'jpa_jetpack_premium_analytics_wp_admin_render_page'
527 + : array( __CLASS__, 'render_missing_build_notice' );
528 +
529 + $menu_title = self::menu_title();
530 +
531 + $menu_title = esc_html( $menu_title );
532 +
533 + // An older admin-ui, loaded first by another plugin, may predate add_top_level_menu().
534 + if ( ! method_exists( Admin_Menu::class, 'add_top_level_menu' ) ) {
535 + add_menu_page( $menu_title, $menu_title, Capabilities::VIEW_ANALYTICS, self::MENU_PAGE_SLUG, $render_callback, 'dashicons-chart-bar', 2 );
536 + return;
537 + }
538 +
539 + // A fixed key rather than the slug, which carries a build-specific suffix. No gate:
540 + // the dashboard has no My Jetpack product class and no module to name.
541 + Admin_Menu::add_top_level_menu( $menu_title, $menu_title, Capabilities::VIEW_ANALYTICS, self::MENU_PAGE_SLUG, $render_callback, 'dashicons-chart-bar', 2, array( 'key' => 'jetpack-premium-analytics' ) );
542 + }
543 +
544 + /**
545 + * Stand-in for the generated render callback when the build output is absent.
546 + *
547 + * The PHP classes come from Composer and the build output from pnpm, so a
548 + * partial deploy can leave the class loadable with nothing to render.
549 + *
550 + * @return void
551 + */
552 + public static function render_missing_build_notice() {
553 + printf(
554 + '<div class="wrap"><h1>%s</h1><p>%s</p></div>',
555 + esc_html( self::menu_title() ),
556 + esc_html__( 'The Premium Analytics assets are missing. The package build did not run for this deploy.', 'jetpack-premium-analytics-pkg' )
557 + );
558 + }
559 +
560 + /**
561 + * The caller's menu label override, or the package's own translated label.
562 + *
563 + * Call only once translations can load — admin_menu or later — and memoize so every call site
564 + * agrees. Deliberately not is_callable(): PHP function names are case-insensitive, so a plain
565 + * label like "Analytics" could match a stray analytics() function and get called.
566 + *
567 + * @return string
568 + */
569 + private static function menu_title() {
570 + if ( null !== self::$resolved_menu_title ) {
571 + return self::$resolved_menu_title;
572 + }
573 +
574 + $title = self::$menu_title instanceof \Closure
575 + ? ( self::$menu_title )()
576 + : self::$menu_title;
577 +
578 + // A positive check rather than a null coalesce: a closure may return an empty string, or
579 + // something that isn't a string at all, and either would reach esc_html() as a broken label.
580 + self::$resolved_menu_title = is_string( $title ) && '' !== $title
581 + ? $title
582 + : __( 'Stats v2', 'jetpack-premium-analytics-pkg' );
583 +
584 + return self::$resolved_menu_title;
585 + }
586 +
587 + /**
588 + * Enqueue the i18n loader so the wp-build init module can download its JS
589 + * translation catalogs. It's registered on every admin page by jetpack-assets
590 + * but only enqueued when depended on; the esbuild bundles don't pull it in.
591 + *
592 + * @return void
593 + */
594 + public static function enqueue_i18n_loader() {
595 + if ( wp_script_is( 'wp-jp-i18n-loader', 'registered' ) ) {
596 + wp_enqueue_script( 'wp-jp-i18n-loader' );
597 + }
598 + }
599 +
600 + /**
601 + * Load the Tracks transport for the dashboard.
602 + *
603 + * `@automattic/jetpack-analytics` only queues events into `window._tkq` — its own w.js
604 + * loader is disabled — so without this handle no `jetpack_premium_analytics_*` event
605 + * ever flushes. Simple is skipped because stats.php already prints the same script.
606 + *
607 + * @return void
608 + */
609 + public static function enqueue_tracks_transport() {
610 + if ( ( new Host() )->is_wpcom_simple() ) {
611 + return;
612 + }
613 +
614 + wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
615 + }
616 +
617 + /**
618 + * Publish the WPCOM identity the dashboard attributes its Tracks events to.
619 + *
620 + * Core's script data carries only the local user. Publicize is the one package that fills
621 + * `current_user.wpcom` in, and the standalone plugin does not bundle it, so without this
622 + * every event would land anonymous there.
623 + *
624 + * @param array $data The script data.
625 + * @return array The script data with the WPCOM identity added.
626 + */
627 + public static function add_tracks_identity_script_data( $data ) {
628 + if ( ( new Host() )->is_wpcom_simple() ) {
629 + $wpcom_user = array(
630 + 'ID' => get_current_user_id(),
631 + 'login' => wp_get_current_user()->user_login,
632 + );
633 + } else {
634 + $connected = ( new Connection_Manager() )->get_connected_user_data();
635 +
636 + if ( empty( $connected['ID'] ) || empty( $connected['login'] ) ) {
637 + return $data;
638 + }
639 +
640 + // Only the two fields `identifyUser` needs: the rest of the connected-user payload
641 + // is profile data the dashboard never reads.
642 + $wpcom_user = array(
643 + 'ID' => $connected['ID'],
644 + 'login' => $connected['login'],
645 + );
646 + }
647 +
648 + $data['user']['current_user']['wpcom'] = array_merge(
649 + $data['user']['current_user']['wpcom'] ?? array(),
650 + $wpcom_user
651 + );
652 +
653 + return $data;
654 + }
655 +}