Flat map of metric name => scalar. */ public static function collect() { $data = array_merge( self::widget_usage(), self::block_usage(), self::campaign_usage(), self::subscription_usage(), self::feature_configuration() ); /** * Filter the usage payload before it is attached to the tracker request. * * Return an empty array to send no usage data at all while keeping the * environment/compatibility half of the tracker payload. * * @since 2.3.2 * * @param mixed $data Flat map of metric name => scalar. Typed `mixed` * because a third-party callback can return anything, * which is what the guard below is for. */ $data = apply_filters( 'better_payment/usage_data', $data ); return is_array( $data ) ? $data : array(); } /** * Elementor widget usage, keyed `widget_{name}`. * * Two sources, and they do not count the same thing — which is why the * payload says which one produced the numbers: * * 1. Elementor's own `elementor_controls_usage` aggregate. Free to read * and counts widget INSTANCES. * 2. A `_elementor_data` scan, counting POSTS that contain the widget. * * The aggregate is only populated when Elementor's usage module has seen a * post save, so a site that has not re-saved its pages since installing * Elementor has an empty option and would otherwise report no widget usage * at all — indistinguishable from genuinely using none. That blind spot is * what the fallback exists for. * * `widget_count_basis` names the source (`instances` / `posts`) so the two * are never silently averaged together on the receiving end. * * @return array */ protected static function widget_usage() { $counts = self::widget_usage_from_elementor(); if ( ! empty( $counts ) ) { $counts['widget_count_basis'] = 'instances'; return $counts; } $counts = self::widget_usage_from_post_meta(); if ( ! empty( $counts ) ) { $counts['widget_count_basis'] = 'posts'; } return $counts; } /** * Widget INSTANCE counts from Elementor's maintained usage aggregate. * * @return array */ protected static function widget_usage_from_elementor() { $usage = get_option( 'elementor_controls_usage', array() ); if ( empty( $usage ) || ! is_array( $usage ) ) { return array(); } $counts = array(); // Shape: [ doc_type => [ widget_name => [ 'count' => int, ... ] ] ]. foreach ( $usage as $elements ) { if ( ! is_array( $elements ) ) { continue; } foreach ( $elements as $widget_name => $element_data ) { if ( ! in_array( $widget_name, self::WIDGETS, true ) ) { continue; } if ( ! is_array( $element_data ) || ! isset( $element_data['count'] ) ) { continue; } $key = 'widget_' . $widget_name; $counts[ $key ] = isset( $counts[ $key ] ) ? $counts[ $key ] + (int) $element_data['count'] : (int) $element_data['count']; } } return array_filter( $counts ); } /** * Widget usage as a count of POSTS whose `_elementor_data` contains it. * * One `SUM(... LIKE ...)` pass restricted by `meta_key` (which is indexed), * so this touches only Elementor's own rows rather than all of post meta. * Matched on the JSON fragment `"widgetType":"{name}"` — a bare name match * would also hit a widget's saved settings that happen to mention it. * * @return array */ protected static function widget_usage_from_post_meta() { global $wpdb; $selects = array(); $params = array(); foreach ( self::WIDGETS as $widget_name ) { $selects[] = 'SUM( pm.meta_value LIKE %s ) AS ' . self::widget_alias( $widget_name ); // Elementor stores its tree JSON-encoded, unspaced. $params[] = '%"widgetType":"' . $wpdb->esc_like( $widget_name ) . '"%'; } // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $selects is built from a class constant; every user-facing value is a placeholder. $sql = 'SELECT ' . implode( ', ', $selects ) . " FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE pm.meta_key = '_elementor_data' AND p.post_status NOT IN ( 'trash', 'auto-draft', 'inherit' )"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared $row = $wpdb->get_row( $wpdb->prepare( $sql, $params ), ARRAY_A ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared if ( empty( $row ) ) { return array(); } $counts = array(); foreach ( self::WIDGETS as $widget_name ) { $alias = self::widget_alias( $widget_name ); $count = isset( $row[ $alias ] ) ? (int) $row[ $alias ] : 0; if ( $count > 0 ) { $counts[ 'widget_' . $widget_name ] = $count; } } return $counts; } /** * Gutenberg block usage, keyed `block_{name}` — the number of posts that * contain each block. * * One `SUM(... LIKE ...)` query rather than three `COUNT(*)`s, matched on the * block comment delimiter (``), have attributes (`{"x":1}`), or neither. $params[] = '%