PluginProbe
SEO Engine – Smart SEO with AI, Schema & Redirection for WordPress / trunk
SEO Engine – Smart SEO with AI, Schema & Redirection for WordPress vtrunk
0.9.4 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 0.8.6 0.8.5 0.8.4 0.8.2 0.8.1 0.7.9 0.8.0 0.7.7 0.7.8 0.7.6 0.7.5 0.7.4 0.7.3 0.7.2 0.7.1 0.7.0 0.6.5 All 88 releases
seo-engine / classes / modules / plausibleanalytics.php

plausibleanalytics.php in SEO Engine – Smart SEO with AI, Schema & Redirection for WordPress trunk, at classes/modules/plausibleanalytics.php

433 lines 12.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 // TODO [2025]: Refactor to unified analytics provider interface
4 class Meow_MWSEO_Modules_PlausibleAnalytics
5 {
6 private $core = null;
7 private $api_key = null;
8 private $server_url = null;
9 private $site_id = null;
10 private $use_cache = false;
11 private $disabled_tracking = false;
12
13 const TRANSIENT_REPORT_PREFIX = 'mwseo_plausible_analytics_report_';
14
15 public function __construct( $core )
16 {
17 $this->core = $core;
18 $this->init();
19 $this->tracking();
20 }
21
22 /**
23 * Initialize the module and load settings.
24 */
25 public function init()
26 {
27 $this->api_key = $this->core->get_option( 'plausible_api_key', '' );
28 $this->server_url = $this->core->get_option( 'plausible_server_url', 'https://plausible.io' );
29 $this->site_id = $this->core->get_option( 'plausible_site_id', '' );
30 $this->use_cache = $this->core->get_option( 'analytics_cache', false );
31
32 // Remove trailing slash from server URL
33 $this->server_url = rtrim( $this->server_url, '/' );
34
35 if ( empty( $this->api_key ) || empty( $this->site_id ) ) {
36 return;
37 }
38 }
39
40 /**
41 * Initialize tracking script injection.
42 */
43 public function tracking() {
44
45 if ( is_admin() ) { return; }
46
47 $this->disabled_tracking = $this->core->get_option( 'plausible_analytics_tracking_disabled', false );
48
49 if ( $this->disabled_tracking || empty( $this->site_id ) ) {
50 return;
51 }
52
53 add_action( 'wp_enqueue_scripts', array( $this, 'wp_enqueue_tracking_scripts' ) );
54 }
55
56 public function wp_enqueue_tracking_scripts() {
57
58 // Don't track logged-in users
59 $is_logged_in = is_user_logged_in();
60 if ( $is_logged_in ) {
61 $track_logged_users = $this->core->get_option( 'plausible_track_logged_users', false );
62 if ( !$track_logged_users ) { return; }
63
64 // Don't track editors and admins
65 $is_power_user = current_user_can( 'editor' ) || current_user_can( 'administrator' );
66 if ( $is_power_user ) {
67 $track_power_users = $this->core->get_option( 'plausible_track_power_users', false );
68 if ( !$track_power_users ) { return; }
69 }
70 }
71
72 add_filter( 'script_loader_tag', array( $this, 'script_loader_tag' ), 10, 2 );
73
74 // Enqueue the Plausible Analytics script
75 $script_url = $this->server_url . '/js/script.js';
76 wp_register_script( 'mwseo-analytics-plausible', $script_url, array(), null, true );
77 wp_enqueue_script( 'mwseo-analytics-plausible' );
78 }
79
80 function script_loader_tag($tag, $handle)
81 {
82 if ($handle === 'mwseo-analytics-plausible') {
83 // Add defer and data-domain attributes to Plausible script
84 $tag = str_replace('<script src=', '<script defer data-domain="' . esc_attr( $this->site_id ) . '" src=', $tag);
85 return $tag;
86 }
87 return $tag;
88 }
89
90 /**
91 * Check if Plausible Analytics is configured.
92 */
93 public function is_configured() {
94 return ! empty( $this->api_key ) && ! empty( $this->site_id ) && ! empty( $this->server_url );
95 }
96
97 /**
98 * Make an API request to Plausible.
99 */
100 private function make_request( $endpoint, $params = array() ) {
101 if ( ! $this->is_configured() ) {
102 throw new Exception( 'Plausible Analytics is not configured.' );
103 }
104
105 $url = $this->server_url . '/api/v1/' . ltrim( $endpoint, '/' );
106
107 // Add site_id to params
108 $params['site_id'] = $this->site_id;
109
110 // Build query string
111 $url .= '?' . http_build_query( $params );
112
113 $response = wp_remote_get( $url, array(
114 'headers' => array(
115 'Authorization' => 'Bearer ' . $this->api_key,
116 ),
117 'timeout' => 30,
118 ) );
119
120 if ( is_wp_error( $response ) ) {
121 $error_message = $response->get_error_message();
122 throw new Exception( 'Plausible API request failed: ' . $error_message );
123 }
124
125 $code = wp_remote_retrieve_response_code( $response );
126 $body = wp_remote_retrieve_body( $response );
127
128 if ( $code !== 200 ) {
129 throw new Exception( 'Plausible API returned status code: ' . $code );
130 }
131
132 $data = json_decode( $body, true );
133
134 if ( json_last_error() !== JSON_ERROR_NONE ) {
135 throw new Exception( 'Failed to parse Plausible API response.' );
136 }
137
138 return $data;
139 }
140
141 /**
142 * Get analytics data for a time series.
143 */
144 // TODO [2025]: Refactor to unified analytics provider interface
145 public function get_data( $args = array() ) {
146 $start_date = isset( $args['start_date'] ) ? $args['start_date'] : date( 'Y-m-d', strtotime( '-30 days' ) );
147 $end_date = isset( $args['end_date'] ) ? $args['end_date'] : date( 'Y-m-d' );
148 $period = isset( $args['group_by'] ) ? $this->convert_group_by_to_period( $args['group_by'] ) : 'day';
149
150 $cache_key = self::TRANSIENT_REPORT_PREFIX . md5( $start_date . $end_date . $period );
151
152 if ( $this->use_cache ) {
153 $cached_data = get_transient( $cache_key );
154 if ( $cached_data !== false ) {
155 return $cached_data;
156 }
157 }
158
159 try {
160 $params = array(
161 'period' => 'custom',
162 'date' => $start_date . ',' . $end_date,
163 'metrics' => 'visitors,pageviews,bounce_rate,visit_duration',
164 );
165
166 $data = $this->make_request( 'stats/timeseries', $params );
167
168 // Transform Plausible format to our format
169 $results = isset( $data['results'] ) ? $data['results'] : array();
170
171 $transformed = $this->transform_timeseries_data( $results, $start_date, $end_date );
172
173 if ( $this->use_cache ) {
174 set_transient( $cache_key, $transformed, 12 * HOUR_IN_SECONDS );
175 }
176
177 return $transformed;
178
179 } catch ( Exception $e ) {
180 return array();
181 }
182 }
183
184 /**
185 * Get analytics summary.
186 */
187 // TODO [2025]: Refactor to unified analytics provider interface
188 public function get_summary( $start_date = null, $end_date = null ) {
189 if ( ! $start_date ) {
190 $start_date = date( 'Y-m-d', strtotime( '-30 days' ) );
191 }
192 if ( ! $end_date ) {
193 $end_date = date( 'Y-m-d' );
194 }
195
196 $cache_key = self::TRANSIENT_REPORT_PREFIX . 'summary_' . md5( $start_date . $end_date );
197
198 if ( $this->use_cache ) {
199 $cached_data = get_transient( $cache_key );
200 if ( $cached_data !== false ) {
201 return $cached_data;
202 }
203 }
204
205 try {
206 $params = array(
207 'period' => 'custom',
208 'date' => $start_date . ',' . $end_date,
209 'metrics' => 'visitors,visits,pageviews,views_per_visit,bounce_rate,visit_duration',
210 );
211
212 $data = $this->make_request( 'stats/aggregate', $params );
213
214 // Map Plausible data to expected format
215 $summary = array(
216 'total_visits' => isset( $data['results']['visits']['value'] ) ? $data['results']['visits']['value'] : 0,
217 'unique_visitors' => isset( $data['results']['visitors']['value'] ) ? $data['results']['visitors']['value'] : 0,
218 'unique_posts' => 0, // Plausible doesn't provide this
219 'logged_in_visits' => 0, // Plausible doesn't track this
220 'bounce_rate' => isset( $data['results']['bounce_rate']['value'] ) ? $data['results']['bounce_rate']['value'] : 0,
221 // Additional Plausible-specific metrics
222 'pageviews' => isset( $data['results']['pageviews']['value'] ) ? $data['results']['pageviews']['value'] : 0,
223 'views_per_visit' => isset( $data['results']['views_per_visit']['value'] ) ? $data['results']['views_per_visit']['value'] : 0,
224 'visit_duration' => isset( $data['results']['visit_duration']['value'] ) ? $data['results']['visit_duration']['value'] : 0,
225 );
226
227 if ( $this->use_cache ) {
228 set_transient( $cache_key, $summary, 12 * HOUR_IN_SECONDS );
229 }
230
231 return $summary;
232
233 } catch ( Exception $e ) {
234 return array(
235 'total_visits' => 0,
236 'unique_visitors' => 0,
237 'unique_posts' => 0,
238 'logged_in_visits' => 0,
239 'bounce_rate' => 0,
240 'pageviews' => 0,
241 'views_per_visit' => 0,
242 'visit_duration' => 0,
243 );
244 }
245 }
246
247 /**
248 * Visitors on the site right now. The realtime endpoint answers with a bare number.
249 */
250 public function get_realtime_data() {
251 if ( ! $this->is_configured() ) {
252 return array();
253 }
254
255 try {
256 $data = $this->make_request( 'stats/realtime/visitors' );
257
258 return array( 'active_users' => (int) $data );
259 } catch ( Exception $e ) {
260 return array();
261 }
262 }
263
264 /**
265 * Get top posts/pages.
266 */
267 // TODO [2025]: Refactor to unified analytics provider interface
268 public function get_post_analytics( $page_path, $start_date = null, $end_date = null ) {
269 if ( ! $start_date ) {
270 $start_date = date( 'Y-m-d', strtotime( '-30 days' ) );
271 }
272 if ( ! $end_date ) {
273 $end_date = date( 'Y-m-d' );
274 }
275
276 try {
277 $params = array(
278 'period' => 'custom',
279 'date' => $start_date . ',' . $end_date,
280 'metrics' => 'visitors,pageviews,bounce_rate,visit_duration',
281 'filters' => 'event:page==' . $page_path,
282 );
283
284 $data = $this->make_request( 'stats/aggregate', $params );
285
286 if ( empty( $data['results'] ) ) {
287 return array();
288 }
289
290 $results = $data['results'];
291 return array(
292 'visits' => isset( $results['pageviews']['value'] ) ? (int) $results['pageviews']['value'] : 0,
293 'unique_visitors' => isset( $results['visitors']['value'] ) ? (int) $results['visitors']['value'] : 0,
294 'pageviews' => isset( $results['pageviews']['value'] ) ? (int) $results['pageviews']['value'] : 0,
295 'bounce_rate' => isset( $results['bounce_rate']['value'] ) ? (float) $results['bounce_rate']['value'] : 0,
296 'avg_time_on_page' => isset( $results['visit_duration']['value'] ) ? (int) $results['visit_duration']['value'] : 0,
297 'page_path' => $page_path
298 );
299 } catch ( Exception $e ) {
300 return array();
301 }
302 }
303
304 public function get_top_posts( $start_date = null, $end_date = null, $limit = 10 ) {
305 if ( ! $start_date ) {
306 $start_date = date( 'Y-m-d', strtotime( '-30 days' ) );
307 }
308 if ( ! $end_date ) {
309 $end_date = date( 'Y-m-d' );
310 }
311
312 $cache_key = self::TRANSIENT_REPORT_PREFIX . 'top_posts_' . md5( $start_date . $end_date . $limit );
313
314 if ( $this->use_cache ) {
315 $cached_data = get_transient( $cache_key );
316 if ( $cached_data !== false ) {
317 return $cached_data;
318 }
319 }
320
321 try {
322 $params = array(
323 'period' => 'custom',
324 'date' => $start_date . ',' . $end_date,
325 'property' => 'event:page',
326 'metrics' => 'visitors,pageviews,bounce_rate,visit_duration',
327 'limit' => $limit,
328 );
329
330 $data = $this->make_request( 'stats/breakdown', $params );
331
332 $top_posts = array();
333
334 if ( isset( $data['results'] ) && is_array( $data['results'] ) ) {
335 foreach ( $data['results'] as $result ) {
336 $page_path = isset( $result['page'] ) ? $result['page'] : '';
337
338 // Try to get post ID from path
339 $post_id = url_to_postid( home_url( $page_path ) );
340
341 // Get post data if post_id is valid
342 $post_title = 'Untitled';
343 $post_url = home_url( $page_path );
344 $post_type = 'page';
345
346 if ( $post_id > 0 ) {
347 $post = get_post( $post_id );
348 if ( $post ) {
349 $post_title = $post->post_title;
350 $post_url = get_permalink( $post_id );
351 $post_type = $post->post_type;
352 }
353 }
354
355 $top_posts[] = array(
356 'post_id' => $post_id,
357 'post_title' => $post_title,
358 'post_url' => $post_url,
359 'post_type' => $post_type,
360 'path' => $page_path,
361 'visits' => isset( $result['pageviews'] ) ? $result['pageviews'] : 0, // Map pageviews to visits
362 'unique_visitors' => isset( $result['visitors'] ) ? $result['visitors'] : 0, // Map visitors to unique_visitors
363 'bounce_rate' => isset( $result['bounce_rate'] ) ? $result['bounce_rate'] : 0,
364 'visit_duration' => isset( $result['visit_duration'] ) ? $result['visit_duration'] : 0,
365 );
366 }
367 }
368
369 if ( $this->use_cache ) {
370 set_transient( $cache_key, $top_posts, 12 * HOUR_IN_SECONDS );
371 }
372
373 return $top_posts;
374
375 } catch ( Exception $e ) {
376 return array();
377 }
378 }
379
380 /**
381 * Transform Plausible timeseries data to our format.
382 */
383 private function transform_timeseries_data( $results, $start_date, $end_date ) {
384 if ( empty( $results ) ) {
385 return array();
386 }
387
388 $transformed = array();
389
390 foreach ( $results as $result ) {
391 $date = isset( $result['date'] ) ? $result['date'] : '';
392
393 $transformed[] = array(
394 'period' => $date, // Chart expects 'period' not 'date'
395 'visits' => isset( $result['pageviews'] ) ? $result['pageviews'] : 0, // Map pageviews to visits
396 'unique_visitors' => isset( $result['visitors'] ) ? $result['visitors'] : 0, // Map visitors to unique_visitors
397 'unique_posts' => 0, // Not tracked by Plausible
398 'bounce_rate' => isset( $result['bounce_rate'] ) ? $result['bounce_rate'] : 0,
399 'visit_duration' => isset( $result['visit_duration'] ) ? $result['visit_duration'] : 0,
400 );
401 }
402
403 return $transformed;
404 }
405
406 /**
407 * Convert our group_by format to Plausible's period format.
408 */
409 private function convert_group_by_to_period( $group_by ) {
410 $map = array(
411 'day' => 'day',
412 'week' => 'week',
413 'month' => 'month',
414 'year' => 'year',
415 );
416
417 return isset( $map[$group_by] ) ? $map[$group_by] : 'day';
418 }
419
420 /**
421 * Clear all cached reports.
422 */
423 public function clear_cache() {
424 global $wpdb;
425
426 $pattern = '_transient_' . self::TRANSIENT_REPORT_PREFIX . '%';
427 $wpdb->query( $wpdb->prepare(
428 "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
429 $pattern
430 ) );
431 }
432 }
433