PluginProbe
SureCookie – GDPR Cookie Consent Banner, Cookie Scanner & Script Blocking / 1.1.0
SureCookie – GDPR Cookie Consent Banner, Cookie Scanner & Script Blocking v1.1.0
1.5.0 1.4.0 1.3.0 1.3.1 trunk 0.0.0-alpha.1 0.0.0-alpha.2 0.0.0-alpha.3 0.0.1-beta.1 0.0.1-beta.2 0.0.1-beta.3 0.0.1-beta.4 1.0.0 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4
surecookie / admin / analytics.php

analytics.php in SureCookie – GDPR Cookie Consent Banner, Cookie Scanner & Script Blocking 1.1.0, at admin/analytics.php

413 lines 13.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SureCookie Analytics — BSF Analytics Integration
4 *
5 * @package SureCookie\Admin
6 * @since 0.0.1-beta.1
7 */
8
9 namespace SureCookie\Admin;
10
11 use SureCookie\Inc\Functions\Helper;
12 use SureCookie\Inc\Functions\Settings;
13 use SureCookie\Inc\Traits\GetInstance;
14
15 defined( 'ABSPATH' ) || exit;
16
17 /**
18 * Analytics class.
19 *
20 * Handles BSF Analytics integration: event tracking, stats payload, and KPI collection.
21 *
22 * @since 0.0.1-beta.1
23 */
24 class Analytics {
25 use GetInstance;
26
27 /**
28 * Events tracker.
29 *
30 * @var \BSF_Analytics_Events|null
31 */
32 private static $events_tracker = null;
33
34 /**
35 * Constructor.
36 *
37 * @since 0.0.1-beta.1
38 */
39 public function __construct() {
40 // Stats payload filter.
41 add_filter( 'bsf_core_stats', [ $this, 'add_analytics_data' ] );
42
43 // Only run analytics in admin context.
44 if ( ! is_admin() ) {
45 return;
46 }
47
48 // Load Astra Notices for opt-in UI.
49 if ( ! class_exists( 'BSF_Admin_Notices' ) ) {
50 require_once SURECOOKIE_DIR . 'inc/lib/astra-notices/class-bsf-admin-notices.php';
51 }
52
53 add_filter(
54 'uds_survey_allowed_screens',
55 static function () {
56 return [ 'plugins' ];
57 }
58 );
59
60 // Load BSF Analytics library.
61 if ( ! class_exists( 'BSF_Analytics_Loader' ) ) {
62 require_once SURECOOKIE_DIR . 'inc/lib/bsf-analytics/class-bsf-analytics-loader.php';
63 }
64
65 if ( ! class_exists( 'BSF_Analytics_Loader' ) ) {
66 return;
67 }
68
69 /** @var \BSF_Analytics_Loader $loader */
70 $loader = \BSF_Analytics_Loader::get_instance();
71 // Upstream docblock types $data as string, but the implementation pushes it onto an array of entity configs — see class-bsf-analytics-loader.php::set_entity().
72 $loader->set_entity(
73 [ // @phpstan-ignore argument.type
74 'surecookie' => [
75 'product_name' => 'SureCookie',
76 'path' => SURECOOKIE_DIR . 'inc/lib/bsf-analytics',
77 'author' => 'Brainstorm Force',
78 'time_to_display' => '+24 hours',
79 // IMPORTANT: Must be array of arrays — library iterates and passes each to show_feedback_form().
80 'deactivation_survey' => [
81 [
82 'id' => 'deactivation-survey-surecookie',
83 'popup_logo' => SURECOOKIE_URL . 'assets/images/surecookie--brand-colored.svg',
84 'plugin_slug' => 'surecookie',
85 'popup_title' => 'Quick Feedback',
86 'support_url' => Helper::get_marketing_link( 'contact/', 'deactivation_survey_support' ),
87 'popup_description' => 'If you have a moment, please share why you are deactivating SureCookie:',
88 'show_on_screens' => [ 'plugins' ],
89 'plugin_version' => SURECOOKIE_VERSION,
90 ],
91 ],
92 ],
93 ]
94 );
95
96 // Detect version change vs `surecookie_saved_version` (owned by Maintenance).
97 // Runs before the daily throttle gate so an update is never missed between checks.
98 // Note: on frontend-first upgrades or BSF_Analytics_Events load failure, the
99 // `plugin_updated` event may be skipped — accepted tradeoff for key consolidation.
100 $saved_version = get_option( 'surecookie_saved_version', '' );
101 if ( ! empty( $saved_version ) && $saved_version !== SURECOOKIE_VERSION ) {
102 delete_transient( 'surecookie_state_events_checked' );
103 }
104
105 // State-based events — throttled to once per day.
106 // IMPORTANT: Transient is set INSIDE detect_state_events() after confirming
107 // BSF_Analytics_Events class is loaded. If class isn't ready, it retries next load.
108 if ( get_transient( 'surecookie_state_events_checked' ) === false ) {
109 $this->detect_state_events();
110 }
111 }
112
113 // ============================================
114 // Event Tracker
115 // ============================================
116
117 /**
118 * Get shared event tracker instance.
119 *
120 * @since 0.0.1-beta.1
121 * @return \BSF_Analytics_Events|null
122 */
123 public static function events() {
124 if ( ! class_exists( 'BSF_Analytics_Events' ) ) {
125 require_once SURECOOKIE_DIR . 'inc/lib/bsf-analytics/class-bsf-analytics-events.php';
126 }
127
128 if ( self::$events_tracker === null ) {
129 self::$events_tracker = new \BSF_Analytics_Events( 'surecookie' );
130 }
131
132 return self::$events_tracker;
133 }
134
135 // ============================================
136 // Stats Payload
137 // ============================================
138
139 /**
140 * Add SureCookie analytics data to the BSF core stats payload.
141 *
142 * @param array<string, mixed> $stats_data Existing stats data.
143 * @return array<string, mixed> Modified stats data.
144 * @since 0.0.1-beta.1
145 */
146 public function add_analytics_data( $stats_data ) {
147 $events = self::events();
148
149 $stats_data['plugin_data']['surecookie'] = [
150 'free_version' => SURECOOKIE_VERSION,
151 'site_language' => get_locale(),
152
153 // One-time events (flushed from pending queue).
154 'events_record' => $events ? $events->flush_pending() : [],
155
156 // Daily KPIs (last 2 days).
157 'kpi_records' => $this->get_kpi_tracking_data(),
158
159 // Get some total important data.
160 'total_scans' => $this->get_total_scans(),
161 'total_logs' => (int) Settings::get( 'total_logs' ),
162 ];
163
164 return $stats_data;
165 }
166
167 // ============================================
168 // State Event Detection
169 // ============================================
170
171 /**
172 * Detect and queue state-based events on admin page load.
173 *
174 * Runs on every admin load but throttled by a daily transient.
175 * BSF_Analytics_Events dedup prevents duplicate tracking.
176 *
177 * @since 0.0.1-beta.1
178 * @return void
179 */
180 private function detect_state_events(): void {
181 $events = self::events();
182
183 if ( $events === null ) {
184 // BSF_Analytics_Events class not loaded — do NOT set transient; retry next load.
185 return;
186 }
187
188 // Class is available — set throttle transient so we don't re-run for 24h.
189 set_transient( 'surecookie_state_events_checked', 1, DAY_IN_SECONDS );
190
191 // ── 1. plugin_activated ──────────────────────────────────────────
192 $install_time = get_option( 'surecookie_usage_installed_time', 0 );
193 if ( ! $install_time ) {
194 update_option( 'surecookie_usage_installed_time', time(), false );
195 }
196
197 $bsf_referrers = get_option( 'bsf_product_referers', [] );
198 $source = ! empty( $bsf_referrers['surecookie'] )
199 ? sanitize_text_field( $bsf_referrers['surecookie'] )
200 : 'self';
201
202 $events->track( 'plugin_activated', SURECOOKIE_VERSION, [ 'source' => $source ] );
203
204 // ── 2. plugin_updated ────────────────────────────────────────────
205 // Read `surecookie_saved_version` (owned by Maintenance::init). On an admin
206 // request, Analytics runs on `plugins_loaded` p10 and Maintenance on
207 // `admin_init`, so the value read here is the pre-upgrade version.
208 $saved_version = get_option( 'surecookie_saved_version', '' );
209 if ( $saved_version !== SURECOOKIE_VERSION && ! empty( $saved_version ) ) {
210 $events->flush_pushed( [ 'plugin_updated' ] );
211 $events->track(
212 'plugin_updated',
213 SURECOOKIE_VERSION,
214 [ 'from_version' => $saved_version ]
215 );
216 }
217
218 // ── 3. onboarding_completed ──────────────────────────────────────
219 if ( get_option( SURECOOKIE_ONBOARDING_COMPLETED_OPTION, false ) ) {
220 $events->track( 'onboarding_completed', SURECOOKIE_VERSION );
221 }
222
223 // ── 4. onboarding_skipped ────────────────────────────────────────
224 // Fires when onboarding has not been completed after 3+ days since install.
225 $days_since_install = $this->get_days_since_install();
226 if ( ! get_option( SURECOOKIE_ONBOARDING_COMPLETED_OPTION, false ) && $days_since_install >= 3 ) {
227 $events->track(
228 'onboarding_skipped',
229 SURECOOKIE_VERSION,
230 [ 'days_since_install' => (string) $days_since_install ]
231 );
232 }
233
234 // ── 5. banner_configured ─────────────────────────────────────────
235 // Fires when admin settings have been explicitly saved at least once.
236 if ( get_option( SURECOOKIE_SETTINGS_OPTION ) !== false ) {
237 $events->track(
238 'banner_configured',
239 SURECOOKIE_VERSION,
240 [ 'days_since_install' => (string) $days_since_install ]
241 );
242 }
243
244 // ── 6. script_blocking_enabled ───────────────────────────────────
245 if ( (bool) Settings::get( 'script_blocking_enabled' ) ) {
246 $events->track( 'script_blocking_enabled', 'enabled' );
247 }
248
249 // ── 7. content_blocking_enabled ──────────────────────────────────
250 if ( (bool) Settings::get( 'content_blocking_enabled' ) ) {
251 $events->track( 'content_blocking_enabled', 'enabled' );
252 }
253
254 // ── 8. geo_targeting_configured ──────────────────────────────────
255 // Fires when at least one geographic compliance law (GDPR/CCPA/LGPD) is configured.
256 $geo_laws = Settings::get( 'geo_laws' );
257 if ( ! empty( $geo_laws ) && is_array( $geo_laws ) ) {
258 $law_ids = implode( ',', array_keys( $geo_laws ) );
259 $events->track(
260 'geo_targeting_configured',
261 $law_ids,
262 [ 'law_count' => (string) count( $geo_laws ) ]
263 );
264 }
265
266 // ── 9. first_scan_started ────────────────────────────────────────
267 if ( get_option( 'surecookie_first_scan_started_flag', false ) ) {
268 $pages_count = (int) get_option( 'surecookie_first_scan_pages_count', 0 );
269 $events->track(
270 'first_scan_started',
271 SURECOOKIE_VERSION,
272 [ 'pages_count' => (string) $pages_count ]
273 );
274 }
275
276 // ── 10. first_scan_completed (ACTIVATION EVENT) ──────────────────
277 if ( get_option( 'surecookie_first_scan_completed_flag', false ) ) {
278 $pages_scanned = (int) get_option( 'surecookie_first_scan_pages_scanned', 0 );
279 $events->track(
280 'first_scan_completed',
281 SURECOOKIE_VERSION,
282 [
283 'days_since_install' => (string) $days_since_install,
284 'pages_scanned' => (string) $pages_scanned,
285 ]
286 );
287 }
288
289 // ── 11. first_consent_recorded ───────────────────────────────────
290 // Fires when the first real visitor consent is stored in the database.
291 $first_consent = Settings::get( 'total_logs' );
292 if ( $first_consent > 0 ) {
293 $events->track(
294 'first_consent_recorded',
295 SURECOOKIE_VERSION,
296 [ 'days_since_install' => (string) $days_since_install ]
297 );
298 }
299
300 // ── 12. consent_logging_enabled ──────────────────────────────────
301 if ( (bool) Settings::get( 'consent_logging_enabled' ) ) {
302 $events->track( 'consent_logging_enabled', 'enabled' );
303 }
304
305 // ── 13. first_custom_cookie_added ────────────────────────────────
306 // Fires when the user has manually added at least one custom cookie.
307 $custom_cookies = Settings::get( 'custom_cookies' );
308 if ( ! empty( $custom_cookies ) && is_array( $custom_cookies ) ) {
309 $events->track(
310 'first_custom_cookie_added',
311 (string) count( $custom_cookies )
312 );
313 }
314
315 // ── 14. upgrade_banner_dismissed ─────────────────────────────────
316 // Fires when the user has dismissed the pro upgrade banner nudge.
317 $nudges = get_option( SURECOOKIE_NUDGES, [] );
318 if ( ! empty( $nudges['upgrade_banner'] ) && empty( $nudges['upgrade_banner']['display'] ) ) {
319 $events->track(
320 'upgrade_banner_dismissed',
321 (string) ( $nudges['upgrade_banner']['count'] ?? 1 )
322 );
323 }
324 }
325
326 // ============================================
327 // KPI Tracking
328 // ============================================
329
330 /**
331 * Get KPI tracking data for the last 2 days (excluding today).
332 *
333 * @since 0.0.1-beta.1
334 * @return array<string, array<string, array<string, int>>> KPI records keyed by date.
335 */
336 private function get_kpi_tracking_data(): array {
337 $kpi_records = [];
338
339 for ( $i = 1; $i <= 2; $i++ ) {
340 $date = wp_date( 'Y-m-d', strtotime( "-{$i} days" ) );
341
342 if ( ! $date ) {
343 continue;
344 }
345
346 $kpi_records[ $date ] = [
347 'numeric_values' => [
348 'consent_logs' => $this->get_daily_consent_log_count( $date ),
349 ],
350 ];
351 }
352
353 return $kpi_records;
354 }
355
356 /**
357 * Get daily consent log entry count for a specific date.
358 *
359 * @param string $date Date in Y-m-d format.
360 * @since 0.0.1-beta.1
361 * @return int Count of consent log entries for that date.
362 */
363 private function get_daily_consent_log_count( string $date ): int {
364 global $wpdb;
365
366 $table = $wpdb->prefix . SURECOOKIE_CONSENT_LOG_DB;
367
368 return (int) $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
369 $wpdb->prepare(
370 "SELECT COUNT(*) FROM {$table} WHERE DATE(timestamp) = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
371 $date
372 )
373 );
374 }
375
376 // ============================================
377 // Helpers
378 // ============================================
379
380 /**
381 * Get number of days since plugin installation.
382 *
383 * @since 0.0.1-beta.1
384 * @return int Days since install (0 if unknown).
385 */
386 private function get_days_since_install(): int {
387 $install_time = (int) get_option( 'surecookie_usage_installed_time', 0 );
388
389 if ( $install_time <= 0 ) {
390 return 0;
391 }
392
393 return (int) floor( ( time() - $install_time ) / DAY_IN_SECONDS );
394 }
395
396 /**
397 * Get total scans from the database.
398 *
399 * @since 0.0.1-beta.1
400 * @return int Total number of scans.
401 */
402 private function get_total_scans(): int {
403 $option = get_option(
404 SURECOOKIE_SCANNED_DETAILS_OPTION,
405 [
406 'total_scans' => 0,
407 ]
408 );
409
410 return isset( $option['total_scans'] ) ? (int) $option['total_scans'] : 0;
411 }
412 }
413