PluginProbe ʕ •ᴥ•ʔ
LatePoint – Calendar Booking Plugin for Appointments and Events / 5.6.3
LatePoint – Calendar Booking Plugin for Appointments and Events v5.6.3
5.6.6 5.6.5 5.6.4 5.6.3 5.6.2 5.6.1 5.6.0 5.5.2 5.5.1 5.5.0 5.4.2 trunk 5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.1.8 5.1.9 5.1.91 5.1.92 5.1.93 5.1.94 5.2.0 5.2.1 5.2.10 5.2.11 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7 5.2.8 5.2.9 5.3.0 5.3.1 5.3.2 5.4.0 5.4.1
latepoint / lib / kit / bsf-analytics / class-bsf-analytics-events.php
latepoint / lib / kit / bsf-analytics Last commit date
assets 4 months ago classes 4 months ago modules 1 week ago class-bsf-analytics-events.php 2 months ago class-bsf-analytics-loader.php 4 months ago class-bsf-analytics-stats.php 2 months ago class-bsf-analytics.php 1 week ago version.json 1 week ago
class-bsf-analytics-events.php
239 lines
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