PluginProbe
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management / 1.1.0
SureDonation – Donation Forms, Fundraising Campaigns & Donor Management v1.1.0
1.6.0 1.5.1 1.5.0 1.4.0 1.3.0 trunk 0.0.1 1.0.0 1.1.0 1.1.1 1.1.2 1.2.0
← All changes | inc/admin/analytics.php +13 -263 1.2.01.1.0 View file →
@@ -11,9 +11,8 @@
11 11 use SureDonation\Inc\Payments\Offline\Offline_Helper;
12 12 use SureDonation\Inc\Payments\Payment_Helper;
13 13 use SureDonation\Inc\Payments\PayPal\PayPal_Helper;
14 14 use SureDonation\Inc\Payments\Stripe\Stripe_Helper;
15 -use SureDonation\Inc\Privacy\Privacy_Settings;
16 15 use SureDonation\Inc\Traits\Get_Instance;
17 16
18 17 // Exit if accessed directly.
19 18 if ( ! defined( 'ABSPATH' ) ) {
@@ -70,10 +69,8 @@
70 69 // onboarding completion and campaign publishes fire during REST requests.
71 70 add_action( 'suredonation_onboarding_user_details_saved', [ $this, 'track_onboarding_completed' ] );
72 71 add_action( 'transition_post_status', [ $this, 'track_first_campaign_published' ], 10, 3 );
73 72 add_action( 'current_screen', [ $this, 'track_first_campaign_editor_opened' ] );
74 - add_action( 'suredonation_privacy_data_exported', [ $this, 'track_privacy_data_exported' ] );
75 - add_action( 'suredonation_privacy_data_erased', [ $this, 'track_privacy_data_erased' ] );
76 73
77 74 // Detect state-based events (daily throttle; dedup prevents repeat
78 75 // tracking). Admin-only so the detection never runs on the frontend.
79 76 if ( is_admin() ) {
@@ -185,37 +182,24 @@
185 182 $internal_referer = is_array( $bsf_internal_referrer ) && ! empty( $bsf_internal_referrer['suredonation'] )
186 183 ? sanitize_text_field( (string) $bsf_internal_referrer['suredonation'] )
187 184 : 'self';
188 185
189 - $privacy_settings = Privacy_Settings::get_settings();
190 -
191 186 $plugin_data = [
192 - 'free_version' => SUREDONATION_VER,
193 - 'numeric_values' => [
194 - 'total_campaigns' => absint( $campaign_counts->publish ?? 0 ),
195 - 'total_donation_forms' => absint( $form_counts->publish ?? 0 ),
196 - 'total_donations' => $aggregates['total'],
197 - 'completed_donations' => $aggregates['completed'],
198 - 'recurring_donations' => $aggregates['recurring'],
199 - 'total_donors' => $this->get_total_donors(),
200 - 'posts_with_social_sharing_block' => $this->get_social_sharing_block_count(),
187 + 'free_version' => SUREDONATION_VER,
188 + 'numeric_values' => [
189 + 'total_campaigns' => absint( $campaign_counts->publish ?? 0 ),
190 + 'total_donation_forms' => absint( $form_counts->publish ?? 0 ),
191 + 'total_donations' => $aggregates['total'],
192 + 'completed_donations' => $aggregates['completed'],
193 + 'recurring_donations' => $aggregates['recurring'],
194 + 'total_donors' => $this->get_total_donors(),
201 195 ],
202 - 'boolean_values' => [
203 - 'stripe_enabled' => Stripe_Helper::is_stripe_connected(),
204 - 'paypal_enabled' => PayPal_Helper::is_paypal_connected(),
205 - 'offline_enabled' => Offline_Helper::is_offline_enabled(),
206 - // True only when the OttoKit plugin is active AND authenticated,
207 - // so this implies the plugin is active.
208 - 'ottokit_connected' => Helper::is_suretriggers_ready(),
209 - 'contact_consent_enabled' => ! empty( $privacy_settings['contact_consent_field'] ),
210 - 'privacy_policy_field_enabled' => ! empty( $privacy_settings['privacy_policy_field'] ),
211 - 'terms_field_enabled' => ! empty( $privacy_settings['terms_conditions_field'] ),
196 + 'boolean_values' => [
197 + 'stripe_enabled' => Stripe_Helper::is_stripe_connected(),
198 + 'paypal_enabled' => PayPal_Helper::is_paypal_connected(),
199 + 'offline_enabled' => Offline_Helper::is_offline_enabled(),
212 200 ],
213 - 'data_retention_period' => isset( $privacy_settings['minimum_data_retention_period'] ) ? Helper::get_string_value( $privacy_settings['minimum_data_retention_period'] ) : 'none',
214 - 'block_usage' => $this->get_block_usage(),
215 - 'elementor_widget_usage' => $this->get_elementor_widget_usage(),
216 - 'bricks_element_usage' => $this->get_bricks_element_usage(),
217 - 'internal_referer' => $internal_referer,
201 + 'internal_referer' => $internal_referer,
218 202 ];
219 203
220 204 // Add KPI tracking data.
221 205 $kpi_data = $this->get_kpi_tracking_data();
@@ -287,51 +271,8 @@
287 271 );
288 272 }
289 273
290 274 /**
291 - * Track first personal-data export that included SureDonation data
292 - * (adoption event — deduped, sent once).
293 - *
294 - * @since 1.2.0
295 - * @return void
296 - */
297 - public function track_privacy_data_exported() {
298 - $events = self::events();
299 - if ( null === $events ) {
300 - return;
301 - }
302 -
303 - $events->track( 'privacy_data_export_used' );
304 - }
305 -
306 - /**
307 - * Track first personal-data erasure processed for SureDonation data
308 - * (adoption event — deduped, sent once).
309 - *
310 - * @since 1.2.0
311 - * @param array<string, mixed> $outcome Erasure outcome flags.
312 - * @return void
313 - */
314 - public function track_privacy_data_erased( $outcome ) {
315 - $events = self::events();
316 - if ( null === $events ) {
317 - return;
318 - }
319 -
320 - $outcome = is_array( $outcome ) ? $outcome : [];
321 -
322 - $events->track(
323 - 'privacy_data_erasure_used',
324 - '',
325 - [
326 - 'items_removed' => ! empty( $outcome['items_removed'] ) ? 'yes' : 'no',
327 - 'items_retained' => ! empty( $outcome['items_retained'] ) ? 'yes' : 'no',
328 - 'erase_failed' => ! empty( $outcome['erase_failed'] ) ? 'yes' : 'no',
329 - ]
330 - );
331 - }
332 -
333 - /**
334 275 * Track first time a campaign is published (activation event).
335 276 *
336 277 * @param string $new_status New post status.
337 278 * @param string $old_status Old post status.
@@ -443,199 +384,8 @@
443 384 $count = $wpdb->get_var(
444 385 $wpdb->prepare(
445 386 'SELECT COUNT(*) FROM %i',
446 387 $wpdb->prefix . 'suredonation_donors'
447 - )
448 - );
449 -
450 - return absint( $count );
451 - }
452 -
453 - /**
454 - * How many published posts use each SureDonation campaign/donation block.
455 - *
456 - * A privacy-preserving usage count (no content leaves the site) so we can see
457 - * which blocks are actually adopted. One conditional-SUM query (a single table
458 - * scan), run only at analytics send time. Elementor/Bricks placements are not
459 - * counted here (they store their config outside the block grammar) — see
460 - * get_elementor_widget_usage().
461 - *
462 - * @return array<string, int> Block key => number of published posts using it.
463 - * @since 1.2.0
464 - */
465 - private function get_block_usage() {
466 - global $wpdb;
467 -
468 - $blocks = [
469 - 'campaign_goal' => 'suredonation/campaign-goal',
470 - 'campaign_stats' => 'suredonation/campaign-stats',
471 - 'campaign_donations' => 'suredonation/campaign-donations',
472 - 'campaign_donors' => 'suredonation/campaign-donors',
473 - 'campaign_donate_button' => 'suredonation/campaign-donate-button',
474 - 'campaign_social_sharing' => 'suredonation/campaign-social-sharing',
475 - 'donation_form' => 'suredonation/donation-form',
476 - ];
477 -
478 - // prepare() fills placeholders in SQL order: the SELECT-list %s LIKEs
479 - // first, then the FROM %i, then the status %s.
480 - $selects = [];
481 - $values = [];
482 - foreach ( array_keys( $blocks ) as $key ) {
483 - $selects[] = "SUM(post_content LIKE %s) AS {$key}";
484 - // The space after the block name is the delimiter the serializer always
485 - // emits (before attrs JSON, "-->" or "/-->"), so a future
486 - // "campaign-goal-x" block can't prefix-match campaign-goal.
487 - $values[] = '%' . $wpdb->esc_like( '<!-- wp:' . $blocks[ $key ] . ' ' ) . '%';
488 - }
489 - $values[] = $wpdb->posts;
490 - $values[] = 'publish';
491 -
492 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Single aggregate scan at analytics send time; SELECT list is built from hardcoded keys and %s placeholders only.
493 - $row = $wpdb->get_row(
494 - $wpdb->prepare( 'SELECT ' . implode( ', ', $selects ) . ' FROM %i WHERE post_status = %s', $values ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- placeholders are built alongside $values; prepare() accepts the array form.
495 - ARRAY_A
496 - );
497 -
498 - $usage = [];
499 - foreach ( array_keys( $blocks ) as $key ) {
500 - $usage[ $key ] = absint( is_array( $row ) ? ( $row[ $key ] ?? 0 ) : 0 );
501 - }
502 -
503 - return $usage;
504 - }
505 -
506 - /**
507 - * How many published posts use each SureDonation Elementor widget.
508 - *
509 - * The Elementor counterpart of get_block_usage(): widgets live in the
510 - * _elementor_data postmeta (JSON with a quoted "widgetType"), not in the
511 - * block grammar. Same privacy-preserving single-scan shape, run only at
512 - * analytics send time.
513 - *
514 - * @return array<string, int> Widget key => number of published posts using it.
515 - * @since 1.2.0
516 - */
517 - private function get_elementor_widget_usage() {
518 - global $wpdb;
519 -
520 - $widgets = [
521 - 'campaign_goal' => 'suredonation-campaign-goal',
522 - 'campaign_stats' => 'suredonation-campaign-stats',
523 - 'campaign_donations' => 'suredonation-campaign-donations',
524 - 'campaign_donors' => 'suredonation-campaign-donors',
525 - 'campaign_donate_button' => 'suredonation-campaign-donate-button',
526 - 'campaign_social_sharing' => 'suredonation-campaign-social-sharing',
527 - 'donation_form' => 'suredonation-donation-form',
528 - ];
529 -
530 - // prepare() fills placeholders in SQL order: the SELECT-list %s LIKEs
531 - // first, then the two FROM/JOIN %i tables, then meta_key and status.
532 - $selects = [];
533 - $values = [];
534 - foreach ( array_keys( $widgets ) as $key ) {
535 - $selects[] = "SUM(pm.meta_value LIKE %s) AS {$key}";
536 - // Quoted as stored in the _elementor_data JSON ("widgetType":"…"),
537 - // which bounds the match on both sides.
538 - $values[] = '%' . $wpdb->esc_like( '"' . $widgets[ $key ] . '"' ) . '%';
539 - }
540 - $values[] = $wpdb->postmeta;
541 - $values[] = $wpdb->posts;
542 - $values[] = '_elementor_data';
543 - $values[] = 'publish';
544 -
545 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Single aggregate scan at analytics send time; SELECT list is built from hardcoded keys and %s placeholders only.
546 - $row = $wpdb->get_row(
547 - $wpdb->prepare( 'SELECT ' . implode( ', ', $selects ) . ' FROM %i AS pm INNER JOIN %i AS p ON p.ID = pm.post_id WHERE pm.meta_key = %s AND p.post_status = %s', $values ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- placeholders are built alongside $values; prepare() accepts the array form.
548 - ARRAY_A
549 - );
550 -
551 - $usage = [];
552 - foreach ( array_keys( $widgets ) as $key ) {
553 - $usage[ $key ] = absint( is_array( $row ) ? ( $row[ $key ] ?? 0 ) : 0 );
554 - }
555 -
556 - return $usage;
557 - }
558 -
559 - /**
560 - * How many published posts use each SureDonation Bricks element.
561 - *
562 - * A privacy-preserving usage count (no content leaves the site) so we can see
563 - * which Bricks elements are actually adopted — the Bricks counterpart of the
564 - * Gutenberg block_usage stat. Bricks stores builder data as serialized element
565 - * arrays in postmeta, so each element name is matched inside its quotes.
566 - * One conditional-SUM query (a single scan), run only at analytics send time.
567 - *
568 - * @return array<string, int> Element key => number of published posts using it.
569 - * @since 1.2.0
570 - */
571 - private function get_bricks_element_usage() {
572 - global $wpdb;
573 -
574 - $elements = [
575 - 'campaign_goal' => 'suredonation-campaign-goal',
576 - 'campaign_stats' => 'suredonation-campaign-stats',
577 - 'campaign_donations' => 'suredonation-campaign-donations',
578 - 'campaign_donors' => 'suredonation-campaign-donors',
579 - 'campaign_donate_button' => 'suredonation-campaign-donate-button',
580 - 'campaign_social_sharing' => 'suredonation-campaign-social-sharing',
581 - 'donation_form' => 'suredonation-donation-form',
582 - ];
583 -
584 - // prepare() fills placeholders in SQL order: the SELECT-list %s LIKEs
585 - // first, then the two FROM/JOIN %i tables, then meta keys and status.
586 - $selects = [];
587 - $values = [];
588 - foreach ( array_keys( $elements ) as $key ) {
589 - $selects[] = "SUM(pm.meta_value LIKE %s) AS {$key}";
590 - // Quoted as stored in the serialized Bricks element data, which
591 - // bounds the match on both sides.
592 - $values[] = '%' . $wpdb->esc_like( '"' . $elements[ $key ] . '"' ) . '%';
593 - }
594 - $values[] = $wpdb->postmeta;
595 - $values[] = $wpdb->posts;
596 - $values[] = '_bricks_page_content_2';
597 - $values[] = '_bricks_page_header_2';
598 - $values[] = '_bricks_page_footer_2';
599 - $values[] = 'publish';
600 -
601 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- Single aggregate scan at analytics send time; SELECT list is built from hardcoded keys and %s placeholders only.
602 - $row = $wpdb->get_row(
603 - $wpdb->prepare( 'SELECT ' . implode( ', ', $selects ) . ' FROM %i AS pm INNER JOIN %i AS p ON p.ID = pm.post_id WHERE pm.meta_key IN ( %s, %s, %s ) AND p.post_status = %s', $values ), // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- placeholders are built alongside $values; prepare() accepts the array form.
604 - ARRAY_A
605 - );
606 -
607 - $usage = [];
608 - foreach ( array_keys( $elements ) as $key ) {
609 - $usage[ $key ] = absint( is_array( $row ) ? ( $row[ $key ] ?? 0 ) : 0 );
610 - }
611 -
612 - return $usage;
613 - }
614 -
615 - /**
616 - * How many published posts use the Campaign Social Sharing block.
617 - *
618 - * A privacy-preserving adoption count (no content leaves the site), run only
619 - * at analytics send time. The trailing space is the delimiter the block
620 - * serializer always emits after the block name, so a future
621 - * "campaign-social-sharing-x" block can't prefix-match.
622 - *
623 - * @return int Number of published posts containing the block.
624 - * @since 1.2.0
625 - */
626 - private function get_social_sharing_block_count() {
627 - global $wpdb;
628 -
629 - $like = '%' . $wpdb->esc_like( '<!-- wp:suredonation/campaign-social-sharing ' ) . '%';
630 -
631 - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Single aggregate COUNT run only at analytics send time.
632 - $count = $wpdb->get_var(
633 - $wpdb->prepare(
634 - 'SELECT COUNT(ID) FROM %i WHERE post_status = %s AND post_content LIKE %s',
635 - $wpdb->posts,
636 - 'publish',
637 - $like
638 388 )
639 389 );
640 390
641 391 return absint( $count );