PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 8.5.76
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v8.5.76
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 8.5.4 All 221 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.76, at includes/class-wpvr-linno-telemetry.php

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