PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 8.5.74
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v8.5.74
9.1.3 9.1.2 9.1.1 9.1.0 9.0.3 9.0.2 9.0.1 9.0.0 8.5.79 8.5.78 8.5.77 8.5.76 8.5.75 8.5.74 8.5.73 8.5.72 8.5.71 8.5.70 8.5.69 8.5.68 8.5.35 8.5.36 8.5.37 8.5.38 8.5.39 All 222 releases
wpvr / includes / class-wpvr-linno-telemetry.php

class-wpvr-linno-telemetry.php in WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress 8.5.74, at includes/class-wpvr-linno-telemetry.php

429 lines 18.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 use LinnoSDK\Telemetry\Client;
3 if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly
4
5 /**
6 * Class WPVR_Linno_Telemetry
7 *
8 * Telemetry event bridge for the Linno SDK.
9 *
10 * Canonical events tracked:
11 * - activation/plugin_activated — SDK lifecycle, no consent (automatic).
12 * - activation/plugin_deactivated — SDK lifecycle, no consent (automatic).
13 * - activation/onboarding_completed — non-PII, override (track_immediate).
14 * - activation/aha_reached — consent-gated; wizard completion fires on consent grant,
15 * active tour views fire via define_triggers → aha.
16 * - retention/feature_used — consent-gated (define_triggers → feature_used).
17 *
18 * @since 8.5.57
19 */
20
21 class WPVR_Linno_Telemetry {
22
23 /**
24 * Linno API key.
25 *
26 * @var string
27 */
28 private $api_key;
29
30 /**
31 * Linno API secret.
32 *
33 * @var string
34 */
35 private $api_secret;
36
37 /**
38 * Prevent duplicate client initialization.
39 *
40 * @var bool
41 */
42 private $client_initialized = false;
43
44 /**
45 * Constructor.
46 */
47 public function __construct() {
48 $this->api_key = defined( 'WPVR_TELEMETRY_API_KEY' ) ? WPVR_TELEMETRY_API_KEY : '';
49 $this->api_secret = defined( 'WPVR_TELEMETRY_API_SECRET' ) ? WPVR_TELEMETRY_API_SECRET : '';
50
51 $this->init_client();
52 add_filter( 'wpvr_telemetry_report_interval', array( $this, 'set_daily_telemetry_report_interval' ) );
53 add_action( 'rex_wpvr_embadded_tour', array( $this, 'handle_embedded_tour_view' ), 10, 1 );
54 add_action( 'wpvr_telemetry_consent_granted', array( $this, 'track_aha_after_consent' ) );
55 add_filter( 'wpvr_telemetry_deactivation_reasons', array( $this, 'set_deactivation_reasons' ) );
56 }
57
58 /**
59 * Force Linno telemetry queue interval to daily.
60 *
61 * @return string
62 */
63 public function set_daily_telemetry_report_interval() {
64 return 'daily';
65 }
66
67 /**
68 * Initialize Linno telemetry client and trigger mappings.
69 *
70 * @return void
71 */
72 public function init_client() {
73 global $wpvr_telemetry;
74
75 if ( $this->client_initialized ) {
76 return;
77 }
78
79 if ( ! class_exists( 'LinnoSDK\\Telemetry\\Client' ) || ! defined( 'WPVR_FILE' ) ) {
80 error_log( 'WPVR Telemetry: Linno Client class unavailable or WPVR_FILE not defined.' );
81 return;
82 }
83
84 if ( empty( $this->api_key ) || empty( $this->api_secret ) ) {
85 error_log( 'WPVR Telemetry: API key or API secret is empty. Telemetry client not initialized.' );
86 return;
87 }
88
89 Client::set_text_domain( 'wpvr' );
90
91 $wpvr_telemetry = new Client(
92 array(
93 'pluginFile' => WPVR_FILE,
94 'slug' => 'wpvr',
95 'pluginName' => 'WP VR',
96 'version' => defined( 'WPVR_VERSION' ) ? WPVR_VERSION : '',
97 'apiKey' => $this->api_key,
98 'apiSecret' => $this->api_secret,
99 'driver' => 'posthog',
100 'driver_config' => array(
101 'host' => defined( 'WPVR_TELEMETRY_HOST' ) ? WPVR_TELEMETRY_HOST : 'https://eu.i.posthog.com',
102 'api_key' => $this->api_key,
103 ),
104 )
105 );
106
107 // Non-PII onboarding event — bypasses consent via override=true on track_immediate().
108 add_action( 'wpvr_setup_wizard_completed_event', array( $this, 'track_onboarding_event' ), 10, 1 );
109
110 // Consent-gated events via SDK trigger system.
111 // activation/aha_reached — queued when ≥60% of tours are actively viewed (recurring).
112 // retention/feature_used — queued on every tour save.
113 // Both require opt-in consent.
114 $self = $this;
115 $wpvr_telemetry->define_triggers(
116 array(
117 'aha' => array(
118 'tours_actively_viewed' => array(
119 'hook' => 'wpvr_kui_unique_views_updated',
120 'callback' => function ( $unique_views, $tour_id ) use ( $self ) {
121 return $self->build_kui_payload( $unique_views, $tour_id );
122 },
123 ),
124 ),
125 'feature_used' => array(
126 'tour_creation' => array(
127 'hook' => 'wpvr_tour_settings_saved',
128 'callback' => function ( $tour_id ) use ( $self ) {
129 return $self->build_tour_creation_payload( $tour_id );
130 },
131 ),
132 ),
133 )
134 );
135
136 $this->ensure_daily_telemetry_queue_schedule();
137 $this->client_initialized = true;
138 }
139
140 /**
141 * Ensure telemetry queue cron hook runs daily.
142 *
143 * @return void
144 */
145 private function ensure_daily_telemetry_queue_schedule() {
146 if ( ! defined( 'WPVR_FILE' ) ) {
147 return;
148 }
149
150 $slug = dirname( plugin_basename( WPVR_FILE ) );
151 $hook = $slug . '_telemetry_queue_process';
152 $scheduled_event = wp_get_scheduled_event( $hook );
153
154 if ( $scheduled_event && 'daily' === $scheduled_event->schedule ) {
155 return;
156 }
157
158 wp_clear_scheduled_hook( $hook );
159 wp_schedule_event( time(), 'daily', $hook );
160 }
161
162 /**
163 * Build setup event payload.
164 *
165 * @param string $industry Industry from setup wizard.
166 * @return array
167 */
168 public function build_setup_payload( $industry = '' ) {
169 return array(
170 'industry' => sanitize_text_field( (string) $industry ),
171 'time' => current_time( 'mysql' ),
172 );
173 }
174
175 /**
176 * Build KUI payload.
177 *
178 * @param int $unique_views Unique active-tour views in 7 days.
179 * @param int $tour_id Last viewed tour ID.
180 * @return array
181 */
182 public function build_kui_payload( $unique_views = 0, $tour_id = 0 ) {
183 return array(
184 'unique_views' => absint( $unique_views ),
185 'tour_id' => absint( $tour_id ),
186 'window_days' => 7,
187 'time' => current_time( 'mysql' ),
188 );
189 }
190
191 /**
192 * Build tour creation payload with feature usage as properties.
193 *
194 * Fires when tour settings are saved. Inspects the stored panodata meta to
195 * determine which core features the user has configured, so all feature
196 * usage is captured in a single event instead of separate ones.
197 *
198 * @param int $tour_id Tour (post) ID.
199 * @return array
200 */
201 public function build_tour_creation_payload( $tour_id = 0 ) {
202 $tour_id = absint( $tour_id );
203 $panodata = get_post_meta( $tour_id, 'panodata', true );
204
205 $hotspot_editor = 'no';
206 $floor_plan = 'no';
207 $tour_builder = 'no';
208
209 if ( is_array( $panodata ) ) {
210 // Floor plan — added to panodata by pro version via filter.
211 if (
212 isset( $panodata['floor_plan_tour_enabler'] ) &&
213 'on' === $panodata['floor_plan_tour_enabler'] &&
214 ! empty( $panodata['floor_plan_attachment_url'] )
215 ) {
216 $floor_plan = 'yes';
217 }
218
219 // Scene list lives one level deeper inside panodata.
220 $scene_list = isset( $panodata['panodata']['scene-list'] ) ? $panodata['panodata']['scene-list'] : array();
221
222 if ( is_array( $scene_list ) ) {
223 // Tour builder = more than one scene configured.
224 if ( count( $scene_list ) > 1 ) {
225 $tour_builder = 'yes';
226 }
227
228 // Hotspot editor = at least one non-empty hotspot in any scene.
229 foreach ( $scene_list as $scene ) {
230 if ( ! empty( $scene['hotspot-list'] ) && is_array( $scene['hotspot-list'] ) ) {
231 $hotspot_editor = 'yes';
232 break;
233 }
234 }
235 }
236 }
237
238 return array(
239 'hotspot_editor' => $hotspot_editor,
240 'floor_plan' => $floor_plan,
241 'tour_builder' => $tour_builder,
242 );
243 }
244
245 /**
246 * Track onboarding completed event (non-PII, bypasses consent gate).
247 *
248 * @param string $industry Industry from setup wizard.
249 * @return void
250 */
251 public function track_onboarding_event( $industry = '' ) {
252 global $wpvr_telemetry;
253 if ( ! is_object( $wpvr_telemetry ) ) {
254 return;
255 }
256 if ( method_exists( $wpvr_telemetry, 'has_sent_event' ) && $wpvr_telemetry->has_sent_event( 'onboarding_completed' ) ) {
257 return;
258 }
259 $wpvr_telemetry->track_immediate( 'activation/onboarding_completed', $this->build_setup_payload( $industry ), true );
260 if ( method_exists( $wpvr_telemetry, 'mark_event_sent' ) ) {
261 $wpvr_telemetry->mark_event_sent( 'onboarding_completed' );
262 }
263 }
264
265 /**
266 * Track activation/aha_reached after consent is granted.
267 *
268 * The wizard completion hook fires BEFORE consent is set (separate AJAX calls),
269 * so aha cannot use define_triggers. Instead, this method fires once when
270 * consent is granted on the Success step, if the wizard was completed.
271 *
272 * @return void
273 */
274 public function track_aha_after_consent() {
275 global $wpvr_telemetry;
276 if ( ! is_object( $wpvr_telemetry ) ) {
277 return;
278 }
279
280 // Only fire if the wizard was actually completed.
281 if ( ! get_option( 'wpvr_wizard_onboarding_done' ) ) {
282 return;
283 }
284
285 // One-shot guard — never send twice.
286 if ( method_exists( $wpvr_telemetry, 'has_sent_event' ) && $wpvr_telemetry->has_sent_event( 'aha_reached_wizard_completed' ) ) {
287 return;
288 }
289
290 $industry = sanitize_text_field( get_option( 'wpvr_industry_name', '' ) );
291
292 $wpvr_telemetry->track_kui(
293 'wizard_completed',
294 array(
295 'industry' => $industry,
296 'time' => current_time( 'mysql' ),
297 )
298 );
299
300 if ( method_exists( $wpvr_telemetry, 'mark_event_sent' ) ) {
301 $wpvr_telemetry->mark_event_sent( 'aha_reached_wizard_completed' );
302 }
303 }
304
305 /**
306 * Update rolling 7-day unique active-tour views and emit KUI trigger event.
307 *
308 * @param int $tour_id Tour ID.
309 * @return void
310 */
311 public function handle_embedded_tour_view( $tour_id ) {
312 $tour_id = absint( $tour_id );
313 if ( $tour_id <= 0 ) {
314 return;
315 }
316
317 $tour = get_post( $tour_id );
318 if ( ! $tour || 'wpvr_item' !== $tour->post_type || 'publish' !== $tour->post_status ) {
319 return;
320 }
321
322 $option_key = 'wpvr_kui_unique_views_7d';
323 $last_kui_sent_key = 'wpvr_kui_unique_views_7d_last_sent_at';
324 $window_seconds = 7 * DAY_IN_SECONDS;
325 $now = time();
326
327 $tracked_views = get_option( $option_key, array() );
328 if ( ! is_array( $tracked_views ) ) {
329 $tracked_views = array();
330 }
331
332 foreach ( $tracked_views as $tracked_tour_id => $timestamp ) {
333 $tracked_tour_id = absint( $tracked_tour_id );
334 $timestamp = absint( $timestamp );
335
336 if (
337 $tracked_tour_id <= 0 ||
338 $timestamp <= 0 ||
339 ( $now - $timestamp ) > $window_seconds ||
340 'publish' !== get_post_status( $tracked_tour_id )
341 ) {
342 unset( $tracked_views[ $tracked_tour_id ] );
343 }
344 }
345
346 $tracked_views[ $tour_id ] = $now;
347 update_option( $option_key, $tracked_views );
348
349 $unique_views = count( $tracked_views );
350 $published_tours = wp_count_posts( 'wpvr_item' );
351 $total_published_tours = 0;
352
353 if ( is_object( $published_tours ) && isset( $published_tours->publish ) ) {
354 $total_published_tours = absint( $published_tours->publish );
355 }
356
357 $active_tour_ratio = 0;
358 if ( $total_published_tours > 0 ) {
359 $active_tour_ratio = (float) ( $unique_views / $total_published_tours );
360 }
361
362 update_option( 'wpvr_last_active_tour_ratio', $active_tour_ratio );
363
364 $last_kui_sent_at = absint( get_option( $last_kui_sent_key, 0 ) );
365 $minimum_active_tour_ratio = 0.6;
366
367 if ( $active_tour_ratio < $minimum_active_tour_ratio ) {
368 return;
369 }
370
371 if ( $last_kui_sent_at > 0 && ( $now - $last_kui_sent_at ) < $window_seconds ) {
372 return;
373 }
374
375 do_action( 'wpvr_kui_unique_views_updated', $unique_views, $tour_id );
376 update_option( $last_kui_sent_key, $now );
377 }
378
379 /**
380 * Replace generic deactivation reasons with VR-specific ones.
381 *
382 * @param array $reasons
383 * @return array
384 */
385 public function set_deactivation_reasons( $reasons ) {
386 return array(
387 array(
388 'id' => 'interface_too_complex',
389 'text' => __( 'Setup / Interface is too complex', 'wpvr' ),
390 'placeholder' => __( 'What was the biggest blocker?', 'wpvr' ),
391 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="23" height="23" viewBox="0 0 23 23"><g fill="none"><g fill="#3B86FF"><path d="M11.5 0C17.9 0 23 5.1 23 11.5 23 17.9 17.9 23 11.5 23 10.6 23 9.6 22.9 8.8 22.7L8.8 22.6C9.3 22.5 9.7 22.3 10 21.9 10.3 21.6 10.4 21.3 10.4 20.9 10.8 21 11.1 21 11.5 21 16.7 21 21 16.7 21 11.5 21 6.3 16.7 2 11.5 2 6.3 2 2 6.3 2 11.5 2 13 2.3 14.3 2.9 15.6 2.7 16 2.4 16.3 2.2 16.8L2.1 17.1 2.1 17.3C2 17.5 2 17.7 2 18 0.7 16.1 0 13.9 0 11.5 0 5.1 5.1 0 11.5 0ZM11.7 11.2C13.1 11.2 14.3 11.7 15.2 12.9 15.3 13 15.4 13.1 15.4 13.2 15.4 13.4 15.3 13.8 15.2 13.8 15 13.9 14.9 13.8 14.8 13.7 14.6 13.5 14.4 13.2 14.1 13.1 13.5 12.6 12.8 12.3 12 12.2 10.7 12.1 9.5 12.3 8.4 12.8 8.3 12.8 8.2 12.8 8.1 12.8 7.9 12.8 7.8 12.4 7.8 12.2 7.7 12.1 7.8 11.9 8 11.8 8.4 11.7 8.8 11.5 9.2 11.4 10 11.2 10.9 11.1 11.7 11.2ZM16.3 5.9C17.3 5.9 18 6.6 18 7.6 18 8.5 17.3 9.3 16.3 9.3 15.4 9.3 14.7 8.5 14.7 7.6 14.7 6.6 15.4 5.9 16.3 5.9ZM8.3 5C9.2 5 9.9 5.8 9.9 6.7 9.9 7.7 9.2 8.4 8.2 8.4 7.3 8.4 6.6 7.7 6.6 6.7 6.6 5.8 7.3 5 8.3 5Z"/></g></g></svg>',
392 ),
393 array(
394 'id' => 'missing_vr_feature',
395 'text' => __( 'Missing specific VR feature', 'wpvr' ),
396 'placeholder' => __( 'Which feature? (Floor Plan, WebVR, etc.)', 'wpvr' ),
397 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="17" viewBox="0 0 24 17"><g fill="none"><g fill="#3B86FF"><path d="M19.4 0C19.7 0.6 19.8 1.3 19.8 2 19.8 3.2 19.4 4.4 18.5 5.3 17.6 6.2 16.5 6.7 15.2 6.7 15.2 6.7 15.2 6.7 15.2 6.7 14 6.7 12.9 6.2 12 5.3 11.2 4.4 10.7 3.3 10.7 2 10.7 1.3 10.8 0.6 11.1 0L7.6 0 7 0 6.5 0 6.5 5.7C6.3 5.6 5.9 5.3 5.6 5.1 5 4.6 4.3 4.3 3.5 4.3 3.5 4.3 3.5 4.3 3.4 4.3 1.6 4.4 0 5.9 0 7.9 0 8.6 0.2 9.2 0.5 9.7 1.1 10.8 2.2 11.5 3.5 11.5 4.3 11.5 5 11.2 5.6 10.8 6 10.5 6.3 10.3 6.5 10.2L6.5 10.2 6.5 17 6.5 17 7 17 7.6 17 22.5 17C23.3 17 24 16.3 24 15.5L24 0 19.4 0Z"/></g></g></svg>',
398 ),
399 array(
400 'id' => 'tour_not_displaying',
401 'text' => __( 'Tour not displaying / Error', 'wpvr' ),
402 'placeholder' => __( 'Any error message or URL?', 'wpvr' ),
403 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="23" height="23" viewBox="0 0 23 23"><g fill="none"><g fill="#3B86FF"><path d="M11.5 0C17.9 0 23 5.1 23 11.5 23 17.9 17.9 23 11.5 23 5.1 23 0 17.9 0 11.5 0 5.1 5.1 0 11.5 0ZM11.8 14.4C11.2 14.4 10.7 14.8 10.7 15.4 10.7 16 11.2 16.4 11.8 16.4 12.4 16.4 12.8 16 12.8 15.4 12.8 14.8 12.4 14.4 11.8 14.4ZM12 7C10.1 7 9.1 8.1 9 9.6L10.5 9.6C10.5 8.8 11.1 8.3 11.9 8.3 12.7 8.3 13.2 8.8 13.2 9.5 13.2 10.1 13 10.4 12.2 10.9 11.3 11.4 10.9 12 11 12.9L11 13.4 12.5 13.4 12.5 13C12.5 12.4 12.7 12.1 13.5 11.6 14.4 11.1 14.9 10.4 14.9 9.4 14.9 8 13.7 7 12 7Z"/></g></g></svg>',
404 ),
405 array(
406 'id' => 'performance_issue',
407 'text' => __( 'Image quality / Performance issues', 'wpvr' ),
408 'placeholder' => __( 'Image size or browser details help.', 'wpvr' ),
409 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="23" height="23" viewBox="0 0 23 23"><g fill="none"><g fill="#3B86FF"><path d="M11.5 0C17.9 0 23 5.1 23 11.5 23 17.9 17.9 23 11.5 23 5.1 23 0 17.9 0 11.5 0 5.1 5.1 0 11.5 0ZM11.5 2C6.3 2 2 6.3 2 11.5 2 16.7 6.3 21 11.5 21 16.7 21 21 16.7 21 11.5 21 6.3 16.7 2 11.5 2ZM12.5 12.9L12.7 5 10.2 5 10.5 12.9 12.5 12.9ZM11.5 17.4C12.4 17.4 13 16.8 13 15.9 13 15 12.4 14.4 11.5 14.4 10.6 14.4 10 15 10 15.9 10 16.8 10.6 17.4 11.5 17.4Z"/></g></g></svg>',
410 ),
411 array(
412 'id' => 'found_better_solution',
413 'text' => __( 'Found a better solution', 'wpvr' ),
414 'placeholder' => __( 'Which tool? What did it do better?', 'wpvr' ),
415 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="23" height="23" viewBox="0 0 23 23"><g fill="none"><g fill="#3B86FF"><path d="M17.1 14L22.4 19.3C23.2 20.2 23.2 21.5 22.4 22.4 21.5 23.2 20.2 23.2 19.3 22.4L19.3 22.4 14 17.1C15.3 16.3 16.3 15.3 17.1 14L17.1 14ZM8.6 0C13.4 0 17.3 3.9 17.3 8.6 17.3 13.4 13.4 17.2 8.6 17.2 3.9 17.2 0 13.4 0 8.6 0 3.9 3.9 0 8.6 0ZM8.6 2.2C5.1 2.2 2.2 5.1 2.2 8.6 2.2 12.2 5.1 15.1 8.6 15.1 12.2 15.1 15.1 12.2 15.1 8.6 15.1 5.1 12.2 2.2 8.6 2.2ZM8.6 3.6L8.6 5C6.6 5 5 6.6 5 8.6L5 8.6 3.6 8.6C3.6 5.9 5.9 3.6 8.6 3.6L8.6 3.6Z"/></g></g></svg>',
416 ),
417 array(
418 'id' => 'just_testing',
419 'text' => __( 'Just testing / One-time project', 'wpvr' ),
420 'placeholder' => __( 'What were you building?', 'wpvr' ),
421 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="23" viewBox="0 0 24 6"><g fill="none"><g fill="#3B86FF"><path d="M3 0C4.7 0 6 1.3 6 3 6 4.7 4.7 6 3 6 1.3 6 0 4.7 0 3 0 1.3 1.3 0 3 0ZM12 0C13.7 0 15 1.3 15 3 15 4.7 13.7 6 12 6 10.3 6 9 4.7 9 3 9 1.3 10.3 0 12 0ZM21 0C22.7 0 24 1.3 24 3 24 4.7 22.7 6 21 6 19.3 6 18 4.7 18 3 18 1.3 19.3 0 21 0Z"/></g></g></svg>',
422 ),
423 );
424 }
425
426 }
427
428 new WPVR_Linno_Telemetry();
429