PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 8.5.69
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v8.5.69
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.69, at includes/class-wpvr-linno-telemetry.php

428 lines 18.2 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
4 /**
5 * Class WPVR_Linno_Telemetry
6 *
7 * Telemetry event bridge for the Linno SDK.
8 *
9 * Canonical events tracked:
10 * - activation/plugin_activated — SDK lifecycle, no consent (automatic).
11 * - activation/plugin_deactivated — SDK lifecycle, no consent (automatic).
12 * - activation/onboarding_completed — non-PII, override (track_immediate).
13 * - activation/aha_reached — consent-gated; wizard completion fires on consent grant,
14 * active tour views fire via define_triggers → aha.
15 * - retention/feature_used — consent-gated (define_triggers → feature_used).
16 *
17 * @since 8.5.57
18 */
19
20 class WPVR_Linno_Telemetry {
21
22 /**
23 * Linno API key.
24 *
25 * @var string
26 */
27 private $api_key;
28
29 /**
30 * Linno API secret.
31 *
32 * @var string
33 */
34 private $api_secret;
35
36 /**
37 * Prevent duplicate client initialization.
38 *
39 * @var bool
40 */
41 private $client_initialized = false;
42
43 /**
44 * Constructor.
45 */
46 public function __construct() {
47 $this->api_key = defined( 'WPVR_TELEMETRY_API_KEY' ) ? WPVR_TELEMETRY_API_KEY : '';
48 $this->api_secret = defined( 'WPVR_TELEMETRY_API_SECRET' ) ? WPVR_TELEMETRY_API_SECRET : '';
49
50 $this->init_client();
51 add_filter( 'wpvr_telemetry_report_interval', array( $this, 'set_daily_telemetry_report_interval' ) );
52 add_action( 'rex_wpvr_embadded_tour', array( $this, 'handle_embedded_tour_view' ), 10, 1 );
53 add_action( 'wpvr_telemetry_consent_granted', array( $this, 'track_aha_after_consent' ) );
54 add_filter( 'wpvr_telemetry_deactivation_reasons', array( $this, 'set_deactivation_reasons' ) );
55 }
56
57 /**
58 * Force Linno telemetry queue interval to daily.
59 *
60 * @return string
61 */
62 public function set_daily_telemetry_report_interval() {
63 return 'daily';
64 }
65
66 /**
67 * Initialize Linno telemetry client and trigger mappings.
68 *
69 * @return void
70 */
71 public function init_client() {
72 global $wpvr_telemetry;
73
74 if ( $this->client_initialized ) {
75 return;
76 }
77
78 if ( ! class_exists( 'LinnoSDK\\Telemetry\\Client' ) || ! defined( 'WPVR_FILE' ) ) {
79 error_log( 'WPVR Telemetry: Linno Client class unavailable or WPVR_FILE not defined.' );
80 return;
81 }
82
83 if ( empty( $this->api_key ) || empty( $this->api_secret ) ) {
84 error_log( 'WPVR Telemetry: API key or API secret is empty. Telemetry client not initialized.' );
85 return;
86 }
87
88 Client::set_text_domain( 'wpvr' );
89
90 $wpvr_telemetry = new Client(
91 array(
92 'pluginFile' => WPVR_FILE,
93 'slug' => 'wpvr',
94 'pluginName' => 'WP VR',
95 'version' => defined( 'WPVR_VERSION' ) ? WPVR_VERSION : '',
96 'apiKey' => $this->api_key,
97 'apiSecret' => $this->api_secret,
98 'driver' => 'posthog',
99 'driver_config' => array(
100 'host' => defined( 'WPVR_TELEMETRY_HOST' ) ? WPVR_TELEMETRY_HOST : 'https://eu.i.posthog.com',
101 'api_key' => $this->api_key,
102 ),
103 )
104 );
105
106 // Non-PII onboarding event — bypasses consent via override=true on track_immediate().
107 add_action( 'wpvr_setup_wizard_completed_event', array( $this, 'track_onboarding_event' ), 10, 1 );
108
109 // Consent-gated events via SDK trigger system.
110 // activation/aha_reached — queued when ≥60% of tours are actively viewed (recurring).
111 // retention/feature_used — queued on every tour save.
112 // Both require opt-in consent.
113 $self = $this;
114 $wpvr_telemetry->define_triggers(
115 array(
116 'aha' => array(
117 'tours_actively_viewed' => array(
118 'hook' => 'wpvr_kui_unique_views_updated',
119 'callback' => function ( $unique_views, $tour_id ) use ( $self ) {
120 return $self->build_kui_payload( $unique_views, $tour_id );
121 },
122 ),
123 ),
124 'feature_used' => array(
125 'tour_creation' => array(
126 'hook' => 'wpvr_tour_settings_saved',
127 'callback' => function ( $tour_id ) use ( $self ) {
128 return $self->build_tour_creation_payload( $tour_id );
129 },
130 ),
131 ),
132 )
133 );
134
135 $this->ensure_daily_telemetry_queue_schedule();
136 $this->client_initialized = true;
137 }
138
139 /**
140 * Ensure telemetry queue cron hook runs daily.
141 *
142 * @return void
143 */
144 private function ensure_daily_telemetry_queue_schedule() {
145 if ( ! defined( 'WPVR_FILE' ) ) {
146 return;
147 }
148
149 $slug = dirname( plugin_basename( WPVR_FILE ) );
150 $hook = $slug . '_telemetry_queue_process';
151 $scheduled_event = wp_get_scheduled_event( $hook );
152
153 if ( $scheduled_event && 'daily' === $scheduled_event->schedule ) {
154 return;
155 }
156
157 wp_clear_scheduled_hook( $hook );
158 wp_schedule_event( time(), 'daily', $hook );
159 }
160
161 /**
162 * Build setup event payload.
163 *
164 * @param string $industry Industry from setup wizard.
165 * @return array
166 */
167 public function build_setup_payload( $industry = '' ) {
168 return array(
169 'industry' => sanitize_text_field( (string) $industry ),
170 'time' => current_time( 'mysql' ),
171 );
172 }
173
174 /**
175 * Build KUI payload.
176 *
177 * @param int $unique_views Unique active-tour views in 7 days.
178 * @param int $tour_id Last viewed tour ID.
179 * @return array
180 */
181 public function build_kui_payload( $unique_views = 0, $tour_id = 0 ) {
182 return array(
183 'unique_views' => absint( $unique_views ),
184 'tour_id' => absint( $tour_id ),
185 'window_days' => 7,
186 'time' => current_time( 'mysql' ),
187 );
188 }
189
190 /**
191 * Build tour creation payload with feature usage as properties.
192 *
193 * Fires when tour settings are saved. Inspects the stored panodata meta to
194 * determine which core features the user has configured, so all feature
195 * usage is captured in a single event instead of separate ones.
196 *
197 * @param int $tour_id Tour (post) ID.
198 * @return array
199 */
200 public function build_tour_creation_payload( $tour_id = 0 ) {
201 $tour_id = absint( $tour_id );
202 $panodata = get_post_meta( $tour_id, 'panodata', true );
203
204 $hotspot_editor = 'no';
205 $floor_plan = 'no';
206 $tour_builder = 'no';
207
208 if ( is_array( $panodata ) ) {
209 // Floor plan — added to panodata by pro version via filter.
210 if (
211 isset( $panodata['floor_plan_tour_enabler'] ) &&
212 'on' === $panodata['floor_plan_tour_enabler'] &&
213 ! empty( $panodata['floor_plan_attachment_url'] )
214 ) {
215 $floor_plan = 'yes';
216 }
217
218 // Scene list lives one level deeper inside panodata.
219 $scene_list = isset( $panodata['panodata']['scene-list'] ) ? $panodata['panodata']['scene-list'] : array();
220
221 if ( is_array( $scene_list ) ) {
222 // Tour builder = more than one scene configured.
223 if ( count( $scene_list ) > 1 ) {
224 $tour_builder = 'yes';
225 }
226
227 // Hotspot editor = at least one non-empty hotspot in any scene.
228 foreach ( $scene_list as $scene ) {
229 if ( ! empty( $scene['hotspot-list'] ) && is_array( $scene['hotspot-list'] ) ) {
230 $hotspot_editor = 'yes';
231 break;
232 }
233 }
234 }
235 }
236
237 return array(
238 'hotspot_editor' => $hotspot_editor,
239 'floor_plan' => $floor_plan,
240 'tour_builder' => $tour_builder,
241 );
242 }
243
244 /**
245 * Track onboarding completed event (non-PII, bypasses consent gate).
246 *
247 * @param string $industry Industry from setup wizard.
248 * @return void
249 */
250 public function track_onboarding_event( $industry = '' ) {
251 global $wpvr_telemetry;
252 if ( ! is_object( $wpvr_telemetry ) ) {
253 return;
254 }
255 if ( method_exists( $wpvr_telemetry, 'has_sent_event' ) && $wpvr_telemetry->has_sent_event( 'onboarding_completed' ) ) {
256 return;
257 }
258 $wpvr_telemetry->track_immediate( 'activation/onboarding_completed', $this->build_setup_payload( $industry ), true );
259 if ( method_exists( $wpvr_telemetry, 'mark_event_sent' ) ) {
260 $wpvr_telemetry->mark_event_sent( 'onboarding_completed' );
261 }
262 }
263
264 /**
265 * Track activation/aha_reached after consent is granted.
266 *
267 * The wizard completion hook fires BEFORE consent is set (separate AJAX calls),
268 * so aha cannot use define_triggers. Instead, this method fires once when
269 * consent is granted on the Success step, if the wizard was completed.
270 *
271 * @return void
272 */
273 public function track_aha_after_consent() {
274 global $wpvr_telemetry;
275 if ( ! is_object( $wpvr_telemetry ) ) {
276 return;
277 }
278
279 // Only fire if the wizard was actually completed.
280 if ( ! get_option( 'wpvr_wizard_onboarding_done' ) ) {
281 return;
282 }
283
284 // One-shot guard — never send twice.
285 if ( method_exists( $wpvr_telemetry, 'has_sent_event' ) && $wpvr_telemetry->has_sent_event( 'aha_reached_wizard_completed' ) ) {
286 return;
287 }
288
289 $industry = sanitize_text_field( get_option( 'wpvr_industry_name', '' ) );
290
291 $wpvr_telemetry->track_kui(
292 'wizard_completed',
293 array(
294 'industry' => $industry,
295 'time' => current_time( 'mysql' ),
296 )
297 );
298
299 if ( method_exists( $wpvr_telemetry, 'mark_event_sent' ) ) {
300 $wpvr_telemetry->mark_event_sent( 'aha_reached_wizard_completed' );
301 }
302 }
303
304 /**
305 * Update rolling 7-day unique active-tour views and emit KUI trigger event.
306 *
307 * @param int $tour_id Tour ID.
308 * @return void
309 */
310 public function handle_embedded_tour_view( $tour_id ) {
311 $tour_id = absint( $tour_id );
312 if ( $tour_id <= 0 ) {
313 return;
314 }
315
316 $tour = get_post( $tour_id );
317 if ( ! $tour || 'wpvr_item' !== $tour->post_type || 'publish' !== $tour->post_status ) {
318 return;
319 }
320
321 $option_key = 'wpvr_kui_unique_views_7d';
322 $last_kui_sent_key = 'wpvr_kui_unique_views_7d_last_sent_at';
323 $window_seconds = 7 * DAY_IN_SECONDS;
324 $now = time();
325
326 $tracked_views = get_option( $option_key, array() );
327 if ( ! is_array( $tracked_views ) ) {
328 $tracked_views = array();
329 }
330
331 foreach ( $tracked_views as $tracked_tour_id => $timestamp ) {
332 $tracked_tour_id = absint( $tracked_tour_id );
333 $timestamp = absint( $timestamp );
334
335 if (
336 $tracked_tour_id <= 0 ||
337 $timestamp <= 0 ||
338 ( $now - $timestamp ) > $window_seconds ||
339 'publish' !== get_post_status( $tracked_tour_id )
340 ) {
341 unset( $tracked_views[ $tracked_tour_id ] );
342 }
343 }
344
345 $tracked_views[ $tour_id ] = $now;
346 update_option( $option_key, $tracked_views );
347
348 $unique_views = count( $tracked_views );
349 $published_tours = wp_count_posts( 'wpvr_item' );
350 $total_published_tours = 0;
351
352 if ( is_object( $published_tours ) && isset( $published_tours->publish ) ) {
353 $total_published_tours = absint( $published_tours->publish );
354 }
355
356 $active_tour_ratio = 0;
357 if ( $total_published_tours > 0 ) {
358 $active_tour_ratio = (float) ( $unique_views / $total_published_tours );
359 }
360
361 update_option( 'wpvr_last_active_tour_ratio', $active_tour_ratio );
362
363 $last_kui_sent_at = absint( get_option( $last_kui_sent_key, 0 ) );
364 $minimum_active_tour_ratio = 0.6;
365
366 if ( $active_tour_ratio < $minimum_active_tour_ratio ) {
367 return;
368 }
369
370 if ( $last_kui_sent_at > 0 && ( $now - $last_kui_sent_at ) < $window_seconds ) {
371 return;
372 }
373
374 do_action( 'wpvr_kui_unique_views_updated', $unique_views, $tour_id );
375 update_option( $last_kui_sent_key, $now );
376 }
377
378 /**
379 * Replace generic deactivation reasons with VR-specific ones.
380 *
381 * @param array $reasons
382 * @return array
383 */
384 public function set_deactivation_reasons( $reasons ) {
385 return array(
386 array(
387 'id' => 'interface_too_complex',
388 'text' => __( 'Setup / Interface is too complex', 'wpvr' ),
389 'placeholder' => __( 'What was the biggest blocker?', 'wpvr' ),
390 '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>',
391 ),
392 array(
393 'id' => 'missing_vr_feature',
394 'text' => __( 'Missing specific VR feature', 'wpvr' ),
395 'placeholder' => __( 'Which feature? (Floor Plan, WebVR, etc.)', 'wpvr' ),
396 '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>',
397 ),
398 array(
399 'id' => 'tour_not_displaying',
400 'text' => __( 'Tour not displaying / Error', 'wpvr' ),
401 'placeholder' => __( 'Any error message or URL?', 'wpvr' ),
402 '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>',
403 ),
404 array(
405 'id' => 'performance_issue',
406 'text' => __( 'Image quality / Performance issues', 'wpvr' ),
407 'placeholder' => __( 'Image size or browser details help.', 'wpvr' ),
408 '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>',
409 ),
410 array(
411 'id' => 'found_better_solution',
412 'text' => __( 'Found a better solution', 'wpvr' ),
413 'placeholder' => __( 'Which tool? What did it do better?', 'wpvr' ),
414 '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>',
415 ),
416 array(
417 'id' => 'just_testing',
418 'text' => __( 'Just testing / One-time project', 'wpvr' ),
419 'placeholder' => __( 'What were you building?', 'wpvr' ),
420 '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>',
421 ),
422 );
423 }
424
425 }
426
427 new WPVR_Linno_Telemetry();
428