PluginProbe
ChatHelp – Click to Chat Button, WooCommerce Chat to Order & Floating Chat Form / 3.5.4
ChatHelp – Click to Chat Button, WooCommerce Chat to Order & Floating Chat Form v3.5.4
3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.0 3.5.1 3.4.2 3.4.1 3.4.0 3.3.2 trunk 1.6.1 1.6.2 1.6.3 1.6.4 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 All 76 releases
chat-help / src / Admin / Analytics.php

Analytics.php in ChatHelp – Click to Chat Button, WooCommerce Chat to Order & Floating Chat Form 3.5.4, at src/Admin/Analytics.php

436 lines 16.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace ThemeAtelier\ChatHelp\Admin;
4
5 use WP_REST_Server;
6 use WP_REST_Request;
7 use WP_REST_Response;
8
9 if (! defined('ABSPATH')) {
10 die;
11 }
12
13 class Analytics
14 {
15 private $table;
16 private $leads_table;
17
18 function __construct()
19 {
20 global $wpdb;
21 $this->table = $wpdb->prefix . 'chat_help_analytics';
22 $this->leads_table = $wpdb->prefix . 'chat_help_leads';
23
24 $this->create_table();
25 add_action('rest_api_init', [$this, 'register_rest_routes']);
26 add_action('wp_footer', [$this, 'inject_tracking_script']);
27 }
28
29 public function create_table()
30 {
31 global $wpdb;
32 $charset_collate = $wpdb->get_charset_collate();
33 $table = $this->table;
34
35 $sql = "CREATE TABLE $table (
36 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
37 widget_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
38 event_type VARCHAR(20) NOT NULL,
39 device_type VARCHAR(10) NOT NULL DEFAULT 'desktop',
40 country VARCHAR(3) NOT NULL DEFAULT '',
41 browser VARCHAR(30) NOT NULL DEFAULT '',
42 page_url VARCHAR(500) NOT NULL DEFAULT '',
43 session_id VARCHAR(64) DEFAULT NULL,
44 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
45 PRIMARY KEY (id),
46 KEY idx_widget_event_date (widget_id, event_type, created_at),
47 KEY idx_device (device_type),
48 KEY idx_country (country),
49 KEY idx_browser (browser)
50 ) $charset_collate;";
51
52 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
53 dbDelta($sql);
54 }
55
56 public function register_rest_routes()
57 {
58 register_rest_route('chat-help/v1', '/analytics/track', [
59 'methods' => WP_REST_Server::CREATABLE,
60 'callback' => [$this, 'track_event'],
61 'permission_callback' => '__return_true',
62 ]);
63
64 register_rest_route('chat-help/v1', '/analytics', [
65 'methods' => WP_REST_Server::READABLE,
66 'callback' => [$this, 'get_summary'],
67 'permission_callback' => [$this, 'admin_permission'],
68 ]);
69
70 register_rest_route('chat-help/v1', '/analytics/chart', [
71 'methods' => WP_REST_Server::READABLE,
72 'callback' => [$this, 'get_chart'],
73 'permission_callback' => [$this, 'admin_permission'],
74 ]);
75
76 register_rest_route('chat-help/v1', '/analytics/devices', [
77 'methods' => WP_REST_Server::READABLE,
78 'callback' => [$this, 'get_devices'],
79 'permission_callback' => [$this, 'admin_permission'],
80 ]);
81
82 register_rest_route('chat-help/v1', '/analytics/tables', [
83 'methods' => WP_REST_Server::READABLE,
84 'callback' => [$this, 'get_tables'],
85 'permission_callback' => [$this, 'admin_permission'],
86 ]);
87 }
88
89 public function admin_permission()
90 {
91 return current_user_can('manage_options');
92 }
93
94 public function track_event(WP_REST_Request $request)
95 {
96 global $wpdb;
97
98 $event_type = sanitize_text_field($request->get_param('event_type') ?? '');
99 if (! in_array($event_type, ['visitor', 'view', 'conversion'], true)) {
100 return new WP_REST_Response(['success' => false], 400);
101 }
102
103 $widget_id = absint($request->get_param('widget_id') ?? 0);
104 $device = sanitize_text_field($request->get_param('device_type') ?? 'desktop');
105 $session_id = sanitize_text_field($request->get_param('session_id') ?? '');
106 $page_url = esc_url_raw(substr($request->get_param('page_url') ?? '', 0, 500));
107
108 $country = $this->detect_country();
109 $browser = $this->detect_browser();
110
111 $wpdb->insert(
112 $this->table,
113 [
114 'widget_id' => $widget_id,
115 'event_type' => $event_type,
116 'device_type' => in_array($device, ['desktop', 'mobile', 'other'], true) ? $device : 'desktop',
117 'country' => substr($country, 0, 3),
118 'browser' => substr($browser, 0, 30),
119 'page_url' => $page_url,
120 'session_id' => substr($session_id, 0, 64),
121 ],
122 ['%d', '%s', '%s', '%s', '%s', '%s', '%s']
123 );
124
125 return new WP_REST_Response(['success' => true], 200);
126 }
127
128 public function get_summary(WP_REST_Request $request)
129 {
130 global $wpdb;
131
132 [$from, $to, $widget_where, $date_where] = $this->build_where($request->get_params());
133
134 $table = $this->table;
135 $row = $wpdb->get_row(
136 "SELECT
137 SUM(event_type = 'visitor') AS visitors,
138 SUM(event_type = 'view') AS views,
139 SUM(event_type = 'conversion') AS conversions
140 FROM $table WHERE 1=1 $date_where $widget_where"
141 );
142
143 $visitors = (int) ($row->visitors ?? 0);
144 $views = (int) ($row->views ?? 0);
145 $conversions = (int) ($row->conversions ?? 0);
146 $rate = $visitors > 0 ? round(($conversions / $visitors) * 100, 1) : 0;
147
148 $leads_table = $this->leads_table;
149 $total_leads = (int) $wpdb->get_var(
150 $wpdb->prepare(
151 "SELECT COUNT(*) FROM $leads_table WHERE created_at >= %s AND created_at <= %s",
152 $from . ' 00:00:00',
153 $to . ' 23:59:59'
154 )
155 );
156
157 return new WP_REST_Response([
158 'visitors' => $visitors,
159 'views' => $views,
160 'conversions' => $conversions,
161 'conversion_rate' => $rate,
162 'total_leads' => $total_leads,
163 ]);
164 }
165
166 public function get_chart(WP_REST_Request $request)
167 {
168 global $wpdb;
169
170 $params = $request->get_params();
171 [$from, $to, $widget_where, $date_where] = $this->build_where($params);
172
173 $period = $params['period'] ?? 'this_week';
174 $group_by = in_array($period, ['this_year', 'last_year'], true) ? 'month' : 'day';
175 $table = $this->table;
176 $leads_table = $this->leads_table;
177
178 if ($group_by === 'month') {
179 $select = "DATE_FORMAT(created_at, '%Y-%m') AS period_key";
180 $group = "DATE_FORMAT(created_at, '%Y-%m')";
181 } else {
182 $select = 'DATE(created_at) AS period_key';
183 $group = 'DATE(created_at)';
184 }
185
186 $rows = $wpdb->get_results(
187 "SELECT $select, event_type, COUNT(*) AS cnt
188 FROM $table WHERE 1=1 $date_where $widget_where
189 GROUP BY $group, event_type"
190 );
191
192 $labels = $this->build_label_range($from, $to, $group_by);
193 $datasets = [
194 'visitors' => array_fill(0, count($labels), 0),
195 'views' => array_fill(0, count($labels), 0),
196 'conversions' => array_fill(0, count($labels), 0),
197 'leads' => array_fill(0, count($labels), 0),
198 ];
199
200 $index_map = array_flip($labels);
201 foreach ($rows as $row) {
202 $key = $group_by === 'month'
203 ? substr($row->period_key, 0, 7)
204 : $row->period_key;
205 if (! isset($index_map[$key])) {
206 continue;
207 }
208 $i = $index_map[$key];
209 $type_map = ['visitor' => 'visitors', 'view' => 'views', 'conversion' => 'conversions'];
210 if (isset($type_map[$row->event_type])) {
211 $datasets[$type_map[$row->event_type]][$i] = (int) $row->cnt;
212 }
213 }
214
215 if ($group_by === 'month') {
216 $leads_rows = $wpdb->get_results(
217 $wpdb->prepare(
218 "SELECT DATE_FORMAT(created_at, '%%Y-%%m') AS period_key, COUNT(*) AS cnt
219 FROM $leads_table WHERE created_at >= %s AND created_at <= %s
220 GROUP BY DATE_FORMAT(created_at, '%%Y-%%m')",
221 $from . ' 00:00:00',
222 $to . ' 23:59:59'
223 )
224 );
225 } else {
226 $leads_rows = $wpdb->get_results(
227 $wpdb->prepare(
228 "SELECT DATE(created_at) AS period_key, COUNT(*) AS cnt
229 FROM $leads_table WHERE created_at >= %s AND created_at <= %s
230 GROUP BY DATE(created_at)",
231 $from . ' 00:00:00',
232 $to . ' 23:59:59'
233 )
234 );
235 }
236
237 foreach ($leads_rows as $row) {
238 if (isset($index_map[$row->period_key])) {
239 $datasets['leads'][$index_map[$row->period_key]] = (int) $row->cnt;
240 }
241 }
242
243 return new WP_REST_Response(['labels' => $labels, 'datasets' => $datasets]);
244 }
245
246 public function get_devices(WP_REST_Request $request)
247 {
248 global $wpdb;
249
250 [$from, $to, $widget_where, $date_where] = $this->build_where($request->get_params());
251 $table = $this->table;
252
253 $rows = $wpdb->get_results(
254 "SELECT device_type, event_type, COUNT(*) AS cnt
255 FROM $table WHERE 1=1 $date_where $widget_where
256 GROUP BY device_type, event_type"
257 );
258
259 $result = [
260 'desktop' => ['visitors' => 0, 'views' => 0, 'conversions' => 0],
261 'mobile' => ['visitors' => 0, 'views' => 0, 'conversions' => 0],
262 'other' => ['visitors' => 0, 'views' => 0, 'conversions' => 0],
263 ];
264
265 $type_map = ['visitor' => 'visitors', 'view' => 'views', 'conversion' => 'conversions'];
266 foreach ($rows as $row) {
267 $dev = $row->device_type;
268 if (! isset($result[$dev])) {
269 $dev = 'other';
270 }
271 if (isset($type_map[$row->event_type])) {
272 $result[$dev][$type_map[$row->event_type]] = (int) $row->cnt;
273 }
274 }
275
276 return new WP_REST_Response($result);
277 }
278
279 public function get_tables(WP_REST_Request $request)
280 {
281 // Traffic Breakdown (countries / pages / browsers) is Pro-only. The
282 // free dashboard renders a locked preview and never calls this, but
283 // the data is withheld server-side too (defence-in-depth).
284 return new WP_REST_Response([
285 'pro' => true,
286 'message' => __('Traffic Breakdown is a Pro feature. Upgrade to see countries, pages, and browsers.', 'chat-help'),
287 ], 403);
288 }
289
290 private function build_where(array $params)
291 {
292 global $wpdb;
293
294 $period = $params['period'] ?? 'this_week';
295 $widget_id = $params['widget_id'] ?? 'all';
296 $date_from = $params['date_from'] ?? '';
297 $date_to = $params['date_to'] ?? '';
298
299 [$from, $to] = $this->resolve_period($period, $date_from, $date_to);
300
301 $widget_where = '';
302 if ($widget_id !== 'all' && is_numeric($widget_id)) {
303 $widget_where = $wpdb->prepare(' AND widget_id = %d', (int) $widget_id);
304 }
305
306 $date_where = $wpdb->prepare(
307 ' AND created_at >= %s AND created_at <= %s',
308 $from . ' 00:00:00',
309 $to . ' 23:59:59'
310 );
311
312 return [$from, $to, $widget_where, $date_where];
313 }
314
315 private function resolve_period($period, $date_from = '', $date_to = '')
316 {
317 $now = current_time('timestamp');
318
319 switch ($period) {
320 case 'last_week':
321 $dow = (int) date('N', $now);
322 $this_monday = $now - ($dow - 1) * DAY_IN_SECONDS;
323 $last_monday = $this_monday - 7 * DAY_IN_SECONDS;
324 $from = date('Y-m-d', $last_monday);
325 $to = date('Y-m-d', $last_monday + 6 * DAY_IN_SECONDS);
326 break;
327 case 'this_month':
328 $from = date('Y-m-01', $now);
329 $to = date('Y-m-t', $now);
330 break;
331 case 'last_month':
332 $first_of_this = mktime(0, 0, 0, (int) date('m', $now), 1, (int) date('Y', $now));
333 $last_ts = $first_of_this - DAY_IN_SECONDS;
334 $from = date('Y-m-01', $last_ts);
335 $to = date('Y-m-t', $last_ts);
336 break;
337 case 'this_year':
338 $from = date('Y-01-01', $now);
339 $to = date('Y-12-31', $now);
340 break;
341 case 'last_year':
342 $year = (int) date('Y', $now) - 1;
343 $from = "{$year}-01-01";
344 $to = "{$year}-12-31";
345 break;
346 case 'custom':
347 $from = ! empty($date_from) ? sanitize_text_field($date_from) : date('Y-m-d', $now - 6 * DAY_IN_SECONDS);
348 $to = ! empty($date_to) ? sanitize_text_field($date_to) : date('Y-m-d', $now);
349 break;
350 default: // this_week
351 $dow = (int) date('N', $now);
352 $from = date('Y-m-d', $now - ($dow - 1) * DAY_IN_SECONDS);
353 $to = date('Y-m-d', $now + (7 - $dow) * DAY_IN_SECONDS);
354 }
355
356 return [$from, $to];
357 }
358
359 private function build_label_range($from, $to, $group_by)
360 {
361 $labels = [];
362 $start = strtotime($from);
363 $end = strtotime($to);
364
365 if ($group_by === 'month') {
366 while ($start <= $end) {
367 $labels[] = date('Y-m', $start);
368 $start = mktime(0, 0, 0, (int) date('m', $start) + 1, 1, (int) date('Y', $start));
369 }
370 } else {
371 while ($start <= $end) {
372 $labels[] = date('Y-m-d', $start);
373 $start += DAY_IN_SECONDS;
374 }
375 }
376
377 return $labels;
378 }
379
380 private function detect_country()
381 {
382 $headers = ['CF-IPCountry', 'HTTP_X_COUNTRY_CODE', 'HTTP_GEOIP_COUNTRY_CODE'];
383 foreach ($headers as $h) {
384 $val = $_SERVER[$h] ?? '';
385 if ($val && strlen($val) <= 3) {
386 return strtoupper(sanitize_text_field($val));
387 }
388 }
389 return '';
390 }
391
392 private function detect_browser()
393 {
394 $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
395 if (stripos($ua, 'Edg') !== false) return 'Edge';
396 if (stripos($ua, 'OPR') !== false) return 'Opera';
397 if (stripos($ua, 'YaBrowser') !== false) return 'Yandex';
398 if (stripos($ua, 'SamsungBrowser') !== false) return 'Samsung';
399 if (stripos($ua, 'Chrome') !== false) return 'Chrome';
400 if (stripos($ua, 'Firefox') !== false) return 'Firefox';
401 if (stripos($ua, 'Safari') !== false) return 'Safari';
402 if (stripos($ua, 'MSIE') !== false || stripos($ua, 'Trident') !== false) return 'IE';
403 return 'Other';
404 }
405
406 public function inject_tracking_script()
407 {
408 $track_url = esc_url_raw(rest_url('chat-help/v1/analytics/track'));
409 ?>
410 <script>(function(){
411 var trackUrl=<?php echo wp_json_encode($track_url); ?>;
412 function getDevice(){var ua=navigator.userAgent;if(/tablet|ipad|playbook|silk/i.test(ua))return"other";if(/mobile|android|iphone|ipod|blackberry|iemobile|opera mini/i.test(ua))return"mobile";return"desktop";}
413 var device=getDevice();
414 function track(eventType,widgetId,sessionId){fetch(trackUrl,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({event_type:eventType,widget_id:widgetId||0,device_type:device,session_id:sessionId||"",page_url:window.location.pathname})}).catch(function(){});}
415 function getOrCreateSession(){var key="ch_sid";var sid=sessionStorage.getItem(key);if(!sid){sid=Math.random().toString(36).slice(2)+Date.now().toString(36);sessionStorage.setItem(key,sid);}return sid;}
416 var sid=getOrCreateSession();
417 if(!sessionStorage.getItem("ch_visited")){sessionStorage.setItem("ch_visited","1");track("visitor",0,sid);}
418 function setupTracking(){
419 var bubbles=document.querySelectorAll(".wHelp_bubble");
420 if(!bubbles.length)return;
421 if("IntersectionObserver"in window){var observer=new IntersectionObserver(function(entries){entries.forEach(function(entry){if(entry.isIntersecting){var wid=entry.target.getAttribute("data-widget-id")||0;track("view",wid,sid);observer.unobserve(entry.target);}});},{threshold:0.5});bubbles.forEach(function(el){observer.observe(el);});}
422 document.addEventListener("click",function(e){
423 if(e.target.closest(".layout_button")){track("conversion",0,sid);return;}
424 var analyticsEl=e.target.closest(".wHelp_woo_btn,.chat_help_analytics");if(analyticsEl&&!analyticsEl.hasAttribute("data-tab")){track("conversion",0,sid);return;}
425 if(e.target.closest(".wHelp__send-message.chat_help_link:not(.agent_input)")){track("conversion",0,sid);return;}
426 var agentItem=e.target.closest(".chat_help_multi_user:not(.avatar-inactive)");
427 if(agentItem&&!agentItem.hasAttribute("data-tab")){track("conversion",0,sid);return;}
428 },{passive:true});
429 document.addEventListener("submit",function(e){var id=e.target.id;if(id==="wHelp_form"||id==="agent_input"){track("conversion",0,sid);}});
430 }
431 if(document.readyState==="loading"){document.addEventListener("DOMContentLoaded",setupTracking);}else{setupTracking();}
432 })();</script>
433 <?php
434 }
435 }
436