get(); return array( 'schema_version' => 2, // bumped: anon_id added (landing-experiments spec §5.1). 'locale' => get_locale(), 'plugin_version' => PLUGIN_VERSION, 'pro_active' => wcpos_is_pro_active(), 'anon_id' => $anon_id, 'bootstrap_flags' => ( new Feature_Flags() )->get_landing_bootstrap_flags( $anon_id ), ); } /** * Get consent-gated data (store profile + service config). * * Only sent when the user has explicitly allowed tracking. * * @return array */ public function get_consented_data(): array { return array( 'profile' => $this->get_profile(), 'updates_server' => $this->get_updates_server_config(), ); } /** * Get the store profile (consent-gated fields only). * * Merges cached expensive metrics with cheap/user-specific fields * that are computed fresh on every call. * * @return array */ public function get_profile(): array { $cached = $this->get_metrics(); $user = wp_get_current_user(); return array_merge( $cached, array( 'wc_version' => WC()->version, 'php_version' => PHP_VERSION, 'site_uuid' => wcpos_get_site_uuid(), 'user_uuid' => get_user_meta( $user->ID, '_woocommerce_pos_uuid', true ), 'user_role' => ! empty( $user->roles ) ? $user->roles[0] : '', 'site_domain' => $this->get_url_host( home_url() ), 'admin_domain' => $this->get_url_host( admin_url() ), 'wc_currency' => get_woocommerce_currency(), 'wc_country' => WC()->countries->get_base_country(), ) ); } /** * Extract a hostname from a WordPress URL without retaining paths or query strings. * * @param string $url URL to parse. * * @return string */ private function get_url_host( string $url ): string { $host = wp_parse_url( $url, PHP_URL_HOST ); if ( ! is_string( $host ) ) { return ''; } return strtolower( $host ); } /** * Get updates server configuration. * * @return array */ public function get_updates_server_config(): array { /** * Filters the updates server profile endpoint URL. * * @since 1.9.0 * * @param string $profile_url The profile endpoint URL. */ $profile_url = apply_filters( 'woocommerce_pos_updates_server_profile_url', self::UPDATES_SERVER_URL . '/v1/profile' ); return array( 'profile_url' => $profile_url, ); } /** * Get cached expensive metrics, computing them if the cache is stale. * * Public because the analytics site profile reports the same store-size * numbers; sharing the cache keeps the count queries to one per hour * instead of one per consumer. * * @return array */ public function get_metrics(): array { $cached = get_transient( self::TRANSIENT_KEY ); if ( false !== $cached ) { return $cached; } $metrics = $this->compute_metrics(); set_transient( self::TRANSIENT_KEY, $metrics, self::CACHE_TTL ); return $metrics; } /** * Compute expensive store metrics. * * @return array */ private function compute_metrics(): array { $installed_at = get_option( 'woocommerce_pos_installed_at' ); if ( false === $installed_at ) { $installed_at = time(); add_option( 'woocommerce_pos_installed_at', $installed_at ); } $installed_at = (int) $installed_at; return array( 'days_since_install' => max( 0, (int) floor( ( time() - $installed_at ) / DAY_IN_SECONDS ) ), 'product_count' => (int) wp_count_posts( 'product' )->publish, 'order_count' => $this->get_pos_order_count(), 'pos_user_count' => $this->get_pos_user_count(), 'active_gateways' => $this->get_active_gateway_ids(), 'active_extensions' => $this->get_active_extension_slugs(), ); } /** * Count POS orders using direct SQL for performance. * * Supports both HPOS (custom orders table) and legacy post-based storage. * * @return int */ private function get_pos_order_count(): int { global $wpdb; if ( class_exists( OrderUtil::class ) && OrderUtil::custom_orders_table_usage_is_enabled() ) { /* @phpstan-ignore-next-line */ $orders_table = method_exists( OrdersTableDataStore::class, 'get_orders_table_name' ) ? OrdersTableDataStore::get_orders_table_name() : $wpdb->prefix . 'wc_orders'; /* @phpstan-ignore-next-line */ $op_table = method_exists( OrdersTableDataStore::class, 'get_operational_data_table_name' ) ? OrdersTableDataStore::get_operational_data_table_name() : $wpdb->prefix . 'wc_order_operational_data'; // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$orders_table} orders INNER JOIN {$op_table} op ON op.order_id = orders.id WHERE orders.type = 'shop_order' AND op.created_via = %s AND orders.status IN ('wc-completed', 'wc-processing', 'wc-on-hold', 'wc-pending')", 'woocommerce-pos' ) ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared } return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} p INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id WHERE p.post_type = 'shop_order' AND pm.meta_key = '_created_via' AND pm.meta_value = %s AND p.post_status IN ('wc-completed', 'wc-processing', 'wc-on-hold', 'wc-pending')", 'woocommerce-pos' ) ); } /** * Count users with POS access capability. * * @return int */ private function get_pos_user_count(): int { $users = get_users( array( 'capability' => 'access_woocommerce_pos', 'fields' => 'ID', ) ); return \count( $users ); } /** * Get IDs of enabled POS payment gateways. * * @return array */ private function get_active_gateway_ids(): array { $settings = Settings::instance()->get_payment_gateways_settings(); $gateways = $settings['gateways'] ?? array(); $active = array(); foreach ( $gateways as $id => $gw ) { if ( ! empty( $gw['enabled'] ) ) { $active[] = $id; } } return $active; } /** * Get slugs of active WCPOS extensions. * * @return array */ private function get_active_extension_slugs(): array { $extensions = Extensions::instance()->get_extensions(); $active = array(); foreach ( $extensions as $ext ) { if ( 'active' === ( $ext['status'] ?? '' ) || 'update_available' === ( $ext['status'] ?? '' ) ) { $active[] = $ext['slug'] ?? ''; } } return array_values( array_filter( $active ) ); } }