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
suredonation / inc / lib / bsf-analytics / class-bsf-analytics-events.php

class-bsf-analytics-events.php in SureDonation – Donation Forms, Fundraising Campaigns & Donor Management 1.1.0, at inc/lib/bsf-analytics/class-bsf-analytics-events.php

239 lines 7.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * BSF Analytics Events — reusable one-time milestone tracking.
4 *
5 * Tracks events temporarily, sends them once via BSF Analytics,
6 * then cleans up. Only a minimal dedup flag remains.
7 *
8 * @package bsf-analytics
9 * @since 1.1.21
10 */
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly.
14 }
15
16 if ( ! class_exists( 'BSF_Analytics_Events' ) ) {
17
18 /**
19 * BSF Analytics Events Class.
20 *
21 * @since 1.1.21
22 */
23 class BSF_Analytics_Events {
24
25 /**
26 * Plugin slug used as option key prefix in default storage.
27 *
28 * @var string
29 */
30 private $slug;
31
32 /**
33 * Option resolver callbacks.
34 *
35 * @var array{get: callable|null, update: callable|null}
36 */
37 private $option_resolver;
38
39 /**
40 * Constructor.
41 *
42 * @param string $slug Plugin slug (e.g. 'sureforms', 'astra').
43 * @param array $option_resolver Optional. Custom callbacks for option storage.
44 * 'get' => callable( $key, $default ) — retrieve an option.
45 * 'update' => callable( $key, $value ) — persist an option.
46 * When omitted, uses get_option( '{slug}_{key}' ) / update_option( '{slug}_{key}' ).
47 * @since 1.1.21
48 */
49 public function __construct( $slug, $option_resolver = array() ) {
50 $this->slug = sanitize_key( $slug );
51 $this->option_resolver = wp_parse_args(
52 $option_resolver,
53 array(
54 'get' => null,
55 'update' => null,
56 )
57 );
58 }
59
60 /**
61 * Track an event. By default, skips if already tracked or pending (one-time semantics).
62 * When $force is true, the event is treated as retrackable — bypasses the post-send
63 * dedup check and overwrites any pending entry with the same name. Useful for
64 * recurring events like `plugin_updated` where the latest value should always win.
65 * Only stores temporary data — cleaned up after analytics send.
66 *
67 * @param string $event_name Event identifier.
68 * @param string $event_value Primary value (version, form ID, mode, etc.).
69 * @param array<string, mixed> $properties Additional context as key-value pairs. Values are stored as-is — sanitization is the caller's responsibility.
70 * @param bool $force When true, bypass pushed dedup and overwrite pending entry. Default false.
71 * @since 1.1.21
72 * @since 1.1.25 Added the $force parameter.
73 * @return void
74 */
75 public function track( $event_name, $event_value = '', $properties = array(), $force = false ) {
76 // Sanitize inputs once upfront — ensures dedup comparisons match stored values.
77 $event_name = sanitize_text_field( $event_name );
78 $event_value = sanitize_text_field( (string) $event_value );
79 $properties = is_array( $properties ) ? $properties : array();
80 $force = (bool) $force;
81
82 // Check dedup flag — already sent in a previous cycle.
83 // Force bypasses this check; pushed list will be refreshed on next flush_pending().
84 if ( ! $force ) {
85 $pushed = $this->get_option( 'usage_events_pushed', array() );
86 $pushed = is_array( $pushed ) ? $pushed : array();
87 if ( in_array( $event_name, $pushed, true ) ) {
88 return;
89 }
90 }
91
92 // Check if already queued in current cycle.
93 $pending = $this->get_option( 'usage_events_pending', array() );
94 $pending = is_array( $pending ) ? $pending : array();
95
96 $new_event = array(
97 'event_name' => $event_name,
98 'event_value' => $event_value,
99 'properties' => $properties,
100 'date' => current_time( 'mysql' ),
101 );
102
103 if ( ! $force ) {
104 // Default path: cheap membership check — no need to locate the key.
105 if ( in_array( $event_name, array_column( $pending, 'event_name' ), true ) ) {
106 return;
107 }
108 $pending[] = $new_event;
109 } else {
110 // Force path: locate any existing entry by actual key to overwrite safely.
111 $existing_key = null;
112 foreach ( $pending as $key => $entry ) {
113 if ( isset( $entry['event_name'] ) && $entry['event_name'] === $event_name ) {
114 $existing_key = $key;
115 break;
116 }
117 }
118
119 if ( null !== $existing_key ) {
120 // Skip the write when nothing material changed (only `date` would differ).
121 $existing = $pending[ $existing_key ];
122 if ( array_key_exists( 'event_value', $existing )
123 && array_key_exists( 'properties', $existing )
124 && $existing['event_value'] === $new_event['event_value']
125 && $existing['properties'] === $new_event['properties'] ) {
126 return;
127 }
128 $pending[ $existing_key ] = $new_event;
129 } else {
130 $pending[] = $new_event;
131 }
132 }
133
134 $this->update_option( 'usage_events_pending', $pending );
135 }
136
137 /**
138 * Flush pending events: returns them for the payload, then cleans up.
139 *
140 * After this call:
141 * - usage_events_pending is EMPTY (full event data deleted).
142 * - usage_events_pushed has event_name strings added (minimal dedup).
143 *
144 * @since 1.1.21
145 * @return array Pending events to include in payload. Empty if none.
146 */
147 public function flush_pending() {
148 $pending = $this->get_option( 'usage_events_pending', array() );
149 if ( empty( $pending ) || ! is_array( $pending ) ) {
150 return array();
151 }
152
153 // Add event names to dedup flag (minimal — just strings).
154 $pushed = $this->get_option( 'usage_events_pushed', array() );
155 $pushed = is_array( $pushed ) ? $pushed : array();
156 $pushed = array_unique(
157 array_merge( $pushed, array_column( $pending, 'event_name' ) )
158 );
159 $this->update_option( 'usage_events_pushed', $pushed );
160
161 // DELETE all temporary event data.
162 $this->update_option( 'usage_events_pending', array() );
163
164 return $pending;
165 }
166
167 /**
168 * Remove specific event names from the pushed dedup flag, allowing them to be re-tracked.
169 *
170 * Pass an array of event names to remove only those entries.
171 * Pass an empty array (or omit) to clear all pushed events.
172 *
173 * @param array<string> $event_names Event names to remove. Empty = clear all.
174 * @since 1.1.21
175 * @return void
176 */
177 public function flush_pushed( $event_names = array() ) {
178 $pushed = $this->get_option( 'usage_events_pushed', array() );
179 $pushed = is_array( $pushed ) ? $pushed : array();
180
181 if ( empty( $event_names ) ) {
182 $this->update_option( 'usage_events_pushed', array() );
183 return;
184 }
185
186 $pushed = array_values( array_diff( $pushed, $event_names ) );
187 $this->update_option( 'usage_events_pushed', $pushed );
188 }
189
190 /**
191 * Check if an event has already been tracked (sent or pending).
192 *
193 * @param string $event_name Event identifier.
194 * @since 1.1.21
195 * @return bool
196 */
197 public function is_tracked( $event_name ) {
198 $pushed = $this->get_option( 'usage_events_pushed', array() );
199 $pushed = is_array( $pushed ) ? $pushed : array();
200 if ( in_array( $event_name, $pushed, true ) ) {
201 return true;
202 }
203
204 $pending = $this->get_option( 'usage_events_pending', array() );
205 $pending = is_array( $pending ) ? $pending : array();
206 return in_array( $event_name, array_column( $pending, 'event_name' ), true );
207 }
208
209 /**
210 * Get an option value using custom resolver or default WordPress option.
211 *
212 * @param string $key Option key (e.g. 'usage_events_pending').
213 * @param mixed $default Default value.
214 * @return mixed
215 */
216 private function get_option( $key, $default = null ) {
217 if ( is_callable( $this->option_resolver['get'] ) ) {
218 return call_user_func( $this->option_resolver['get'], $key, $default );
219 }
220 return get_option( $this->slug . '_' . $key, $default );
221 }
222
223 /**
224 * Update an option value using custom resolver or default WordPress option.
225 *
226 * @param string $key Option key (e.g. 'usage_events_pending').
227 * @param mixed $value Value to store.
228 * @return void
229 */
230 private function update_option( $key, $value ) {
231 if ( is_callable( $this->option_resolver['update'] ) ) {
232 call_user_func( $this->option_resolver['update'], $key, $value );
233 return;
234 }
235 update_option( $this->slug . '_' . $key, $value );
236 }
237 }
238 }
239