PluginProbe
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress / 9.0.0
WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress v9.0.0
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 / src / Tracker / WPVRLinnoTelemetry.php

WPVRLinnoTelemetry.php in WPVR – 360 Panorama viewer and Virtual Tour Builder for WordPress 9.0.0, at src/Tracker/WPVRLinnoTelemetry.php

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