PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.6
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.6
1.10.19 1.10.18 1.10.17 1.10.16 1.10.15 1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 All 163 releases
woocommerce-pos / includes / Services / Analytics_Profile.php

Analytics_Profile.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.10.6, at includes/Services/Analytics_Profile.php

291 lines 9.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Analytics site profile.
4 *
5 * Builds the property set attached to the PostHog `site` group. This is the
6 * ONLY place that decides what environment data leaves the install, and it is
7 * deliberately an allowlist: every property is named and shaped here, so no
8 * field can reach PostHog by being added to some other service's payload.
9 *
10 * Two rules the shape follows, both from the telemetry spec (#793):
11 *
12 * - Counts are reported as BANDS, never raw. "this store has 47 products" is
13 * a store metric; "this store is in the 11-100 band" answers every product
14 * question we actually ask, without carrying a fingerprint.
15 * - Payment gateways are reported as a COUNT, never as names. Which gateways a
16 * merchant runs is their business.
17 *
18 * Never add the site URL, admin URL, admin email, IP address, or any
19 * per-order value here. Landing_Profile intentionally carries `site_domain`
20 * and `admin_domain` for the updates server; those must not be copied in.
21 *
22 * @package WCPOS\WooCommercePOS\Services
23 */
24
25 namespace WCPOS\WooCommercePOS\Services;
26
27 use Automattic\WooCommerce\Utilities\OrderUtil;
28 use const WCPOS\WooCommercePOS\VERSION as PLUGIN_VERSION;
29
30 /**
31 * Analytics_Profile service class.
32 */
33 class Analytics_Profile {
34 /**
35 * Upper bound of each count band, in ascending order, mapped to its label.
36 *
37 * A count is reported with the label of the first band it fits under.
38 * Anything above the largest bound falls through to OVERFLOW_BAND.
39 */
40 const COUNT_BANDS = array(
41 '0' => 0,
42 '1-10' => 10,
43 '11-100' => 100,
44 '101-1000' => 1000,
45 );
46
47 /**
48 * Label used for counts above the largest band.
49 *
50 * @var string
51 */
52 const OVERFLOW_BAND = '1000+';
53
54 /**
55 * Classes that indicate a multi-currency plugin is running.
56 *
57 * WooCommerce core has no multi-currency concept, so presence has to be
58 * inferred from the well-known implementations. This is a best-effort
59 * signal for segmentation, not a contract — extend it via the
60 * `woocommerce_pos_analytics_multi_currency` filter rather than assuming
61 * the list is complete.
62 *
63 * @var string[]
64 */
65 const MULTI_CURRENCY_CLASSES = array(
66 'WCML_Multi_Currency', // WPML WooCommerce Multilingual.
67 'WC_Aelia_CurrencySwitcher', // Aelia Currency Switcher.
68 'WOOMULTI_CURRENCY_F', // CURCY / WooCommerce Multi Currency.
69 'Alg_WC_Currency_Switcher', // Currency Switcher for WooCommerce.
70 );
71
72 /**
73 * Build the `site` group properties.
74 *
75 * Safe to call without a logged-in user — every value is derived from
76 * site state, so the scheduled refresh can use it from cron.
77 *
78 * @return array<string, mixed>
79 */
80 public function get_group_properties(): array {
81 $metrics = ( new Landing_Profile() )->get_metrics();
82
83 $properties = array(
84 // Platform.
85 'php_version' => PHP_VERSION,
86 'wp_version' => get_bloginfo( 'version' ),
87 'wc_version' => $this->get_wc_version(),
88 'mysql_version' => $this->get_mysql_version(),
89 'wcpos_version' => PLUGIN_VERSION,
90 'wcpos_edition' => class_exists( '\WCPOS\WooCommercePOSPro\WooCommercePOSPro' ) ? 'pro' : 'free',
91
92 // Locale and market.
93 'wc_country' => $this->get_base_country(),
94 'wc_currency' => function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : '',
95 'locale' => get_locale(),
96 'timezone' => wp_timezone_string(),
97
98 // Environment shape.
99 'multisite' => is_multisite(),
100 'hpos_enabled' => $this->is_hpos_enabled(),
101 'tax_enabled' => function_exists( 'wc_tax_enabled' ) ? wc_tax_enabled() : false,
102 'multi_currency' => $this->has_multi_currency(),
103
104 // Catalogue and trading volume — banded, never raw. These are the
105 // numbers that would fingerprint a store.
106 'product_count_band' => self::band( (int) ( $metrics['product_count'] ?? 0 ) ),
107 'order_count_band' => self::band( (int) ( $metrics['order_count'] ?? 0 ) ),
108
109 // Small-cardinality operational counts, reported exactly. Banding a
110 // staff count into "1-10 / 11-100" would erase the only interesting
111 // thing about it — the difference between a one-person shop and a
112 // six-till store — and neither number identifies a store.
113 'days_since_install' => (int) ( $metrics['days_since_install'] ?? 0 ),
114 'pos_user_count' => (int) ( $metrics['pos_user_count'] ?? 0 ),
115 'gateway_count' => \count( (array) ( $metrics['active_gateways'] ?? array() ) ),
116 );
117
118 /**
119 * Filters the property set attached to the PostHog `site` group.
120 *
121 * Returned values are sent verbatim. Do not add identifying data —
122 * see the class docblock for what this surface deliberately omits.
123 *
124 * @since 1.10.0
125 *
126 * @param array<string, mixed> $properties The group properties.
127 */
128 return apply_filters( 'woocommerce_pos_analytics_group_properties', $properties );
129 }
130
131 /**
132 * Build the feature-adoption snapshot sent with `admin_landing_viewed`.
133 *
134 * One snapshot per landing view answers "what share of stores enable X"
135 * without an event per toggle. Booleans and a fixed enum only — the same
136 * allowlist discipline as the group properties, and for the same reason.
137 *
138 * `barcode_field` is deliberately reduced to default-or-custom: the raw
139 * value is a meta key the merchant chose and can name anything, so it is
140 * free-text from a store we have no business reading.
141 *
142 * @return array<string, mixed>
143 */
144 public function get_settings_summary(): array {
145 // Read through the typed accessors rather than the raw option. They are
146 // what the plugin itself acts on, so the snapshot reports the behaviour
147 // the merchant actually gets. It matters: `force_ssl` can still hold the
148 // legacy string "false" on upgraded stores, which is why its accessor —
149 // alone among the booleans — normalizes with wp_validate_boolean(). A
150 // raw (bool) cast on that value yields true and reports the opposite of
151 // what the store does.
152 $settings = Settings::instance();
153 $defaults = ( new Settings\General_Section() )->defaults();
154
155 $summary = array(
156 'pos_only_products' => $settings->pos_only_products_enabled(),
157 'decimal_qty' => $settings->decimal_qty_enabled(),
158 'force_ssl' => $settings->force_ssl_enabled(),
159 'generate_username' => $settings->generate_username_enabled(),
160 'default_customer_is_cashier' => $settings->default_customer_is_cashier(),
161 'restore_stock_on_delete' => $settings->restore_stock_on_delete_enabled(),
162 'storefront_receipt_enabled' => wp_validate_boolean( $settings->get_settings( 'general', 'storefront_receipt_enabled' ) ),
163 'barcode_field' => $defaults['barcode_field'] === $settings->barcode_field() ? 'default' : 'custom',
164 // Report the persisted answer, so the property cannot describe a
165 // consent state the merchant never chose.
166 'tracking_consent' => $settings->raw_tracking_consent(),
167 'enabled_gateway_count' => \count( (array) ( ( new Landing_Profile() )->get_metrics()['active_gateways'] ?? array() ) ),
168 );
169
170 /**
171 * Filters the feature-adoption snapshot sent with the landing view.
172 *
173 * @since 1.10.0
174 *
175 * @param array<string, mixed> $summary The settings summary.
176 */
177 return apply_filters( 'woocommerce_pos_analytics_settings_summary', $summary );
178 }
179
180 /**
181 * Map a raw count onto its reporting band.
182 *
183 * @param int $count The raw count.
184 *
185 * @return string The band label.
186 */
187 public static function band( int $count ): string {
188 foreach ( self::COUNT_BANDS as $label => $upper_bound ) {
189 if ( $count <= $upper_bound ) {
190 return $label;
191 }
192 }
193
194 return self::OVERFLOW_BAND;
195 }
196
197 /**
198 * Get the WooCommerce version, or an empty string when WC is unavailable.
199 *
200 * @return string
201 */
202 private function get_wc_version(): string {
203 if ( ! function_exists( 'WC' ) ) {
204 return '';
205 }
206
207 return (string) WC()->version;
208 }
209
210 /**
211 * Get the database server version.
212 *
213 * @return string
214 */
215 private function get_mysql_version(): string {
216 global $wpdb;
217
218 if ( ! isset( $wpdb ) || ! method_exists( $wpdb, 'db_version' ) ) {
219 return '';
220 }
221
222 return (string) $wpdb->db_version();
223 }
224
225 /**
226 * Get the WooCommerce base country.
227 *
228 * @return string
229 */
230 private function get_base_country(): string {
231 if ( ! function_exists( 'WC' ) ) {
232 return '';
233 }
234
235 return (string) WC()->countries->get_base_country();
236 }
237
238 /**
239 * Whether WooCommerce High-Performance Order Storage is in use.
240 *
241 * @return bool
242 */
243 private function is_hpos_enabled(): bool {
244 if ( ! class_exists( OrderUtil::class ) ) {
245 return false;
246 }
247
248 return OrderUtil::custom_orders_table_usage_is_enabled();
249 }
250
251 /**
252 * The class names checked to detect a multi-currency plugin.
253 *
254 * Exists as a method rather than reading the constant inline so the list is
255 * typed as plain strings. Read straight from the constant, static analysis
256 * knows the exact literals, sees that none of these third-party classes
257 * exist anywhere in this codebase, and reports the runtime check as
258 * impossible — which it is not: it is answered on the merchant's site.
259 *
260 * @return string[]
261 */
262 private function multi_currency_classes(): array {
263 return self::MULTI_CURRENCY_CLASSES;
264 }
265
266 /**
267 * Whether a known multi-currency plugin is active.
268 *
269 * @return bool
270 */
271 private function has_multi_currency(): bool {
272 $detected = false;
273
274 foreach ( $this->multi_currency_classes() as $class_name ) {
275 if ( class_exists( $class_name ) ) {
276 $detected = true;
277 break;
278 }
279 }
280
281 /**
282 * Filters whether the site is treated as running multi-currency.
283 *
284 * @since 1.10.0
285 *
286 * @param bool $detected Whether a known multi-currency plugin was found.
287 */
288 return (bool) apply_filters( 'woocommerce_pos_analytics_multi_currency', $detected );
289 }
290 }
291